Approche fonctionnelle vs. approche objet (suite...)
 

 

q 

La séparation des données et des traitements : le piège !

Examinons le problème de l'évolution de code fonctionnel plus en détail...

Faire évoluer une application de gestion de bibliothèque pour gérer une médiathèque, afin de prendre en compte de nouveaux types d'ouvrages (cassettes vidéo, CD-ROM, etc...), nécessite :

  • de faire évoluer les structures de données qui sont manipulées par les fonctions,
  • d'adapter les traitements, qui ne manipulaient à l'origine qu'un seul type de document (des livres).

Il faudra donc modifier toutes les portions de code qui utilisent la base documentaire, pour gérer les données et les actions propres aux différents types de documents.

Il faudra par exemple modifier la fonction qui réalise l'édition des "lettres de rappel" (une lettre de rappel est une mise en demeure, qu'on envoie automatiquement aux personnes qui tardent à rendre un ouvrage emprunté). Si l'on désire que le délai avant rappel varie selon le type de document emprunté, il faut prévoir une règle de calcul pour chaque type de document.

En fait, c'est la quasi-totalité de l'application qui devra être adaptée, pour gérer les nouvelles données et réaliser les traitements correspondants. Et cela, à chaque fois qu'on décidera de gérer un nouveau type de document !

 

  struct Document
{
  char nom_doc[50];
 
Type_doc type;
  Bool est_emprunte;
  char emprunteur[50];
  struct tm date_emprunt;
} DOC[MAX_DOCS];

 
  void lettres_de_rappel()
{
  /* ... */
  for (i = 0; i < NB_DOCS; i++)
  {
    if (DOC[i].est_emprunte)
    {
      switch(DOC[i].type)
      {
     
case LIVRE:
        delai_avant_rappel = 20;
        break;
     
case CASSETTE_VIDEO:
        delai_avant_rappel = 7;
        break;
     
case CD_ROM:
        delai_avant_rappel = 5;
        break;
      }
    }
  }
/* ... */
}

 
   void mettre_a_jour(int ref)
   {
     /* ... */
     switch(DOC[ref].type)
     {
       
case LIVRE:
         maj_livre(DOC[ref]);
         break;
       
case CASSETTE_VIDEO:
         maj_k7(DOC[ref]);
         break;
       
case CD_ROM:
         maj_cd(DOC[ref]);
         break;
     }
     /* ... */
   }

 
q  1ère amélioration : rassembler les valeurs qui caractérisent un type, dans le type

Une solution relativement élégante à la multiplication des branches conditionnelles et des redondances dans le code (conséquence logique d'une trop grande ouverture des données), consiste tout simplement à centraliser dans les structures de données, les valeurs qui leurs sont propres :
 
struct Document
{
  char nom_doc[50];
  Type_doc type;
  Bool est_emprunte;
  char emprunteur[50];
  struct tm date_emprunt;
 
int delai_avant_rappel;
} DOC[MAX_DOCS];

 
void lettres_de_rappel()
{
  /* ... */
  for (i = 0; i < NB_DOCS; i++)
  {
    if (DOC[i].est_emprunte) /* SI LE DOC EST EMPRUNTE */
    {
      /* IMPRIME UNE LETTRE SI SON
      DELAI DE RAPPEL EST DEPASSE */

      if (date() >= (
DOC[i].date_emprunt + DOC[i].delai_avant_rappel))
          imprimer_rappel(DOC[i]);
    }
  }
}

Quoi de plus logique ? En effet, le "délai avant édition d'une lettre de rappel" est bien une caractéristique propre à tous les ouvrages gérés par notre application.

Mais cette solution n'est pas encore optimale !
 

 
q  2ème amélioration : centraliser les traitements associés à un type, auprès du type

Pourquoi ne pas aussi rassembler dans une même unité physique les types de données et tous les traitements associés ?

Que se passerait-il par exemple si l'on centralisait dans un même fichier, la structure de données qui décrit les documents et la fonction de calcul du délai avant rappel ? Cela nous permettrait de retrouver en un clin d'oeil le bout de code qui est chargé de calculer le délai avant rappel d'un document, puisqu'il se trouve au plus près de la structure de données concernée.

Ainsi, si notre médiathèque devait gérer un nouveau type d'ouvrage, il suffirait de modifier une seule fonction (qu'on sait retrouver instannément), pour assurer la prise en compte de ce nouveau type de document dans le calcul du délai avant rappel. Plus besoin de fouiller partout dans le code...

 
struct Document
{
  char nom_doc[50];
  Type_doc type;
  Bool est_emprunte;
  char emprunteur[50];
  struct tm date_emprunt;
 
int delai_avant_rappel;
} DOC[MAX_DOCS];

 
int calculer_delai_rappel(Type_doc type)
{
  switch(type)
  {
    case LIVRE:
      return 20;
    case CASSETTE_VIDEO:
      return 7;
    case CD_ROM:
      return 5;
    /* autres "case" bienvenus ici ! */
  }
}


Ecrit en ces termes, notre logiciel sera plus facile à maintenir et bien plus lisible. Le stockage et le calcul du délai avant rappel des documents, est désormais assuré par une seule et unique unité physique (quelques lignes de code, rapidement identifiables).
Pour accéder à la caractéristique "délai avant rappel" d'un document, il suffit de récupérer la valeur correspondante parmi les champs qui décrivent le document. Pour assurer la prise en compte d'un nouveau type de document dans le calcul du délai avant rappel, il suffit de modifier une seule fonction, située au même endroit que la structure de données qui décrit les documents :
 
void ajouter_document(int ref)
{
  DOC[ref].est_emprunte = FAUX;
  DOC[ref].nom_doc = saisir_nom();
  DOC[ref].type = saisir_type();
  DOC[ref].delai_avant_rappel =
calculer_delai_rappel(DOC[ref].type);
}

void afficher_document(int ref)
{
  printf("Nom du document: %s\n",DOC[ref].nom_doc);

  /* ... */

  printf("Delai avant rappel: %d jours\n",
DOC[ref].delai_avant_rappel);

  /* ... */
}

 

 


page précédente


sommaire

© uml@free.fr - tous droits réservés


page suivante