Méthodes avec Ada
Nous avons vu dans la première partie le rôle important joué par les langages du point de vue méthodologique. Dans cette deuxième partie, nous passons en revue les différentes grandes classes de méthodes de conception, en montrant comment Ada leur fournit un soutien actif, et ceci quelle que soit la méthode.
Car là est bien l’enjeu et l’ambition d’Ada : être le langage qui soutient toutes les méthodes.
=Les méthodes structurées=
Principes de la méthode.
La programmation structurée fut historiquement la première formalisation de l’analyse par décomposition descendante. Le principe de cette méthode est simple : on considère qu’un programme est constitué d’une suite d’actions élémentaires. On décompose donc au plus haut niveau le problème à résoudre en actions individuelles que l’on suppose résolues; on analyse ensuite chacune de ces actions en la décomposant sous forme d’actions de plus bas niveau, et ainsi de suite jusqu’à obtenir des actions considérées comme triviales et susceptibles de s’écrire directement dans le langage de programmation. La programmation structurée peut donc être définie comme une méthode dont le critère de décomposition horizontale est l’enchaînement des actions, et le critère de décomposition verticale la généralité des actions.
Le langage dont la philosophie se rapproche le plus de la programmation structurée est Pascal. On peut voir l’arbre de décomposition de la programmation structurée comme l’arbre d’imbrication des procédures d’un programme Pascal : un programme principal, qui se décompose en procédures de premier niveau, elles-mêmes décomposées en procédures de deuxième niveau, etc.
Ada, possédant un sous-ensemble équivalent à Pascal, permet bien entendu cette approche des problèmes. Il lui apporte cependant un perfectionnement important : il est possible d'imbriquer logiquement des modules, tout en conservant la possibilité de les compiler séparément. Le Pascal standard ne possède pas de mécanisme de compilation séparée. Les Pascal «étendus» (tels que Turbo Pascal par exemple) possèdent de tels mécanismes, mais à condition de ne pas conserver la notion d'imbrication. Le mécanisme des corps séparés permet, comme nous allons le voir dans l'exemple suivant, de compiler séparément des modules tout en conservant l'imbrication logique.
Exemple en programmation structurée.
Supposons que nous voulions imprimer un triangle de Pascal (le mathématicien, pas le langage !). Il s'agit d'un triangle de nombres, se présentant comme à la figure 11.
Figure 11 : Triangle de Pascal
Chaque élément d'une ligne s'obtient en faisant la somme de l'élément situé au-dessus de lui et de l'élément situé au-dessus et à gauche, le dernier élément à droite (qui n'a pas d'élément supérieur) étant toujours un 1. Pour résoudre le problème global (imprimer le triangle), il suffit de savoir résoudre deux sous-problèmes : calculer une ligne, et l'imprimer. Nous avons également besoin d'une structure de données pour communiquer entre ces deux étapes : la Ligne. Le programme principal peut s'écrire :
procedure Triangle_Pascal is
 Max : constant := 10;
 Ligne : array(1..Max) of INTEGER;
 procedure Calculer_Ligne (Numéro : INTEGER) is separate;
 procedure Imprimer_Ligne (Numéro : INTEGER) is separate;
begin
 for I in 1..Max loop
 Calculer_Ligne (Numéro => I);
 Imprimer_Ligne (Numéro => I);
 end loop;
end Triangle_Pascal;
Nous pouvons compiler ce programme pour vérifier qu'il ne contient pas d'incohérence à ce niveau de décomposition, alors que nous n'avons encore pris aucune décision de conception en ce qui concerne les niveaux inférieurs.
Ensuite, nous devons descendre d'un niveau en nous préoccupant de savoir comment nous allons réaliser les procédures Calculer_ligne et Imprimer_ligne. Comme les lignes sont calculées dans l'ordre, lorsque Calculer_ligne est appelé la variable Ligne contient la ligne précédente (d'ordre Numéro-1). Nous pouvons utiliser cette information pour calculer la ligne d'ordre N, à condition de calculer de droite à gauche, afin de ne pas «écraser» des valeurs dont nous avons encore besoin. Le premier élément valant toujours 1, il est inutile de le recalculer, et nous savons que le dernier vaut également toujours 1. Nous pouvons donc écrire :
separate (Triangle_Pascal)
procedure Calculer_Ligne(Numéro : Integer) is
begin
 Ligne (Numéro) := 1;
 for I in reverse 2..Numéro-1 loop
 Ligne (I) := Ligne (I) + Ligne(I-1);
 end loop;
end Calculer_ligne;
Au niveau d'abstraction supérieur, nous n'avions pas prévu de traitement spécial pour les premières lignes; nous devons donc vérifier ce qui se passe, pour éventuellement faire un cas spécial. Si Numéro vaut 1, la procédure met la valeur 1 dans Ligne(1), et la boucle n'est pas effectuée (Numéro-1 vaut 0, qui est plus petit que 2). Le traitement est donc correct. Si Numéro vaut 2, le premier élément est intouché, on met 1 dans le deuxième élément, et la boucle n'est pas effectuée : le traitement est encore correct. Enfin, à partir de 3, l'algorithme normal s'applique. Il n'y a donc pas à faire de cas particulier, ce qui n'était pas évident au départ.
Pour imprimer, nous pouvons représenter le comportement souhaité de la façon suivante : écrire la ligne d'ordre I revient à écrire les I premiers éléments, suivis d'un retour à la ligne. Noter qu'à ce niveau, nous ne savons pas comment écrire un élément : nous reportons ce problème à l'étape suivante. Nous pouvons donc écrire :
with Ada.Text_IO;
separate (Triangle_Pascal)
procedure Imprimer_Ligne (Numéro : Integer) is
 procedure Imprimer (Elem : Integer) is separate;
 use Ada.Text_IO;
begin
 for I in 1 .. Numéro loop
 Imprimer (Ligne (I));
 end loop;
 New_Line;
end Imprimer_Ligne;
Il ne nous reste plus qu'à analyser comment imprimer un élément simple.
Ada fournit des entrées-sorties prédéfinies sur le type Integer dans le paquetage Ada.Integer_Text_IO.
Nous allons donc utiliser ce composant :
with Ada.Integer_Text_IO;
separate (Triangle_Pascal.Imprimer_Ligne) 
procedure Imprimer (Elem : Integer) is
 use Ada.Integer_Text_IO;
begin
 Put (Elem, Width =>2);
end Imprimer;
Bien sûr, cet exemple est un peu exagérément détaillé, car son but est de montrer la démarche : pour imprimer un triangle de Pascal, on suppose que l'on sait calculer et imprimer une ligne. Pour imprimer une ligne, on suppose que l'on sait imprimer un élément. Chaque étape s'appuie sur des problèmes plus spécifiques, que l'on suppose résolus, puis que l'on implémente. Inversement, chaque étape de la résolution s'appuie sur une vue de plus en plus précise, mais de plus en plus étroite du problème : Imprimer_ligne ne traite que de l'impression, sans se préoccuper de la façon dont la ligne a été calculée. Imprimer s'occupe uniquement de l'impression d'un élément, sans savoir où ni pourquoi on le lui demande. Enfin, chaque niveau s'exécute séquentiellement dans le temps, et sait dans quel ordre il est appelé : c'est ce qui permet à Calculer_ligne d'utiliser le résultat du calcul précédent.
Critique de la méthode.
Lors de son émergence, la programmation structurée représentait un grand progrès par rapport aux méthodes, ou plutôt à l'absence de méthodes, existantes. Cependant, avec les progrès de l'informatique et l'extension du domaine des problèmes susceptibles d'être résolus de façon informatique, elle bute sur un certain nombre de difficultés que nous allons résumer maintenant.
Cette méthode conduit à une topologie de programme purement arborescente, comme illustrée par la figure 12. Chaque module est fait spécifiquement en fonction des exigences du niveau supérieur : ceci conduit à une architecture de programme très monolithique, semblable à un puzzle où chaque morceau est destiné à entrer à un endroit précis pour lequel il a été conçu. Il y a donc un couplage spatial entre les modules : comme un module «voit» tous les éléments qui lui sont supérieurs, il n'est pas possible de recopier un module dans une autre branche du programme. De plus, le développeur sait quand, dans le déroulement du programme, son module sera appelé : il peut donc faire des suppositions sur l'état des variables, ce que fera le prochain module appelé, etc. Ceci entraîne également un couplage temporel des modules : le même sous-programme, appelé depuis un endroit qui n'était pas celui initialement prévu, risque de ne plus fonctionner normalement.
Figure 12 : Topologie de programme en programmation structurée
Une autre conséquence de l'aspect monolithique des programmes ainsi développés est qu'il est très difficile d'autoriser les compilations séparées. Pascal en standard ne les permet pas. Si de nombreuses extensions de Pascal permettent de le faire, c'est toujours en répétant dans chaque module les déclarations globales qu'il est censé voir; une modification d'un de ces objets globaux doit être répercutée dans tous les modules qui l'utilisent, et le langage ne garantit absolument pas la cohérence des différentes déclarations. Tout ceci entraîne que les modules développés pour une application peuvent difficilement être réutilisés dans un contexte différent. Même à l'intérieur d'une même application, on trouve souvent des fonctionnalités très voisines, mais légèrement différentes, ce qui conduit à une duplication de codes quasi identiques. Ceci est d'ailleurs une conséquence inévitable de la hiérarchisation. C'est ce que nous allons montrer maintenant.
Il arrive souvent qu'une fonctionnalité identique se retrouve à différents endroits d'un même programme; on peut imaginer par exemple que les modules marqués X et Y sur la figure 12 aient tous deux besoin d'effectuer des lectures sur le terminal, et que l'on souhaite passer par une procédure permettant d'éditer la ligne par exemple. Dans la décomposition strictement arborescente de la programmation structurée, ceci s'exprimerait par le fait que deux modules, a priori totalement indépendants, expriment tous deux dans le cours de leurs actions la sous-action «lire une ligne». En suivant strictement la méthode, il faudrait écrire le même sous-programme deux fois dans deux modules distincts. C'est évidemment peu souhaitable; ce n'est pas tant la duplication de l'écriture qui est gênante (un bon éditeur de texte le fait en quelques touches) ni même l'espace mémoire gaspillé par la duplication du code (les machines ont des espaces mémoire importants de nos jours) : le problème réel vient de la maintenance. Si une modification doit être faite dans un de ces modules dupliqués, il faut la répercuter dans toutes les copies, et le risque est grand d'en oublier. On peut ainsi voir des erreurs corrigées à un endroit réapparaître soudainement à un autre; on risque également des problèmes d'incohérence : imaginez un programme où le caractère d'effacement serait Del par endroits et Backspace à d'autres!
La solution généralement adoptée consiste à remonter la fonctionnalité dans l'arbre de décomposition jusqu'à un endroit où elle sera visible de tous les modules qui l'utilisent, c'est-à-dire presque toujours jusqu'à la racine. Le module se situe alors à un niveau global qui ne correspond plus à la décomposition logique. Le même phénomène se produit pour les structures de données, lorsque deux parties du programme doivent à travailler sur une même variable. Les règles de visibilité exigent alors que la variable soit «remontée» dans l'arbre au moins jusqu'au nœud commun aux deux branches concernées.
Figure 13 : Dégénérescence de la topologie de programme en programmation structurée
On s'aperçoit donc qu'en pratique, la belle arborescence théorique décrite par la méthode dégénère pour aboutir à une topologie illustrée par la figure 13. On a alors des arborescences partielles, avec un regroupement important de fonctions et de structures de données au niveau de la racine. Remarquons qu'il s'agit d'une première entorse aux principes de la programmation structurée : des entités sont déclarées à un niveau où elles ne correspondent pas à une action élémentaire du niveau immédiatement supérieur.
Ce regroupement a une conséquence extrêmement néfaste : les éléments ainsi propulsés au niveau global sont alors visibles non seulement depuis les modules concernés, mais également depuis l'ensemble du programme. Si par exemple une variable ne doit être (logiquement) modifiée qu'en passant par l'intermédiaire d'une procédure de contrôle, on n'a plus aucune garantie qu'un programmeur «astucieux» n'a pas trouvé plus commode d'accéder à la variable directement. Il devient donc extrêmement difficile de contrôler quelle partie du programme utilise quelle autre partie, puisque toutes sont visibles entre elles. On a bien décomposé le programme en unités de tailles suffisamment petites pour être gérables, mais on a établi des liens de dépendance qui croissent rapidement, jusqu'à dépasser ce que l'on peut gérer; on ne sait plus alors très bien qui modifie quoi ou qui appelle qui dans le programme.
Notons pour terminer que la définition même de la programmation structurée suppose l'ordonnancement séquentiel des actions. Il n'est donc pas possible de l'utiliser pour définir des systèmes parallèles. On peut pallier cet inconvénient en définissant un système parallèle comme un ensemble de processus, individuellement séquentiels, mais s'exécutant en parallèle. La programmation structurée peut alors s'appliquer à l'analyse de chacun des processus, mais bien sûr l'analyse du comportement global requiert une méthode appropriée au parallélisme.
Ce problème de l'analyse des systèmes parallèles à été longtemps ignoré, ou tout au moins confiné à quelques domaines particuliers, car les langages courants n'offraient pas de possibilité de programmation parallèle, ce qui n'encourageait pas les développeurs de systèmes d'exploitation à développer le parallélisme, ce qui n'encourageait pas le développement de langages parallèles, etc. La situation change aujourd'hui, car la demande de toujours plus de puissance de calcul ne pourra être satisfaite qu'avec des machines parallèles, ce qui a amené l'introduction de processus légers (threads) au niveau des systèmes d'exploitation... ainsi bien entendu que l'apparition d'un langage de programmation industriel complet offrant, au même titre que ses autres fonctionnalités, un modèle simple de programmation parallèle.
Ada et la programmation structurée.
Pascal avait été conçu spécifiquement pour mettre en œuvre les principes de la programmation structurée. Ada a poursuivi dans cette voie, tout en ajoutant les éléments nécessaires à un langage d'envergure industrielle.
Utilisation des sous-programmes.
Ainsi que nous l'avons vu, Ada permet comme Pascal de traduire la hiérarchie des actions résultant de la programmation structurée sous forme d'imbrication de procédures, mais offre de plus la possibilité de compiler séparément les corps. On peut certes s'affranchir de la hiérarchie stricte en regroupant certains sous-programmes en paquetages; mais la hiérarchisation est toujours possible.
On a prétendu [Cla80] que cette possibilité d'imbrication était inutile, voir même nocive. Les langages comme C/C++ n'autorisent d'ailleurs pas l'imbrication de sous-programmes. Il est en effet toujours possible de mettre deux sous-programmes « à côté » l'un de l'autre, au lieu de les imbriquer, avec sensiblement les mêmes fonctionnalités :
La différence vient encore de la maintenance : si un sous-programme semble appelé à tort et qu'il est déclaré au niveau le plus externe, on doit jeter la suspicion sur l'ensemble du programme ; si au contraire il est déclaré à l'intérieur d'un autre sous-programme, l'appel incorrect a forcément lieu depuis l'intérieur de cet autre sous-programme, réduisant considérablement le champ d'investigation nécessaire pour retrouver l'erreur.
Ada permet donc de traduire la structure de la conception directement dans la structure du programme, tout en conservant la possibilité de compilation séparée. Une difficulté est que si l'on modifie une couche haute, il faudra recompiler tous les éléments (séparés) de plus bas niveau; mais ceci est un problème inhérent à la méthode, où une modification des couches hautes a des conséquences sur toutes les couches inférieures; le langage ne fait que répercuter les propriétés de la méthode.
Utilisation des paquetages.
En programmation structurée, les paquetages servent essentiellement à regrouper soit des structures de données, soit des structures de programmes. On trouvera donc, selon la classification établie par Booch, essentiellement des et des . Les principes de la programmation structurée impliquant la visibilité des données, on trouvera rarement des parties privées dans les paquetages utilisés avec cette méthode.
Utilisation des exceptions.
Les canons de la programmation structurée ne connaissent pas la notion de déroutement; l'utilisation des exceptions sera donc généralement limitée si l'on applique strictement la méthode. D'ailleurs, les règles de programmation associées aux méthodologies en programmation structurée (surtout pour les systèmes à haute sécurité) déconseillent ou interdisent l'utilisation des exceptions. L'avantage de la programmation structurée est qu'elle permet de suivre directement l'exécution du programme, propriété qui n'est plus vérifiée en présence d'exceptions. Malgré tout, il faut bien définir une politique d'erreur (nous en discuterons dans la quatrième partie); mais on utilisera plutôt une technique par codes de retour.
On ne peut cependant ignorer totalement les exceptions : d'une part, les composants logiciels n'ont pas d'autre solution que de les utiliser en cas d'impossibilité de rendre le service demandé; d'autre part, une anomalie du programme peut conduire à la levée d'une exception prédéfinie. On évitera donc en général de lever des exceptions, mais on devra prévoir quand même des traite-exceptions de sécurité. En cas de levée d'exception imprévue, on appellera la procédure de gestion normale des situations erronées.
Utilisation des génériques.
Nous avons vu que la réutilisation de code en programmation structurée impliquait soit de le dupliquer, ce qui posait des problèmes de maintenance, soit de le remonter à un niveau global, ce qui violait la décomposition logique. Les génériques d'Ada offrent une solution élégante à ce problème. Supposons par exemple que nous ayons besoin de la procédure Lire_Ligne à plusieurs endroits du programme; cette procédure a pour mission de lire une ligne au terminal (en la complétant avec des espaces, éventuellement dans une fenêtre, avec des fonctions d'édition...) et de la mettre dans une certaine variable de la procédure englobante. Initialement, le programmeur avait écrit :
procedure Utilisatrice is
 Mon_Tampon : String(1..80)
 procedure Lire_Ligne is
 Longueur : Natural;
 begin
 Get (Mon_Tampon, Longueur);
 Mon_Tampon(Longueur+1 .. Mon_Tampon'Last) := (others => ' ');
 end Lire_Ligne;
begin
end Utilisatrice;
Lorsqu'apparaît le besoin de réutiliser cette procédure dans un autre contexte, le programmeur ne duplique pas le code, mais le transforme en générique :
generic
 Tampon : in out String;
procedure Lire_Ligne_Générique is
 Longueur : Natural;
begin
 Get (Tampon, Longueur);
 Tampon(Longueur+1 .. Tampon'Last) := (others => ' ');
end Lire_Ligne_Générique;
Il peut ensuite, dans l'ancienne procédure ainsi que dans toutes celles qui en ont besoin, écrire :
procedure Utilisatrice is
 Mon_Tampon : String(1..80)
 procedure Lire_Ligne is new Lire_Ligne_Générique (Mon_Tampon);
begin
end Utilisatrice;
Ainsi, chaque procédure utilisatrice dispose de sa propre procédure locale, respectant la structure logique, mais le code n'est écrit qu'une fois, et toute modification de maintenance qui lui serait apportée sera automatiquement reportée dans toutes les instanciations. Les génériques permettent donc d'apporter à la programmation structurée une forme de réutilisation qui ne perturbe pas la structure totalement hiérarchique de l'application.
Utilisation du parallélisme.
Nous avons vu que les méthodes en programmation structurée se prêtent mal au développement de systèmes parallèles. Il faut cependant parfois définir des activités parallèles dans des systèmes développés en programmation structurée. Dans ce cas, on décompose le système en tâches indépendantes dès les premières étapes de la conception, ce qui permet ensuite de poursuivre l'analyse de chacune d'elles en programmation structurée «normale».
On trouvera donc surtout des tâches Ada définies directement dans des paquetages de bibliothèque, se comportant essentiellement comme des programmes principaux multiples. En Ada 95, on pourra également utiliser le modèle d'exécution distribuée qui permet effectivement d'avoir plusieurs programmes principaux faiblement couplés.
Notons encore que pour un système développé en programmation structurée ayant besoin de parallélisme, il est particulièrement utile de disposer d'un langage offrant un mécanisme de tâches intrinsèque : le surcroît de complexité est alors négligeable. Autrement, il faut soit s'appuyer sur les primitives d'un exécutif temps réel particulier, mais on y perd la portabilité, soit écrire soi-même son propre système de gestion des tâches, mais l'effort supplémentaire est loin d'être négligeable, et il risque souvent d'imposer des contraintes à travers toutes les couches du logiciel. C'est ainsi que les normes de programmation de systèmes graphiques tels que Windows ou XWindow demandent à toutes les applications utilisatrices de se bloquer volontairement périodiquement, pour permettre au système de .
Exercices.
=Les méthodes orientées objet=
Principes des méthodes orientées objet.
Les méthodes orientées objet sont nées du désir de remédier aux problèmes de la programmation structurée, sans en perdre bien entendu les avantages. Le principal mérite de la programmation structurée vient de ce qu'elle procure une méthode rationnelle, progressive, de décomposition des programmes. Celle-ci s'effectue de façon descendante, ce qui permet de mettre en place la structure générale sans se soucier des détails d'implémentation. Mais son problème essentiel vient du couplage des différents éléments de la décomposition entre eux. Ce que l'on cherche donc, c'est une méthode descendante (par raffinements successifs), mais produisant des modules à faible couplage. De plus, les modules doivent représenter chacun une et une seule entité logique, c'est-à-dire qu'ils doivent être à forte cohésion (il ne doit pas être possible de diviser un module en deux tout en conservant le faible couplage).
Définition.
Pour remédier aux défauts de la programmation structurée, il convenait de remettre en cause son fondement même : la décomposition en actions. L'origine de cette réflexion se trouve dans un article de Parnas [Par71] intitulé « À propos des critères à utiliser pour décomposer un système en modules ». Il s'agissait bien d'une remise en cause des critères de décomposition.
Mais comment définit-on ce qu'est une abstraction? Prenons un exemple. Il est vraisemblable que vous qui lisez ces lignes possédez une voiture. Vous venez de lire la phrase précédente, et vous n'avez eu aucune peine à saisir sa signification. Pourtant possédez-vous vraiment une «voiture»? Non, vous possédez un certain ensemble de tôles et de plastique que vous identifiez comme appartenant à une classe appelée «voiture»; vous possédez donc un objet qui est une voiture, mais beaucoup d'autres objets ayant peu de rapport avec votre chère auto sont également des voitures. Le terme de voiture est une abstraction, qui permet de regrouper un certain nombre d'entités différentes.
Comment donc reconnaître si un objet donné est une voiture ou non? Pour avoir droit à ce titre, il faut que l'objet en question ait quatre roues, des sièges et un moteur. Nous pouvons donc définir un certain nombre de valeurs qui sont caractéristiques de la classe voiture. Mais ceci ne suffit pas à la caractériser : un avion pourrait très bien posséder les caractéristiques précédentes. Il faut en plus spécifier que la voiture sert à transporter des gens sur la route. Nous devons donc définir, en plus des valeurs, un certain nombre d'actions que la voiture peut effectuer ou subir.
On peut donc définir une abstraction comme une réduction d'une classe d'objets à un ensemble de valeurs et d'opérations communes à tous les objets de la classe, et permettant de la définir entièrement.
Le point important de cette définition est la réduction. D'ailleurs, le terme abstraire ne signifie-t-il pas étymologiquement «retirer de»? Par exemple, une propriété d'une voiture est la composition chimique de l'acier dans lequel a été fabriqué le vilebrequin. C'est une propriété inutile pour la vue de la voiture qu'a le conducteur moyen (mais pas pour l'ingénieur qui a conçu la voiture). L'abstraction va donc consister à extraire de l'énorme masse de propriétés caractérisant les objets un petit nombre d'entre elles qui vont être suffisantes pour l'idée que l'on se fait de l'objet à un moment donné. C'est donc essentiellement ce phénomène de réduction, de projection de l'objet réel dans l'espace de problème, qui va caractériser le phénomène d'abstraction.
Avec cette technique, la définition des objets s'appuie sur les propriétés naturelles de l'abstraction du monde réel. Un programme n'est donc plus une suite d'instructions à faire exécuter par une machine, mais une description d'un certain modèle du monde réel. Il s'ensuit une réorganisation complète du logiciel : si les instructions élémentaires se retrouvent quasi identiques quelle que soit la méthode de décomposition choisie, elles ne sont pas regroupées en modules de la même façon. Il n'est pas étonnant que l'ancêtre de tous les langages à objets, Simula, ait été un langage de simulation : dans quel autre contexte aurait-on plus besoin de représenter les entités du monde réel?
De cette différence fondamentale découlent des propriétés partagées par toutes les approches objet :
Les objets informatiques.
Un objet informatique sera donc une entité de modularisation qui rassemblera les structures de données et les opérations pertinentes pour l'utilisation qui est faite de l'objet du point de vue informatique. Différentes façons d'utiliser la notion d'objet sont possibles, que nous allons maintenant passer en revue. Bien qu'il y ait toujours possibilité d'exceptions, l'expérience montre que les «bons objets» correspondent à ces catégories. On vérifiera donc lors de la conception que ce que l'on fait correspond aux critères ci-dessous; sinon, il faut étudier s'il n'y a pas une faute de conception.
Objets actifs.
Le parallélisme d'Ada permet de définir des objets actifs, c'est-à-dire évoluant en parallèle. Ceci s'obtient en associant une tâche à une structure de donnée; plusieurs façons de faire sont possibles, selon le degré de couplage souhaité entre la tâche et la structure de donnée.
Une première solution consiste à attacher manuellement une tâche à un objet. On peut pour cela utiliser un pointeur désignant l'objet comme discriminant de la tâche. Imaginons par exemple que nous voulions représenter un récipient qui fuit : toutes les 10 secondes, il perd un litre d'eau. Nous lui associons un «moniteur de fuite» qui diminue périodiquement la quantité restante :
package Compteur_fuyant is
 type Volume is delta 0.01 range 0.0 .. 100.0;
 type Compteur is
 record
 Contenu : Volume := 0.0;
 end record;
 task type Moniteur (Compteur_Associé: access Compteur);
end Compteur_Fuyant;
package body Compteur_fuyant is
 task body Moniteur is
 begin
 loop
 delay 60.0;
 if Compteur_Associé.Contenu > 1.0 then
 Compteur_Associé.Contenu :=
 Compteur_associé.Contenu - 1.0;
 else
 Compteur_Associé.Contenu := 0.0;
 end if;
 end loop;
 end Moniteur;
end Compteur_Fuyant;
with Compteur_Fuyant; use Compteur_Fuyant;
procedure Test_Compteur is
 Mon_Compteur : aliased Compteur := (Contenu => 100.0);
 Ma_Tâche : Moniteur (Mon_Compteur'Access);
begin
end Test_Compteur;
Avec cette solution la tâche «moniteur» est relativement extérieure à l'objet «compteur» : rien ne nous empêche de ne pas associer de tâche à l'objet, et d'avoir un compteur qui ne fuit pas. Cela peut être le comportement désiré. Si inversement nous voulons être sûrs que tous les compteurs fuient, il faut associer la tâche plus étroitement au compteur. C'est ce que permettent les auto-pointeurs :
package Compteur_fuyant is
 type Volume is delta 0.01 range 0.0 .. 100.0;
 type Compteur;
 task type Moniteur (Compteur_associé: access Compteur);
 type Compteur is limited
 record
 Contenu : Volume := 0.0;
 Le_Moniteur : Moniteur (Compteur'ACCESS);
 end record;
end Compteur_Fuyant;
Remarquer que c'est le nom du type Compteur qui est utilisé ici comme préfixe de l'attribut 'Access. Tout objet déclaré du type Compteur contiendra une tâche Moniteur, dont le discriminant désignera automatiquement l'objet Compteur dont il fait partie; cette disposition est illustrée en figure 14. Ces autopointeurs ne sont autorisés que dans des types limités : en effet, l'affectation ne pourrait conserver la propriété de l'autopointeur de désigner sa propre structure englobante.
Figure 14 : Représentation d'un autopointeur
On peut réaliser ainsi des objets réellement actifs. Noter que l'on pourrait avoir plusieurs tâches associées à un même objet.
Sémantique de valeur et sémantique de référence.
Il existe en fait deux sortes d'objets qu'il importe de bien différencier : ceux à sémantique de valeur et ceux à sémantique de référence. Ils se distinguent par la signification de l'opération d'affectation.
Avec une sémantique de valeur, la signification de l'affectation :
X := Y;
est de transférer le contenu de X (sa valeur) dans Y. Une modification apportée par une partie du programme à X n'affecte pas les autres duplications (comme Y). La comparaison d'égalité n'implique que l'égalité des valeurs, que ces valeurs soient contenues dans un même objet ou non.
Nous parlerons de sémantique de référence chaque fois que la valeur abstraite est un identifiant : type accès, indice de tableau, ou toute autre forme de nommage dont les différentes duplications se réfèrent en fait au même contenant. La signification de l'affectation :
X := Y;
devient : X et Y désignent le même objet. Toute modification apportée par un élément du programme au contenu de l'objet désigné par X sera donc visible (mais pas forcément immédiatement) à tous ceux qui utilisent des copies de la référence à l'objet (comme Y). La comparaison d'égalité entre deux variables aura la signification que les deux variables désignent le même objet (ce qui impliquera évidemment l'égalité des contenus). En revanche, deux objets peuvent être trouvés différents même si les contenus sont identiques. 
Les types numériques offrent typiquement une sémantique de valeur, alors que les objets implémentés par un type accès offrent une sémantique de référence. Mais il ne faut pas restreindre cette différence à la simple alternative «pointeur ou pas pointeur», encore moins à la question de passage par adresse ou par valeur. Une chaîne de caractères représentant un nom de ficher a typiquement une sémantique de référence! Il est également possible d'obtenir une sémantique de valeur sur un objet abstrait représenté par un type accès si l'on utilise un type limité muni d'une redéfinition de l'égalité et d'un sous-programme de recopie. Le choix entre type privé et type limité sera d'une grande importance pour renforcer le respect de la sémantique voulue.
La spécification de tout type de donnée abstrait doit explicitement mentionner s'il est à sémantique de valeur ou de référence. En aucun cas, un type ne doit fournir de sémantique mixte, comme dans l'exemple suivant :
type Info is ...
type Référence is access Info;
type Valeur is ... ;
type Sémantique_Mixte is — À ne pas faire !!
 record
 Champ_1 : Référence;
 Champ_2 : Valeur;
 end record;
Un tel objet offrirait une sémantique de référence pour son Champ_1 (c'est un pointeur) et une sémantique de valeur pour son Champ_2. Il n'est plus possible dans ce cas de décrire simplement la signification de l'affectation!
Même avec une sémantique de référence, on peut vouloir transférer le contenu d'un objet dans un autre. Un type de données à sémantique de référence doit alors disposer d'une fonction de recopie différente de l'affectation. Rappelons qu'Ada fournit désormais un puissant outil de contrôle du mécanisme d'affectation : un type dérivé du type prédéfini Controlled peut définir une opération, appelée Adjust, qui est appelée automatiquement après toute affectation à une variable de ce type. Il est ainsi possible de créer un type à sémantique de valeur, même si l'implémentation utilise des pointeurs, car l'opération Adjust peut forcer une duplication de la valeur de l'objet.
Remarquons que la distinction entre sémantique de valeur et sémantique de référence n'est importante que pour les types de données abstraits; les machines abstraites étant par définition uniques, l'affectation n'est pas définie pour elles.
Bien que théoriquement rien ne l'impose, les gestionnaires de données doivent absolument être à sémantique de référence. Il serait évidemment désastreux du point de vue de l'efficacité, mais surtout au niveau du principe, d'avoir une sémantique de valeur (ce qui n'empêche pas, comme nous l'avons noté ci-dessus, de fournir une fonction de copie de la structure). En revanche, les éléments gérés par la structure de donnée pourront l'être soit sous forme de valeur (par exemple une liste de valeurs), soit sous forme de référence (par exemple une liste de pointeurs sur des objets). Cette distinction doit faire partie de la définition des propriétés de la structure de donnée.
À noter enfin que la valeur (interne) d'un type de donnée à sémantique de référence est un identifiant d'objet : par conséquent, une instanciation d'un gestionnaire de données gérant des valeurs avec un type à sémantique de référence sera en fait une structure de données manipulant des références. L'instanciation d'une structure de données gérant des références avec un type à sémantique de référence sera une structure manipulant des «références doubles». Ceci peut arriver, mais doit être en général évité, car leur manipulation est délicate à maîtriser, et il est difficile d'en garantir une sémantique claire.
Tout n'est pas objet.
L'approche objet présente de nombreux avantages, mais cela ne signifie pas nécessairement qu'il faille tout modéliser en termes d'objets. Si l'entité du monde réel que l'on considère se résume à une suite d'actions, alors il est tout à fait souhaitable d'utiliser une approche structurée. Dans une conception orientée objet, ceci se produit dans deux cas particuliers importants : le premier et le dernier niveaux d'analyse.
Le premier niveau correspond à ce que l'on appelle généralement le programme principal. On aura défini les objets qui composent la représentation du domaine de problème, mais le programme principal est chargé d'animer, de faire vivre ces objets en appelant leurs fonctionnalités dans un certain ordre. Son analyse fait donc logiquement appel à la programmation structurée. De même, lorsqu'il s'agit de coder les corps des opérations des objets, il faudra bien utiliser une suite d'instructions du langage de programmation : là encore, une analyse en programmation structurée est appropriée.
On notera que pour un programme pas trop complexe, il n'existe pas de niveau intermédiaire entre le premier et le dernier niveaux : on se retrouve donc avec une analyse entièrement structurée. Autrement dit, l'approche objet ne renie pas la programmation structurée, mais la considère plutôt comme un cas limite «dégénéré» pour des programmes simples.
Le choix du critère vertical.
Il existe encore un problème important que nous avons laissé en suspens : le choix du critère de décomposition verticale. La notion d'abstraction d'objets du monde réel nous fournit un critère de décomposition horizontale nous permettant de diviser le problème en un ensemble d'objets, représentés par des types de données abstraits (ou des machines abstraites). Cependant, dès que le projet atteint une taille importante, il devient nécessaire d'organiser les objets à plusieurs niveaux. Saisir à la fois tous les aspects d'un objet serait trop complexe; il convient donc d'organiser la décomposition des objets.
Il existe plusieurs façons de faire qui, tout en partageant la même notion d'objet en tant que critère de décomposition horizontale, diffèrent par le critère de décomposition verticale choisi. Il n'y a donc pas une, mais des méthodes orientées objet. Ce point est rarement évoqué et conduit à d'innombrables discussions entre les partisans de telle ou telle méthode, persuadés de détenir seuls la «vraie» orientation objet.
Dans les chapitres suivants, nous présenterons plusieurs de ces méthodes. Les principales utilisent soit la composition comme critère vertical, soit la classification. D'autres organisations sont possibles, et rien ne dit que toutes les façons de faire aient déjà été publiées.
L'approche par composition.
Exemple en composition.
Nous voulons réaliser un «cahier de comptes» pour gérer une comptabilité personnelle. Nous allons nous appuyer sur ce que nous connaissons bien, le cahier que nous gérions auparavant à la main. Qu'est-ce donc qu'un cahier de comptes? C'est un ensemble ordonné d'écritures. Nous voyons donc apparaître deux objets : un gestionnaire de données de type machine abstraite qui est le cahier lui-même, et un type de donnée abstrait qui est la notion d'écriture.
Le cahier doit offrir les fonctionnalités habituelles d'une liste séquentielle (insérer et supprimer des éléments, parcourir la liste, etc.). Il possède une propriété supplémentaire : les écritures doivent rester triées par rapport à un ordre courant. Nous imposons donc que la fonction Insérer ajoute toujours une écriture à la bonne place. Mais pour cela, il faut pouvoir comparer plusieurs écritures en fonction d'un critère courant. Comment définir ce critère? Le plus simple est de le définir par rapport à une fonction de comparaison. Nous devons également fournir un point d'entrée pour changer de critère de comparaison. Comme le cahier est évidemment un gestionnaire de données, nous pouvons considérer qu'il existe toujours une position courante, et que la suppression d'une écriture porte toujours sur l'écriture courante. Nous pouvons décrire cette vue du cahier comme :
with Les_Ecritures; 
package Cahier_Compta is
 use Les_Ecritures;
 procedure Insérer (L_Ecriture : Ecriture);
 procedure Supprimer;
 function Ecriture_Courante return Ecriture;
 type Déplacement is (Début, Fin, Précédent, Suivant);
 procedure Changer_Position (Vers : Déplacement);
 function Début_Cahier return Boolean;
 function Fin_Cahier return Boolean;
 Erreur_Déplacement : exception;
 type Fonction_Ordre is 
 access function (X, Y : Ecriture) return Boolean;
 procedure Changer_Ordre (Comparaison: Fonction_Ordre);
end Cahier_Compta;
Une écriture comprend plusieurs éléments : une date d'opération, une date de valeur, un texte descriptif et un montant. En termes d'opérations, il faut pouvoir éditer une écriture, c'est-à-dire permettre à l'utilisateur de modifier son contenu. Enfin, nous devons fournir des fonctions de comparaison entre écritures. Nous pouvons décrire ainsi la vue abstraite des codice_3 :
with Ada.Calendar; 
package Les_Ecritures is
 type Argent is delta 0.01 digits 10;
 use Ada.Calendar;
 type Ecriture is
 record
 Date_Opération : Time;
 Date_Valeur : Time;
 Descriptif : String(1..20);
 Montant : Argent;
 end record;
 procedure Editer (L_Ecriture : Ecriture);
 function Date_Op_Inférieur (Gauche, Droite : Ecriture)
 return Boolean;
 function Date_Val_Inférieur (Gauche, Droite : Ecriture)
 return Boolean;
 function Montant_Inférieur (Gauche, Droite : Ecriture)
 return Boolean;
end Les_Ecritures;
Enfin, un programme de ce type n'a de sens que s'il est interactif. Il nous faut donc représenter l'interface utilisateur. Une interface digne des environnements modernes sortirait du cadre de cet exemple, mais nous pouvons toujours en faire une abstraction simplifiée : l'interface est ce qui permet au programme de recevoir des ordres de la part de l'utilisateur. Ces ordres sont des entités élémentaires. Il nous faudrait aussi différents moyen d'envoyer des données à l'utilisateur. À ce niveau, nous ne nous préoccupons pas directement des problèmes d'implémentation, aussi nous en tiendrons-nous au niveau logique : par exemple, nous pouvons prévoir d'envoyer des messages d'erreur à l'écran. Nous sommes trop haut dans les niveaux d'analyse pour spécifier des choses telles que faire apparaître une fenêtre avec un beau point d'exclamation pour signaler un problème : nous prévoyons simplement d'envoyer des messages d'erreur, indépendamment de toute représentation. Nous pouvons exprimer ceci comme :
package Interface_Utilisateur is
 type Ordre is 
 (Sortir,
 Avancer, Reculer, Aller_Début, Aller_Fin,
 Insérer, Modifier, Supprimer,
 Trier_date_valeur, Trier_date_opération, 
 Trier_montant);
 function Ordre_Suivant return Ordre;
 procedure Signaler_Erreur (Texte : String);
end Interface_Utilisateur;
Ayant ainsi défini les briques de base, il ne nous reste plus qu'à les assembler pour vérifier qu'elles nous permettent de réaliser le comportement désiré :
with Interface_Utilisateur, Les_Ecritures, Cahier_Compta;
procedure Compta_Perso is
 use Cahier_Compta, Interface_Utilisateur, Les_Ecritures;
begin
 Cahier_Compta.Changer_Ordre (Date_Val_Inférieur'Access);
 loop
 case Ordre_Suivant is
 when Sortir =>
 exit;
 when Aller_Début =>
 Cahier_Compta.Changer_Position (Début);
 when Aller_Fin =>
 Cahier_Compta.Changer_Position (Fin);
 when Avancer =>
 if not Cahier_Compta.Fin_Cahier then
 Cahier_Compta.Changer_Position (Suivant);
 end if;
 when Reculer =>
 if not Cahier_Compta.Début_Cahier then
 Cahier_Compta.Changer_Position (Précédent);
 end if; 
 when Supprimer =>
 Cahier_Compta.Supprimer;
 when Insérer =>
 declare
 Nouvelle_Ecriture : Ecriture;
 begin
 Editer (Nouvelle_Ecriture);
 Cahier_Compta.Insérer (Nouvelle_Ecriture);
 end;
 when Modifier =>
 declare
 Ancienne : constant Ecriture 
 := Cahier_Compta.Ecriture_Courante;
 Nouvelle : Ecriture := Ancienne;
 begin
 Editer (Nouvelle);
 if Nouvelle /= Ancienne then
 Cahier_Compta.Supprimer;
 Cahier_Compta.Insérer (Nouvelle);
 end if;
 end;
 when Trier_date_valeur =>
 Cahier_Compta.Changer_Ordre
 (Date_Val_Inférieur'Access);
 when Trier_date_opération =>
 Cahier_Compta.Changer_Ordre
 (Date_Op_Inférieur'Access);
 when Trier_montant =>
 Cahier_Compta.Changer_Ordre
 (Montant_Inférieur'Access);
 end case;
 end loop;
end Compta_Perso;
Il manque bien sûr encore de nombreuses fonctionnalités (à commencer par la sauvegarde du cahier sur fichier...), mais l'important est que nous avons mis en place une structure où le rôle de chaque module est clairement identifié : tout ce qui concerne la définition et les propriétés des écritures prises individuellement se trouve dans le paquetage Les_Ecritures; tout ce qui concerne le cahier, c'est-à-dire la gestion de l'ensemble d'écritures, est dans Cahier_Compta; tout ce qui concerne les interfaces est dans Interface_Utilisateur. À partir de là, il est facile de rajouter des fonctionnalités en sachant toujours où intervenir.
L'autre point important est que cette structure nous permet de définir des comportements simples et des invariants : par exemple, Insérer ajoute toujours l'écriture au bon endroit compte tenu du critère de tri courant. Un phénomène très intéressant, qui s'est produit lorsque nous avons développé le projet réel d'où est tiré cet exemple, montre bien l'amélioration importante de sécurité apportée par cette indépendance. Nous avions une erreur dans l'algorithme de retri de Changer_Ordre qui conduisait à des écritures incorrectement ordonnées dans certains cas. Cependant, pour visualiser le cahier, le programme «sortait» une écriture du cahier pour une modification éventuelle, puis la «re-rentrait». À ce moment, le logiciel s'apercevait que l'écriture n'était plus à sa place, et la réinsérait au bon endroit. Autrement dit le logiciel se trouvait dans un état incorrect, mais le simple fait de parcourir le cahier faisait qu'il retournait spontanément à l'état correct. Le logiciel était donc en quelque sorte «autostable». Ce résultat fort rassurant a été obtenu grâce aux principes d'indépendance temporelle et de méfiance réciproque : chaque fonctionnalité ne sait rien ni de qui l'appelle, ni en quelles circonstances. Du coup, chacun assure sa propre sécurité... et corrige éventuellement les erreurs de son voisin. Un tel comportement, qui ne peut être obtenu que par la stricte encapsulation permise par la composition, renforce la confiance que l'on peut avoir quant au bon comportement du logiciel, même face à des événements inattendus.
L'approche par classification.
À l'origine de cette approche se trouve l'émergence des langages orientés objet (LOO) [Mas89], dont la classification constitue le cadre méthodologique, en particulier pour maîtriser le mécanisme d'héritage qui les caractérise; on a pris l'habitude d'appeler programmation orientée objet (POO) la programmation avec ces langages. De ce fait, la classification est souvent présentée dans la littérature comme la seule méthode orientée objet. Nous avons déjà vu que l'approche par composition permettait de développer des systèmes «orientés objet» sans nécessiter le recours au mécanisme d'héritage, et nous ne saurions trop insister sur le fait que le concept d'objet couvre un domaine beaucoup plus vaste que celui de la seule POO.
Principes de la méthode.
Dans cette méthode, le critère de décomposition verticale est la classification (au sens de la classification des espèces de Linné [Lin35]). Les objets sont regroupé en classes, elles-mêmes décomposées en sous-classes plus précises, etc. Pour éviter toute confusion, on appelle instance un objet particulier appartenant à une classe.
Par exemple, on regroupera dans la classe Insecte les propriétés communes à tous les insectes. Par la suite, on pourra définir des sous-classes comme Volants ou Rampants, ne contenant respectivement que ce qui est spécifique aux insectes volants (ou rampants), mais commun à tous les insectes volants (ou rampants). Les Volants peuvent à leur tour se décomposer en Mouches, Guêpes, etc., suivant l'arborescence de la figure 17. La guêpe qui vient de vous piquer est une instance de la classe des Guêpes.
Figure 17 : Classification d'insectes
Bien entendu, une mouche est un insecte volant, qui est lui-même un insecte tout court. Une Mouche doit donc également posséder les propriétés des Volants et des Insectes : les propriétés des superclasses doivent être transmises à leurs descendants. Ce mécanisme est appelé héritage, puisqu'une classe enfant hérite des propriétés de ses ancêtres. On distingue classiquement l'héritage simple (toute classe n'est enfant que d'une seule superclasse) qui conduit à des programmes dont la topologie est un arbre, et l'héritage multiple (toute classe peut avoir plusieurs superclasses) qui conduit à des programmes dont la topologie est un graphe général acyclique. Nous reviendrons plus loin sur la question de l'héritage multiple.
Au fur et à mesure que l'on descend dans l'analyse, les objets possèdent de plus en plus de propriétés (celles dont ils ont hérité, plus celles qui sont particulières à la sous-classe), applicables à un nombre de plus en plus restreint d'instances. La descente dans l'analyse conduit donc simultanément à un enrichissement et à une spécialisation des classes.
Comme nous l'avons vu au paragraphe définition, une caractéristique de l'orientation objet en général est l'encapsulation, regroupement dans un même module de structures de données et de structures de programme. Dans le vocabulaire de la classification, on a pris l'habitude d'appeler attributs les structures de données liées à un objet, et méthodes les structures de programme (opérations) associées.
Mécanismes.
La mise en application de la classification nécessite des mécanismes spéciaux au niveau du langage de programmation. On qualifie de «langage orienté objet» un langage offrant ces mécanismes. Nous allons présenter ceux offerts par Ada; d'autres langages orientés objet offrent des fonctionnalités similaires, mais non strictement équivalentes. Nous les comparerons à la fin de ce chapitre.
Utilisation de l'héritage.
L'héritage, comme nous venons de le voir, s'appuie sur des mécanismes particuliers au niveau du langage. Nous allons maintenant présenter quelques utilisations typiques de ces mécanismes. Nous verrons ensuite un exemple d'utilisation plus «méthodologique» au service de la classification.
Conclusion.
Les types étiquetés d’Ada fournissent désormais, l'outil de base qui lui manquait pour les méthodes par classification. Les objets à facettes multiples sont réalisés par un moyen original, l'utilisation de la généricité, plutôt que par un mécanisme spécial du langage comme l'héritage multiple. Cette différence d'approche par rapport aux autres langages permet une meilleure sécurité et un contrôle plus précis du typage et des relations entre classes, au prix d'une plus grande lourdeur d'écriture. Ceci correspond à la philosophie du langage, qui privilégie la facilité de maintenance et la fiabilité par rapport à la facilité de conception initiale. Nous nous permettrons d'insister sur ce point, car la question de l'héritage multiple fait l'objet d'un vif débat jusque dans la communauté POO traditionnelle, certains y voyant la panacée, d'autres un danger permanent. Il est certain que si l'on demande s'il est possible, en Ada 95, de dériver un type directement de plusieurs autres à la fois, la réponse est «non». Mais en fait, l'héritage multiple, comme tout autre outil fourni par un langage d'ailleurs, n'est qu'un moyen au service de besoins. Si l'on demande si Ada 95 répond aux besoins que satisfait l'héritage multiple dans d'autres langages, la réponse est «oui», par ses moyens propres, qui conservent au langage ses points forts que sont la sécurité, la portabilité, l'efficacité et l'indépendance vis-à-vis des représentations machine.
Une autre originalité d'Ada pour la classification est que le mécanisme du langage ne repose absolument pas sur l'utilisation de pointeurs cachés ou non. L'utilisateur reste libre de définir des types à sémantique de valeur aussi bien qu'à sémantique de référence. De plus, de nouveaux outils tels que les autopointeurs et les pointeurs généralisés permettent de définir des relations subtiles entre classes. Il est possible de définir des objets actifs, et même des objets distribués. On peut donc dire non seulement qu'Ada supporte entièrement les méthodes par classification, mais aussi que les nouveaux outils qu'il apporte sont de nature à faire progresser les méthodes par classification elles-mêmes.
Classification ou composition?
L'approche objet conduit à une réorganisation complète de la structure des programmes; elle aura donc une profonde influence sur toutes les étapes du cycle de vie du logiciel. Un certain nombre de ses avantages proviennent de l'approche objet elle-même, que celle-ci soit ensuite organisée par composition ou par classification. Nous allons maintenant essayer de dégager les points communs aux deux approches, et aussi ce qui les différencie.
Avantages communs aux deux approches.
Les méthodes orientées objet conduisent à des programmes plus lisibles et plus compréhensibles : les objets concrets constituent finalement ce que le programmeur connaît le mieux. Une approche «objet» sera plus proche du domaine du monde réel, donc plus facile à appréhender, que les approches fonctionnelles. Les partisans de la classification comme ceux de la composition comparent d'ailleurs toujours leurs méthodes à l'approche fonctionnelle, car c'est par rapport à elle que le bénéfice est le plus grand.
L'objet inclut toutes les valeurs et opérations nécessaires et suffisantes pour le caractériser; il constitue donc une unité à forte cohésion et faible couplage. Pour déterminer les caractéristiques d'un objet, le programmeur est guidé par les caractéristiques «absolues» de l'objet réel que modélise l'objet informatique, et non par les besoins d'une application particulière; ces caractéristiques sont donc indépendantes de l'utilisation qui en est faite dans le cadre d'un projet informatique particulier. On obtient ainsi des unités facilement réutilisables. Tout nouveau programme partageant avec un programme précédent certaines notions du monde réel pourra réutiliser les modules déjà développés. Nous discuterons cependant de ce point de façon plus approfondie par la suite, car les deux approches ne favorisent pas le même genre de réutilisabilité.
L'objet regroupe les différents aspects d'une abstraction; cette encapsulation est caractéristique de toutes les démarches «objet». On obtient ainsi une meilleure localisation, c'est-à-dire que les endroits où il est nécessaire d'intervenir en cas d'évolution du logiciel ou de maintenance sont beaucoup plus faciles à identifier.
Enfin, il n'y a aucune difficulté à utiliser la méthode pour définir des systèmes parallèles : c'est le comportement naturel de la plupart des objets du monde réel; lorsque vous mettez le gaz sous votre autocuiseur, vous allez faire autre chose et ne vous en occupez plus jusqu'au moment où il siffle pour indiquer qu'il a atteint la température requise. Le parallélisme est une conséquence naturelle de cette approche, ce qui rend les méthodes orientées objet particulièrement appropriées à la conception de systèmes parallèles. On dispose ainsi d'une méthode unique pour les systèmes séquentiels et parallèles.
Réutilisabilité.
La réutilisabilité est un peu le monstre du Loch Ness du génie logiciel : tout le monde en parle, mais bien peu la mettent en pratique... Encore faut-il savoir ce que l'on entend par réutilisabilité. On peut dire que l'approche objet, de manière générale, promeut la réutilisabilité, et c'est un argument utilisé par les partisans de la composition comme de la classification. La raison en est simple : les modules correspondant de façon bijective aux objets du monde réel, le fait qu'un nouveau logiciel manie les mêmes objets réels que ceux d'un logiciel existant est une condition suffisante pour permettre la réutilisation des modules correspondants.
Mais du fait de leurs différences, les deux approches conduisent à des vues sensiblement différentes de la réutilisabilité. L'approche par classification favorise le développement incrémental. Il est facile, à partir d'une conception initiale, de dériver de nouvelles versions comportant des propriétés additionnelles sans pour autant affecter les utilisateurs de l'ancienne version : il suffit de faire une nouvelle classe dérivée de l'ancienne, à laquelle on ajoute les nouvelles fonctionnalités. Si certains aspects de l'ancienne version ne conviennent plus, il suffira de les redéfinir, et seulement eux. On peut donc aisément construire un nouvel objet à partir du code développé pour un objet existant. Il n'est pas nécessaire de concevoir spécifiquement dans un but de réutilisabilité : il s'agit d'une réutilisabilité a posteriori, qui permet de développer des objets «sur mesure» sans avoir à reconcevoir leurs aspects standard ou déjà développés. Le prix à payer pour cette facilité est qu'elle risque de conduire à une prolifération de versions ou de variations à partir d'une base donnée, rendant la maintenance difficile, si ce n'est parfois périlleuse, car une modification dans un objet de base sera propagée à un nombre inconnu d'objets dérivés. Si l'on a développé par exemple un objet Menu, il est possible que chacun des N programmeurs d'une équipe décide d'ajuster le Menu à ses désirs propres, conduisant à la nécessité d'en maintenir N+1 versions!
En approche par composition, on construit des composants logiciels, par analogie avec les composants matériels, dont le but est d'être utilisés comme briques de base dans le développement de logiciels complets. Ceci implique les mêmes contraintes que pour les composants matériels : leur conception doit être soignée, car une modification de leur spécification peut être coûteuse. Ce sera au programmeur de s'adapter aux composants disponibles, au lieu de les adapter à ses besoins propres. La qualité du projet sera fortement conditionnée par la qualité des composants, et ceux-ci devront être conçus dès le début dans un but de réutilisabilité : c'est une vue a priori de la réutilisabilité. En revanche, une fois l'interface bien établie, la maintenance peut corriger, modifier ou adapter l'implémentation en ayant la garantie que cela ne peut avoir aucune conséquence néfaste sur les utilisateurs du composant. L'étanchéité entre spécification et implémentation est totale.
Il pourrait apparaître que cette approche nuit à l'évolutivité des programmes; il n'en est rien, et le mieux pour s'en convaincre est de comparer avec les composants matériels. Depuis sa création, la spécification (le brochage) du 7400 n'a pas bougé! En revanche, son implémentation (sur silicium) a pu changer, et varie même d'un fabricant à l'autre sans que l'utilisateur s'en préoccupe. Des produits extrêmement différents ont pu être ainsi créés en réutilisant les mêmes composants de base.
Notons pour clore cette discussion sur la réutilisabilité que les deux approches ont un comportement dynamique opposé. L'héritage tend à produire des composants de plus en plus spécialisés (donc de moins en moins réutilisables) au fil du temps et des dérivations. En revanche, la démarche adoptée en composition va consister à construire des composants spécifiques, puis à les généraliser, par exemple en les rendant génériques. Au fil du temps, les modules tendent donc à devenir de plus en plus généraux et réutilisables. On peut dire que la démarche par classification va du général au particulier, alors que la démarche par composition va du particulier vers le général.
Structuration.
L'approche objet met en relief l'importance de la structuration globale du projet, que nous avons appelée la «topologie de programme». Mais cet aspect est lié à la méthode choisie. On ne sera donc pas étonné de trouver des structures totalement opposées lorsque l'on change de méthode. Comme les différents critères de décomposition sont ce qui différencie les méthodes, des éléments de «haut niveau» pour une méthode seront considérés comme de «bas niveau» pour une autre, et inversement. Un changement de méthode aboutit à un retournement de la structure même du projet, comme une chaussette que l'on retourne. Il n'empêche que les instructions de base se retrouvent quasiment identiques quelle que soit la méthode employée. Ce qui change, c'est la façon dont elles sont organisées, regroupées entre elles.
Nous discutions ainsi avec des ingénieurs chargés d'un logiciel de contrôle de lancement de fusée. Quoi de plus séquentiel et impératif qu'un compte à rebours? Ils avaient donc fait une analyse structurée, où le compte à rebours était l'élément de premier niveau de leur analyse. Dans une approche objet, on commencerait par identifier les éléments principaux intervenant dans le problème : le pas de tir, le centre de contrôle, etc. On finirait par retrouver le compte à rebours, mais seulement comme un détail d'implémentation du système de lancement, et non plus comme le point de départ de la conception.
De même, les encapsulations qui sont caractéristiques des approches objet en général ne regrouperont pas les mêmes éléments selon que l'on travaille en composition ou en classification. Supposons par exemple que nous voulions représenter un carré avec des propriétés graphiques et mathématiques. En composition, nous en ferions un objet autonome, qui regrouperait des objets de plus bas niveau, l'un décrivant les propriétés graphiques, l'autre les propriétés mathématiques; d'autres objets similaires réutiliseraient ces mêmes sous-objets, mais toutes les propriétés de l'objet «carré» seraient regroupées dans un seul module. Inversement, en classification, le carré hériterait certaines propriétés de la classe des objets graphiques, et d'autres de la classe (ou de la facette) des objets mathématiques : ce seraient alors les propriétés communes à plusieurs objets similaires qui seraient regroupées. Ainsi, les propriétés d'une vue d'un objet sont regroupées en composition, alors qu'elles sont réparties sur une ou plusieurs branches du graphe en classification; inversement, les propriétés communes à plusieurs objets sont dupliquées en composition, alors qu'elles sont factorisées en classification (Figure 19).
Ceci montre bien qu'il n'existe pas une méthode d'organisation absolue, mais des méthodes orthogonales... et pas seulement en informatique. Si par exemple je désire trier des documentations publicitaires, je peux les regrouper par marques ou par types de produits. Chaque fabricant fournissant plusieurs produits, l'une ou l'autre des classifications peut se révéler plus favorable. Si je cherche souvent le catalogue d'un fournisseur, il vaudra mieux trier par marques, alors que si je recherche tous ceux qui peuvent fournir des éditeurs syntaxiques, il vaut mieux trier par type de produit. C'est en fonction de l'utilisation souhaitée et des diverses contraintes du projet que l'on choisira l'une ou l'autre des méthodes.
Figure 19 : Regroupement des propriétés en composition et classification
Complexité.
Nous avons vu (paragraphe La COO par composition) que le graphe d'une conception par composition était non transitif. Si nous ajoutons un objet à la conception, il dépendra conceptuellement d'un certain nombre de voisins immédiats; si en moyenne nous avons K dépendances, ajouter un élément dans un graphe à N éléments augmentera la complexité de dépendance de K (K sera typiquement compris entre 2 et 10; au-delà, il faut se poser des questions sur la conception). La complexité de dépendance totale du graphe croîtra donc comme KxN.
Dans un graphe de classification, un objet dépend transitivement de tous ses ancêtres. Le nombre de dépendances d'un objet est donc proportionnel au nombre total N d'objets dans le graphe. Bien sûr, aucun objet ne dépend de tout le graphe. Soit k la proportion du graphe dont dépend en moyenne l'objet (k est toujours bien inférieur à 1). Ajouter un objet ajoute kxN dépendances, donc la complexité totale du graphe croîtra comme kxN².
Il est certain que pour N pas trop grand, kxN² sera inférieur à KxN. Dans ce cas, la complexité de dépendances engendrée par la classification sera inférieure à celle de la composition. Mais lorsque N croît, et ceci quelles que soient les valeurs de k et K, tôt ou tard kxN² dépassera (et même de beaucoup) KxN. 
Autrement dit, pour un petit projet, une structure en classification peut se révéler avantageuse sur le plan de la complexité; mais la taille des projets réels tend à croître rapidement, et fatalement une structure par composition finira pas se révéler préférable. On mesure ici l'effet pervers qui peut se produire lorsqu'une entreprise effectue un projet pilote pour mesurer l'impact possible d'une nouvelle technologie. Un tel projet est par nécessité de taille réduite, et l'on évalue généralement l'effet sur des gros projets par extrapolation linéaire. Or rien ne dit que la croissance de la complexité est linéaire, et nous avons vu que dans le cas de la classification elle ne l'est pas. Les conclusions tirées du projet pilote risquent donc de n'être absolument pas applicables à un projet de taille réelle.
Avantages et inconvénients de la composition.
La composition conduit à une hiérarchie en niveaux d'abstractions qui se prêtent bien à une approche descendante selon des couches logicielles étanches. Elle promeut le développement de composants logiciels standard réutilisables. En revanche, des modifications des spécifications des composants de base peuvent avoir des répercussions importantes en termes de recompilation sur l'ensemble des projets. Il importe donc que ces composants soient stabilisés avant d'être utilisés à grande échelle. En pratique, il apparaît qu'après une période d'évaluation, les spécifications des composants tendent à se figer. Une fois cet état atteint, de nombreuses applications peuvent les utiliser. Si un besoin de modification se fait sentir, il faut apprécier s'il est préférable de modifier un composant existant ou d'en développer un nouveau sur des bases légèrement différentes. Enfin il peut être plus intéressant de modifier la conception d'un programme pour l'ajuster aux composants existants plutôt que de chercher à créer de toutes pièces un composant «parfait». Cet état d'esprit est malheureusement étranger à la plupart des développements logiciels, alors qu'il est universellement accepté dans le monde des composants matériels.
Rigoureuse, abstraite, la composition exige plus d'effort de réflexion de la part des développeurs dans les phases initiales de conception; mais n'était-ce pas là l'un de ses buts? Les bénéfices s'en font sentir après. Réflexion d'un utilisateur (de la société Stratégies, développeur du logiciel CADWIN) : «Nous avons eu du mal en phase de spécification, mais dès qu'on arrive à la maintenance, on se régale.»
Les objets sont faciles à maintenir : un objet rassemblant en un seul module toutes les opérations logiquement reliées, l'endroit d'une modification ou d'une réparation est entièrement défini dès lors que l'objet en cause est identifié; de plus, l'étanchéité des implémentations vis-à-vis des spécifications fait que l'on peut garantir qu'une intervention dans une implémentation n'aura pas d'effet fâcheux sur le reste du programme. Une demande de modification des spécifications au cours du développement d'un projet n'aura de conséquences que sur le module correspondant à l'objet du monde réel sujet à modification.
Il est difficile d'évaluer l'impact économique d'une méthode, car de nombreux autres facteurs peuvent intervenir. Par contre, des études ont été conduites pour mesurer la rentabilité effective de l'utilisation d'Ada (83); on citera en particulier l'étude de Reifer [Rei87, Rei89], portant sur 41 projets terminés, totalisant 15 millions de lignes de code; 30 d'entre eux utilisaient la conception orientée objet par composition comme méthode d'analyse détaillée. Parmi les résultats intéressants de cette étude, on notera que la répartition conception/développement/mise au point, traditionnellement 40/20/40, était passée à 50/15/35, avec une diminution moyenne de 25% du taux d'erreurs dans les programmes. L'étude ne cherchait pas à différencier ce qui venait seulement de la méthode ou seulement du langage, car il est certain que les bénéfices ne peuvent être obtenus que par la conjonction de la méthode et d'un langage approprié.
En résumé, la composition nécessite un effort de réflexion et d'analyse intense; elle se prête au développement de composants logiciels standard, immuables dans leur spécification, mais dont l'implémentation peut être remise en cause à tout moment sans perturber les utilisateurs, suivant le modèle des composants électroniques. Moins évolutive que d'autres méthodes en phase de conception, elle permet de mieux maîtriser la phase de maintenance : une modification d'une implémentation n'a aucune répercussion sur les modules utilisateurs; une modification d'une spécification a des conséquences limitées, et dont on peut déterminer l'étendue a priori.
Avantages et inconvénients de la classification.
L'approche par classification conduit à un regroupement dans un même module de tous les aspects communs à différents objets. Le principal intérêt de cette méthode réside donc dans la factorisation des propriétés communes. En revanche, le graphe de dépendance est transitif, c'est-à-dire que les différentes propriétés d'une instance d'objet se trouvent réparties sur toute une branche du graphe.
Cette méthode favorisera la programmation par adaptation de logiciels, c'est-à-dire qu'il sera facile, à partir d'un objet qui correspond presque aux besoins du programmeur, de dériver un nouvel objet en ne récrivant que le différentiel, c'est-à-dire la différence entre l'objet souhaité et l'objet existant. En revanche, une modification d'un objet ancêtre modifiera le contexte et la sémantique de tous ses descendants. La classification permet donc de minimiser l'effort de réécriture en phase de développement, lorsque la définition des objets évolue rapidement; en revanche, les conséquences d'une modification en phase de maintenance peuvent s'étendre à une partie importante, et non connue a priori, du projet.
De façon générale, les méthodes par classification apportent une grande souplesse lorsque le domaine est mal connu, les concepts mouvants ou en cours de raffinement. Elles permettent bien de modéliser des hiérarchies de connaissance, et souvent de manipuler commodément les entités des applications graphiques. La liaison dynamique permet de pallier l'absence de parallélisme de beaucoup de langages (c'est pourquoi elle est utilisée dans les systèmes de fenêtrage comme XWindow ou MacApp). Ces méthodes se prêtent en revanche assez mal à une approche descendante de la conception, et d'ailleurs beaucoup de partisans de la classification prônent le développement ascendant, en partant des bibliothèques de classes existantes. Une telle façon de faire, si elle permet de développer rapidement des prototypes lorsque l'on dispose d'une vaste bibliothèque de composants (cas des environnements graphiques), devient malheureusement vite impraticable lorsque la taille des logiciels atteint l'échelle industrielle.
On peut enfin se poser des questions sur la notion même de classification comme moyen de conception. Si toutes les sciences utilisent la classification, c'est pour répertorier des éléments préexistants. La problématique est toute différente lorsqu'il s'agit de concevoir un système nouveau. On cherchera en vain dans l'ingénierie électronique, qui est pourtant la branche qui se rapproche le plus de l'informatique, un mécanisme pouvant se comparer à l'héritage pour la conception de systèmes.
Composition et classification : ne peut-on les fusionner?
Parvenu à ce point, il est logique de se demander s'il ne serait pas possible de définir une méthode incorporant à la fois les concepts de composition et de classification pour jouir des avantages propres à chaque approche.
Nous ne saurions trop insister sur le fait que ces deux notions sont absolument orthogonales. [Mas89] utilisent d'ailleurs un exemple de composition pour montrer le cas (selon eux) où il ne faut pas utiliser l'héritage. Par exemple, un ordinateur est composé de transistors; il n'hérite pas pour autant des propriétés des transistors; que signifierait «polariser un ordinateur»? Cette orthogonalité se traduit par le fait que les graphes de dépendance correspondant à chacune des structures possèdent des propriétés radicalement différentes : transitifs avec une complexité quadratique pour la classification, non transitifs avec une complexité linéaire pour la composition.
Les modes de raisonnement sont totalement différents, et même dans la vie courante des personnes différentes tendront à adopter naturellement l'une ou l'autre forme de description. Par exemple, nous participions un jour à une réunion de travail où nous disposions sur la table de jus de fruit concentré, que l'on pouvait diluer soit avec de l'eau plate, soit avec de l'eau gazeuse, permettant à chacun de préparer la boisson de son choix. Cette description est en fait une modélisation par composition. En classification, nous aurions dit que nous disposions de boisson à l'orange, subdivisée en orange piquante et orange plate...
La dichotomie entre les deux approches n'est cependant pas totale. Les langages orientés objet offrent nécessairement des possibilités de composition, et des langages purement «compositionnels», tels que Ada 83, offraient déjà, par les types dérivés, un mécanisme qui avait toutes les propriétés d'un héritage (statique), et même une forme de polymorphisme par les types à discriminants. Ces avancées vers l'autre domaine permettent à ces langages de récupérer quelques avantages spécifiques de «l'autre monde» sans sacrifier leur philosophie propre. Néanmoins, chacune des approches accordera la place dominante à la composition ou à la classification.
Nous verrons que de nombreuses méthodologies cherchent à exprimer simultanément des dépendances de type «composition» (agrégations) et de type «classification» (héritage). Ceci est dû à l'importance, à notre avis exagérée, accordée par de nombreux partisans de l'orienté objet à l'héritage, et comporte un risque inhérent sur le plan de la complexité : en superposant un graphe de composition et un graphe de classification, on risque d'obtenir une complexité globale qui serait la somme des deux complexités, si ce n'est pire.
Plutôt que de vouloir gérer simultanément les deux approches, il peut être plus raisonnable de les faire cohabiter indépendamment, c'est-à-dire de développer certaines parties d'un projet en composition, et d'autres en classification. Selon le profil de chaque partie, on choisirait la méthode la plus appropriée. Dans ce cas, il est tout de même nécessaire de définir un principe d'organisation global pour tout le projet, et il sera toujours plus facile de choisir la composition d'abord. En effet, il est possible d'incorporer dans un graphe de composition des branches qui, localement, sont des graphes d'héritage. Cela vient du fait que par définition, on ignore en composition comment sont implémentés les objets : si ceux-ci utilisent l'héritage, cela demeurera invisible aux niveaux supérieurs. Mais la démarche inverse est beaucoup plus difficile, puisque l'héritage suppose la transparence du graphe. Pour prendre une image, si l'on met une boîte transparente dans une boîte opaque, celle-ci reste opaque; alors que si l'on met une boîte opaque dans une boîte transparente, celle-ci n'est plus transparente.
Ceci ne signifie pas qu'il soit impossible d'utiliser de la composition dans une conception organisée par classification (nous avons déjà dit que c'était nécessaire), mais que l'utilisation de la composition rompt le mécanisme général de la classification, alors qu'une utilisation locale de l'héritage dans une structure par composition n'a pas d'impact sur la philosophie générale de la conception.
Nous pensons donc que même si l'on veut faire coopérer les deux approches, une méthode se doit de choisir une direction principale qui subordonne l'autre; les parties développées avec l'«autre» philosophie doivent rester localisées. Et dans ce cas, l'approche par composition d'abord bénéficie d'un avantage, puisqu'elle est moins perturbée par des classifications locales que dans la démarche inverse. Dans la cinquième partie de cet ouvrage, nous proposerons une telle méthode donnant la priorité à la composition; la classification n'y sera utilisée que là où elle apporte un gain notable, et toujours dans les couches basses de la conception, afin de limiter la complexité.
Bien entendu, il ne faudrait pas conclure de cette discussion que la classification doive être rejetée absolument : elle est certainement très performante dans le cadre de développements rapides, de complexité moyenne et/ou à faible durée de vie. Chaque projet possède ses contraintes propres, et le point important est de trouver, pour chaque type de développement, la méthode la plus adaptée; la quatrième partie de cet ouvrage présentera quelques critères de choix permettant de déterminer la méthode adaptée à un projet en fonction de ses caractéristiques.
Exercices.
=Les méthodes entités-relations=
Principes des méthodes entités-relations.
Les principes des méthodes entités-relations ont été établis par Chen [Chen76], à une époque où l'on ne parlait pas encore d'objets. En fait, la notion d'«entité» est très proche de la notion d'objet, et les publications plus récentes se référant à ces méthodes se considèrent comme «orientées objet». Vu l'importance de ces méthodes, nous avons préféré leur consacrer un chapitre séparé, mais compte tenu de nos définitions précédentes, on peut aussi les considérer comme une forme particulière de méthode objet, où la dimension «verticale» est souvent absente. Tous les critères des autres méthodes (composition, classification) peuvent en effet se mettre sous forme de «relations» particulières, ce qui aboutit à un «aplatissement» total de la conception.
Un schéma d'entités-relations représente une description (statique) d'un système réel pour lequel on souhaite développer une application informatique. Il s'agit donc plutôt d'une méthode d'analyse que d'une méthode de conception comme celles que nous avons vues précédemment; autrement dit, elle décrit plus le but à atteindre que le moyen d'y parvenir. Il faudra donc par la suite développer une conception, et donc transformer le modèle en un autre. Ce problème a donné lieu à de nombreuses publications; on consultera par exemple avec intérêt [Bar92] qui propose une méthode pour passer d'un modèle entités-relations (REMORA) à un modèle objet compositionnel en Ada.
Les entités représentent des classes d'objets, munis d'attributs. Le modèle sous-jacent de la plupart des méthodologies associées est celui des bases de données relationnelles; une entité correspond dans ce cas à une table dont chaque ligne correspond à une instance de l'entité, et chaque colonne à un attribut. Mais on peut également voir une entité comme un type article dont les différents composants correspondent aux attributs.
Ces entités (représentées par des rectangles) sont reliées par des associations (représentées par des lignes entre les rectangles) qui expriment les relations logique entre les entités; les relations sont marquées par un nom ou un verbe (dans un losange – ou un ovale selon la méthode) qui exprime la nature de la dépendance (Figure 20). Les relations sont conceptuellement réciproques; selon les conventions, on indique ou non la relation dans les deux sens (la réciproque de «passe» dans l'exemple de la figure 20 serait «est passée par»).
Figure 20 : Schéma d'entités-relations
On indique la cardinalité de l'association, c'est-à-dire le nombre d'instances d'objets situés d'un côté de la relation susceptibles de dépendre d'une instance d'objet situé de l'autre côté, sous la forme d'une paire de nombres [minimum:maximum]. Il existe de nombreuses variantes de ces diagrammes.
Le schéma de la figure 20 représente une partie des éléments entrant dans un système de gestion des commandes. Les entités sont Personne, Commande, Paiement et Société. Une personne peut passer plusieurs commandes, mais n'est entrée dans le système que si elle en a passé au moins une : c'est ce qu'indique la cardinalité [1:n] au départ de la relation passe; inversement, une commande ne peut être passée que par une seule personne, d'où la cardinalité [1:1] en sens inverse. Une personne appartient (mais pas forcément) à une société (cardinalité [0:1]), mais une société comporte plusieurs personnes (cardinalité [1:n]). Un paiement règle une seule commande (cardinalité [1:1]), et une commande n'est associée à un règlement (et un seul) que lorsque le règlement a été effectué (cardinalité [0:1]). Enfin, une société peut garantir 0, 1 ou plusieurs commandes (cardinalité [0:n]), mais une commande ne peut être garantie que par une société au plus (cardinalité [0:1]).
Ada et les méthodes entités-relations.
Au niveau du langage, il serait bon de pouvoir représenter aussi directement que possible les éléments qui ont été définis par la conception. La représentation des entités ne pose pas vraiment de problème : ce sont des données classiques. En revanche, la notion de relation, qui était très simple au départ, s'est progressivement enrichie, au fur et à mesure du développement des méthodes. On a ajouté progressivement des attributs de relation, des conditions, etc. Si l'on peut facilement considérer que les entités sont assimilables à des objets (au sens des méthodes orientées objet), doit-on considérer les associations comme des objets?
La logique voudrait évidemment que l'on réponde non, car une relation n'est pas de même nature qu'un objet. Cependant, il serait logique d'avoir des types de données pour représenter les associations. Dans un langage purement orienté objet, où la classe est le seul moyen de représenter des données, on se trouve ici face à une contradiction. En Ada en revanche, on peut très bien utiliser des types de données pour représenter des associations sans pour cela devoir les assimiler à des objets.
[Jea93] a présenté différentes façons de modéliser les associations en Ada 83. Nous les présentons brièvement ci-dessous, en y ajoutant les nouvelles possibilités d'Ada 95. Il convient d'abord de noter que la notion d'«association» est extrêmement vague et peut servir à exprimer, dans un cadre unique, toutes sortes de dépendances entre les entités. Ainsi, on peut avoir des relations «est composé de» (agrégation) exprimant qu'une entité est composée d'autres entités, et des relations «est un» (héritage) exprimant qu'une entité est une spécialisation d'une autre. Dans ces cas, l'implémentation est triviale : un article composé des différents éléments pour l'agrégation, un type (étiqueté) dérivé pour l'héritage.
L'agrégation peut poser un problème; dans le modèle entités-relations, les relations sont censées être réciproques. Par conséquent, si « A contient B », on doit pouvoir dire « B est contenu dans A ». L'utilisation d'un simple type article ne permet pas de traduire cette relation inverse (d'utilisation rare, reconnaissons-le). Si elle est nécessaire, on peut l'obtenir au moyen d'un autopointeur :
type A;
type B (Englobant : access A) is
 record
 end record;
type A is limited
 record
 Composant_B : B (A'Access);
 ... autres composants
 end record;
La relation « A contient B » correspond à l'imbrication physique des champs de l'article, et la relation « B est contenu dans A » est obtenue par le pointeur Englobant. Au risque de lasser le lecteur, force nous est de constater qu'Ada est, à notre connaissance, le seul langage permettant ainsi de gérer automatiquement ce type de relation inverse. Pour les autres types de relation, il convient d'analyser soigneusement leur type pour déterminer l'implémentation appropriée; [Jea93] présente d'ailleurs une taxonomie des relations.
Si la relation est unidirectionnelle, on pourra utiliser une référence, sous forme d'un composant pointeur si l'entité associée est susceptible de changer dans le temps, ou d'un discriminant pointeur si l'entité associée est unique et non facultative (traduisant ainsi un couplage beaucoup plus fort entre les entités). Dans le cas des relations où la symétrie est importante, on pourra utiliser des pointeurs doubles. Si la relation est multiple (cardinalité supérieure à 1), on pourra définir une table (ou une liste chaînée) représentant les éléments participant à l'association. Il est facile de faire un générique fournissant la gestion de ces tables d'associations.
Noter que certaines méthodes ajoutent des attributs aux relations; on utilisera dans ce cas le même principe, mais l'association sera représentée par un article contenant les attributs, ainsi éventuellement que la table d'associations.
On voit à travers ces quelques exemples qu'il n'y a pas de moyen unique pour représenter toutes les formes d'associations, mais que c'est la variété des structures de données et autres outils fournis par Ada qui permet de représenter toutes les formes d'associations.
Exercices.
=Méthodologies=
Nous avons présenté les différentes méthodes, c'est-à-dire les grands principes permettant d'organiser les développements de logiciels. Mais des principes ne sont pas suffisants en pratique : il faut également des modalités d'application, des directives organisées qui expliquent concrètement comment conduire un développement logiciel. C'est le but des méthodologies.
Certaines méthodologies étaient, au moins au départ, fondées sur une seule méthode. Mais souvent, la méthode unique n'est pas suffisante pour couvrir les contraintes souvent contradictoires d'un développement. Aussi de nombreuses méthodologies empruntent-elles à plusieurs méthodes, mais privilégient généralement l'une d'entre elles : c'est la direction principale de la méthodologie. Après les principes généraux, nous présenterons quelques-unes des méthodologies les plus courantes, selon leur direction principale.
La mode du tout orienté-objet et d’UML tend à créer une sorte de pensée unique. Et pourtant, l’informatique couvre aujourd’hui des domaines tellement variés qu’il semble difficile de croire qu’un seul processus puisse répondre à tous les besoins. C’est pourquoi nous présentons ci-dessous certaines méthodologies anciennes, qui ne sont parfois plus guère utilisées. Nous pensons qu’il est utile que le lecteur soit convaincu qu’il existe plusieurs façons de faire, et que le rôle de l’ingénieur est avant tout de déterminer quelle technologie est la plus adaptée à son besoin, plutôt que d’adopter aveuglément la seule démarche dont on parle dans les journaux.
Généralités, démarche et notation.
La plupart des méthodologies comportent deux éléments principaux : une démarche et une notation [Rum94]. La démarche définit le processus de conception; elle décrit les différentes étapes permettant de passer de l'énoncé d'un problème à sa solution. La notation décrit un ensemble de symboles permettant d'exprimer la conception. Son but est de guider la démarche et de produire un document de référence pour la maintenance. Enfin, la plupart des méthodologies sont dotées d'outils supports facilitant l'application de la démarche et automatisant l'expression de la notation, notamment lorsque elle est essentiellement graphique.
Le point le plus fondamental d'une méthodologie est la démarche. Le point le plus visible est la notation. Il importe donc de toujours se rappeler que la notation doit être subordonnée à la démarche : trop souvent, nous avons pu constater que l'application d'une méthodologie se bornait à utiliser la notation, ce qui peut conduire à des échecs, car les outils supports ne permettent alors pas d'exprimer l'intention réelle des concepteurs.
Enfin, certaines notations, initialement définies par des méthodologies, ont été adoptées par d'autres, mais généralement avec des «adaptations». Il importe donc de bien connaître la signification précise des symboles utilisés pour la méthodologie considérée, car, sous des apparences similaires, il peut y avoir des différences d'interprétation importantes.
UML.
Pendant longtemps, chaque méthodologie avait ses notations propres. Certaines d’entre-elles ont parfois été adoptées par d'autres méthodologies que celle d’origine, mais généralement avec des «adaptations». Ceci a conduit à des difficultés d’interprétation des symboles courants (formes des boîtes et des flèches).
UML (Unified Modeling Language, langage de modélisation unifié) est né du besoin d’unifier les différentes notations. Il est apparu en 1994, quand J. Rumbaugh rejoignit Rational, où G. Booch était responsable des méthodologies. En 1995, I. Jacobson rejoignit l’équipe, et après plusieurs itérations, UML 1.0 fut publié en Janvier 1997. Plusieurs fabricants firent alors équipe avec Rational, ainsi que l’Object Management Group. En Septembre 1997, les nouveaux partenaires produisirent UML 1.1, première version largement diffusée de la notation.
La méthode continua d’évoluer jusqu’à la version 1.4. Courant 2001, les membres de l’OMG décidèrent que le temps d’une mise à jour majeure était venu, ce qui produisit UML 2.0. À l’heure où nous écrivons ces lignes (2004), UML 2.0 est stabilisé et la plupart des documents sont disponibles, mais il reste encore certains éléments en cours de parution.
Il importe de comprendre qu’UML n’est qu’une notation, pas une méthodologie. Hélas, bien des gens ignorent le sage précepte qui conclue le paragraphe précédent, et «font de l’UML» comme méthodologie. Ceci ne veut strictement rien dire; on peut utiliser une méthodologie qui utilise UML comme représentation, mais l’important est la méthodologie, pas la représentation.
UML se voulait indépendant des langages de programmation; en pratique, surtout avant la version 2, il était directement calqué sur C++. On y retrouve des notions comme la distinction entre membres publics, protégés, et privés, qui n’existent que dans ce langage. En revanche, la notion de module élémentaire, comme les paquetages Ada, est difficile à représenter (les paquetages UML correspondent plutôt aux namespace de C++). UML 2 a introduit la notion de décomposition hiérarchique (qui est à la base de la méthode HOOD), permettant enfin une décomposition descendante; on peut s’étonner qu’il ait fallu tant de temps pour introduire une notion aussi fondamentale.
UML définit 13 formes de diagrammes différents, qu’il serait trop long de détailler ici; de nombreux ouvrages sont à la disposition du lecteur intéressé. Mais il est important de comprendre qu’aucune méthodologie ne saurait utiliser tous les diagrammes, pas plus qu’aucun programme n’utilise jamais toutes les possibilités de son langage de programmation. Le but d’UML est de fournir une infrastructure de description, afin que chaque méthodologie y puise les représentations qui lui sont nécessaires.
Méthodologies en programmation structurée.
La principale méthodologie formalisée utilisée de façon industrielle est SA/SD (Structured Analysis/Structured Design), définie initialement par Yourdon et Constantine [You79] et enrichie par DeMarco, Page-Jones... Elle a introduit une notation, largement reprise par la suite, pour exprimer les flots de données et leurs transformations : un cercle représente un traitement, une flèche un flot de données qui se propage de traitement en traitement. Les éléments à l'origine des données (sources) ou destinataires finals de celles-ci (puits) sont représentés par des rectangles. De plus, les données peuvent être stockées dans des réservoirs de données représentés par deux traits parallèles. Les diagrammes résultant sont appelés «diagrammes en flots de données», ou DFD (Data Flow Diagram). La figure 21 en donne un exemple.
Sur ce diagramme, nous voyons que le client passe une commande. La saisie de la commande donne naissance à un ordre de fabrication en cuisine et fournit le prix à payer. Le client effectue un paiement, qui va dans la caisse (un réservoir de données). Cela produit un calcul de monnaie, qui fournit au client la monnaie, en provenance de la caisse. De l'autre côté, l'ordre de fabrication a déclenché la fabrication d'un hamburger à partir de pain et de viande, tous deux provenant du réfrigérateur, autre réservoir de données.
Ce type de diagramme permet d'exprimer les traitements et les flots de données; il n'exprime pas en revanche les synchronisations. Par exemple, on ne voit pas que le client ne peut fournir le paiement qu'une fois le prix connu. Le diagramme des traitements est complété par un dictionnaire de données qui décrit, de façon textuelle, les caractéristiques des données échangées. Bien entendu, ce sont les traitements qui constituent l'unité principale de structuration, et le découpage en couches s'obtient en subdivisant les traitements généraux en traitements plus spécifiques.
Figure 21 : Diagramme de flot de données SA
La méthode a par la suite été enrichie par Ward et Mellor en fonction des contraintes particulières de la programmation en temps réel pour donner la méthode SART (SA Real-Time) [War85]. En particulier, on considère qu'un système est caractérisé par un état courant, et qu'il subit des transitions qui le font passer d'un état à l'autre. Ceci est décrit par des diagrammes états-transitions, qui ont été repris (avec d'innombrables variantes) dans la plupart des méthodologies.
Figure 22 : Diagramme d'états-transitions
Sur le diagramme de la figure 22, on part de l'état initial «Arrivée au bureau» pour arriver à l'état «Au travail». Sur l'événement «Appel téléphonique», on passe à l'état «Au téléphone» dont on revient sur l'événement «Fin communication». De même, l'événement «12 heures» fait passer dans l'état «Au déjeuner» dont on sort par l'événement «14 heures». Enfin l'événement «18 heures» fait passer à l'état final, «Quitter bureau».
Méthodologies en composition.
La méthode de Booch.
Cette méthode, qui est à la base des autres méthodes orientées objet par composition, a été définie par G. Booch, dans son livre Ingénierie du logiciel avec Ada [Boo88]. Son but était de fournir une méthode adaptée à Ada, mais ceci ne l'empêche pas d'être utilisable avec d'autres langages. Depuis, Booch a défini une autre méthode (Rose, [Boo91]) qui prend en compte les aspects de classification; le terme «méthode de Booch» tend donc à devenir ambigu.
Pour mettre en œuvre sa méthode, Booch a défini un certain nombre d'étapes permettant de guider la démarche pour obtenir de «bons» objets. Ces étapes ont été reprises avec plus ou moins de modifications ou de précisions dans la plupart des méthodologies par composition.
Cette étape est bien entendu celle qui demande le plus de talent et d'expérience personnelle au programmeur; un moyen relativement informel pour identifier les objets consiste à faire une description informelle (en français) du problème. On pourra déduire les bons candidats objets des noms utilisés dans cette description, et leurs propriétés des adjectifs et autres qualifiants.
Figure 23 : Diagrammes de Booch
Booch a introduit des diagrammes, représentés en figure 23, généralement utilisés pour représenter les entités Ada. Ils servent à construire des graphes qui, comme nous l'avons vu dans le chapitre sur la composition, sont essentiellement structurels et dont les flèches expriment les liens de dépendance entre entités. Elles correspondent donc aux clauses with du programme Ada.
La méthode HOOD.
La méthode HOOD (Hierarchical Object Oriented Design) [Ros97] a été développée par CISI Ingénierie, Matra et la société danoise CRI pour le compte de l'Agence spatiale européenne. Elle a été choisie parmi quinze méthodes concurrentes pour le développement des logiciels de la station spatiale Columbus et de l'avion spatial Hermès et est largement utilisée aussi bien dans le domaine spatial que dans l'industrie en général. Elle intègre non seulement une méthode de conception, mais également la définition de toute la documentation associée, et permet de faire la liaison avec des méthodes formelles. Elle a été conçue pour être utilisée avec des outils supports qui sont aujourd'hui présents sur le marché; la situation est même concurrentielle : en 1994, six fournisseurs (au moins) étaient présents sur le marché, ce qui fait de HOOD une des méthodes les mieux supportées en termes d'outillage. Plusieurs versions de la méthode sont apparues, suivant l'expérience acquise et les recommandations des utilisateurs (le HUG, HOOD User Group). La première version stable et largement utilisée fut actuelle est la 3.1; elle servit de base à une extension, appelée HRT-HOOD (Hard Real-Time HOOD) pour les logiciels demandant un contrôle précis et prouvable de l’ordonnancement. La version actuelle est la version 4, qui permet notamment d’introduire une dimension de classification dans la conception, mais de façon toutefois très contrôlée, dont les utilisateurs ont estimé qu'elle était suffisamment bien définie pour la geler pendant au moins trois ans, et donc permettre de stabiliser les outils. Une version 4 est actuellement à l'étude, qui pourrait inclure des concepts de classification.
La méthode elle-même résulte de la fusion des notions de machines abstraites et d'objets. Un projet est donc décomposé en objets qui sont, en principe (les évolutions récentes de la méthode on assoupli ce point), uniquement des machines abstraites. On distingue les objets passifs, s'exécutant séquentiellement et les objets actifs pouvant s'exécuter en parallèle.
Un objet peut être terminal ou non terminal. Dans ce dernier cas, toutes les opérations fournies sont réalisées par «sous-traitance» à des objets «enfants», conduisant à la structure hiérarchique qui a donné son nom à la méthode. Nous pouvons illustrer cette notion de sous-traitance de la façon suivante : imaginons un objet «téléviseur». Extérieurement, il est muni d'opérations (les boutons) telles que «Marche/arrêt», «Réglage volume» ou «Choisir chaîne». Effectivement, l'utilisateur considère qu'il met en marche «la télévision» ou qu'il règle «la télévision». En fait, le bouton d'arrêt fait partie au niveau de l'implémentation (quand on ouvre la boîte noire) de l'alimentation secteur, alors que le bouton de volume appartient à l'ampli son et le sélecteur au tuner. En termes HOOD, on dirait que la télévision sous-traite ces opérations aux objets enfants correspondants. Ce principe est illustré par la figure 24.
Figure 24 : HOOD et la sous-traitance
La méthode sépare clairement les différents aspects de l'analyse. En plus de la relation hiérarchique qui décrit la structure du projet, on analyse et on décrit de façon indépendante les aspects impératifs (correspondant à l'exécution séquentielle des opérations, relevant donc d'une analyse en programmation structurée et décrite par du code Ada) et les aspects réactifs (description des interactions entre objets actifs, susceptibles de modélisation par des méthodes formelles telles que les réseaux de Petri, ou utilisation standardisée de formes Ada). Cependant, chacune de ces descriptions est fournie localement, pour chaque objet; on ne trace jamais de flot de données global. Ceci permet de conserver une stricte hiérarchie et l'étanchéité des niveaux d'abstractions.
Figure 25 : Représentation de la télévision par des objets HOOD
La méthode définit un formalisme graphique et un formalisme textuel, décrivant les flux de contrôle, de données et d'exceptions aussi bien que la structure des dépendances logiques. Notre téléviseur peut ainsi être représenté sous forme d'objets HOOD comme sur la figure 25. Le diagramme montre également des relations entre les objets enfants (ici, l'alimentation 12V fournie par l'alimentation aux autres modules), non visibles de l'extérieur. Les flèches d'utilisation peuvent porter des informations de flux de données (les flèches dans un rond) et de flux d'exceptions.
La méthode est également adaptée à la définition de systèmes distribués, grâce à la notion de nœud virtuel. Un nœud virtuel représente une unité possible de distribution. Une étape de la méthode consiste à attribuer un nœud virtuel à chaque objet de la conception. Les nœuds virtuels sont ensuite eux-mêmes alloués à des nœuds physiques (comme des ordinateurs sur un réseau) est un objet HOOD normal vérifiant certaines contraintes supplémentaires. Les nœuds virtuels ne peuvent apparaître qu'aux premiers niveaux de décomposition, et les restrictions qu'ils vérifient permettent de les répartir sur plusieurs ordinateurs reliés par un réseau. On sépare donc la répartition du système entre une répartition logique et une répartition physique. Ceci permet notamment n'interdit pas d’allouer d'implémenter finalement plusieurs nœuds virtuels sur une même machine : il est ainsi possible d'avancer la conception et même la validation du logiciel indépendamment de lsa répartition physique du système, et même avant d'avoir fait les choix définitifs de matériel.
On trouvera une description complète de plus de détails sur la méthode HOOD dans [Ros97] par exemple, ainsi qu'un compte rendu d'utilisation dans [Lai89]. La définition officielle de la méthode est donnée dans [Hoo93]].
La méthode Mac_Adam.
Mac_Adam [Rig87,Rig89] est une chaîne complète de développement de logiciels, comportant donc plusieurs facettes. La partie méthodologique est directement inspirée de la COO de Booch, avec une orientation plus spécifique du temps réel. Une première étape permet de définir le contexte du problème et les interfaces externes, conduisant à une modélisation de l'espace de problème. Du rapprochement entre ce modèle abstrait et les contraintes spécifiques du temps réel, on dérive un regroupement en tâches, la définition des protections et la structuration en sous-systèmes.
En plus de cet aspect purement «méthodologie de conception», la méthode gère le suivi des unités de test et d'intégration, et la documentation. Celle-ci est conforme à la directive 2167 du DoD [DoD87]. Un outil associé permet de suivre les différentes étapes de la méthode jusqu'à la génération de squelettes de programmes.
La méthode de Buhr.
Composition et classification ne sont pas les seules façons d'organiser des objets; la méthode de Buhr [Buh84,Buh89] est une autre méthode de conception «orientée objet», visant principalement la conception de systèmes temps réel. Cette méthode utilise le comportement (behaviour) comme critère de décomposition verticale, selon une technique inspirée de celles utilisées dans le monde de l'automatique. C'est donc une méthode orthogonale à la fois à la composition et à la classification.
Le module de base dans la méthode de Buhr est soit la boîte (box), qui est une unité passive, soit le robot qui est une unité active. Ces deux unités sont appelées globalement des machines. Une machine est une unité de comportement, possédant des ports qui lui permettent de communiquer avec d'autres robots par l'intermédiaire de canaux. Des événements circulent dans ces canaux. Ces notions sont par la suite raffinées sous forme de boutons qui doivent être poussés par un doigt pour déclencher une action. Certains boutons ne peuvent s'activer que sous l'action combinée d'un doigt qui pousse et d'un doigt qui tire (push-pull button).
Les grandes étapes de l'analyse d'un niveau d'abstraction selon la méthode de Buhr sont :
Comme on le voit, le grand intérêt de la méthode de Buhr est de permettre la prise en compte de l'ordonnancement temporel dès les premières étapes de la conception, ce qui en fait un système très efficace pour le développement de logiciels temps réel. La notion de machine intègre bien également les composants matériels, ce qui permet de définir un système (matériel et logiciel) comme un tout, en remettant à plus tard le stade où l'on doit décider de ce qui doit être implémenté par logiciel ou par matériel. En revanche, elle se prête moins bien à une approche descendante par niveaux successifs, ce qui limite sa portée aux systèmes de taille relativement modeste.
La méthode est supportée par un formalisme graphique, et il existe des outils d'environnement permettant de le mettre en œuvre, aussi bien pour la conception elle-même que pour la vérification automatisée des diagrammes, la simulation temporelle et la génération de squelettes de code.
La méthode Rose.
La méthode Rose [Boo91], la «nouvelle» méthode de Booch, reprend les principes de la méthode initiale, mais y incorpore une dimension de classification. Les étapes sont restées à peu près les mêmes que dans la méthode d'origine, mais les diagrammes se sont enrichis pour décrire différentes formes de dépendance entre objets : utilisation, instanciation, héritage, métaclasses... Ces formes sont cependant limitatives (au contraire des méthodes entités-relations). La méthode étant moins orientée Ada que précédemment, on distingue les diagrammes de classes des diagrammes de modules.
La méthode travaille par niveaux d'abstraction dans la direction verticale; en particulier, on cherche à définir des sous-systèmes étanches.
Méthodologies en classification.
On pourrait s’attendre à ce que la vogue de la classification ait fait naître de nombreuses méthodes de conception, ou au moins qu’une méthode s’impose largement. Or il n’en est rien. Le monde de la classification «fait de l’UML», mais UML est une représentation, pas une méthode. On cherchera en vain une démarche méthodologiquee par classification formalisant le processus permettant de passer de l’énoncé d’un problème à sa réalisation informatique, comme cela existe pour disposant de l'expérience, de la communauté d'utilisateurs et d'un marché d'outils concurrentiel du niveau de HOOD, Buhr ou SART par exemple.
Ceci peut s'expliquer également par l'histoire : le monde de la classification est parti d'une approche langage, et le souci a plus été de représenter les arborescences de classes (qui deviennent rapidement très compliquées) que de guider le processus de conception lui-même. Les méthodes ne sont arrivées que beaucoup plus tard; il s'agit alors généralement de méthodes propriétaires, liées à un seul fournisseur d'outils. Enfin, beaucoup de méthodes (elles bien établies) intègrent une dimension de classification, sans que celle-ci constitue la direction principale. C'est en particulier généralement le cas des méthodologies «entités-relations» qui sont présentées au paragraphe suivant.
Méthodologies par entités-relations.
Shlaer et Mellor.
La méthode de Shlaer et Mellor [Shl88] est caractéristique des méthodologies entités-relations modernes, qui se considèrent «orientées objet» dans la mesure où les «entités» manipulées sont des abstractions d'objets du monde réel. Elle vise essentiellement les applications centrées sur les bases de données, car les entités sont considérées comme des «tables» et les relations comme des «jointures» (au sens de SQL). Certaines relations plus compliquées [m:n] peuvent elles-mêmes être représentées sous forme de tables. L'héritage est introduit par la relation «est-un». Ce modèle structurel est complété par des diagrammes en flots de données exprimant les traitements.
C'est essentiellement une méthode d'analyse, dans la mesure où l'ouvrage de référence se concentre essentiellement sur la façon d'obtenir les renseignements du client afin d'obtenir une «bonne» modélisation du système à concevoir. En revanche, on ne parle que très peu de la dimension verticale : comment subdiviser un système complexe en sous-systèmes plus simples.
OMT / Rumbaugh.
La méthode OMT [Rum94] ressemble beaucoup par certains points à la méthode de Shlaer et Mellor, mais l'aspect «bases de données» n'y est pas fondamental : celles-ci ne sont considérées que comme une façon parmi d'autres d'implémenter les notions d'entités et d'attributs. L'idée de base est que tout système doit être décrit de trois façons orthogonales :
La méthode accorde la place principale à l'aspect structurel, les autres aspects n'intervenant que plus tard dans la conception. Comme avec Shlaer et Mellor, la description se veut complète, c'est-à-dire qu'elle veut exprimer tous les aspects de tous les composants du système. En conséquence, la décomposition verticale n'est que peu présente : le schéma d'entités-relations est découpé en «feuillets», mais qui constituent plus une facilité pour éviter de manipuler de trop grands diagrammes qu'une découpe hiérarchisée. L'analyse fonctionnelle demande d'identifier des sous-systèmes, mais ne dit rien des critères à utiliser pour ce faire. Il s'agit donc d'une méthode très complète au niveau spécification, mais on peut lui reprocher un manque de «profondeur» si on veut la poursuivre vers les étapes ultérieures du développement.
Méthodologies : oui, mais....
Il n'est pas question de remettre en cause l'importance des méthodologies, mais ceci ne dispense pas de certaines précautions. Aucune méthodologie, pas plus qu'aucun langage ne peut garantir le succès d'un développement; toute méthodologie a ses forces et ses faiblesses, et une méthodologie mal comprise ou mal utilisée peut même compliquer le processus de conception. C'est pourquoi nous pensons qu'il est utile de terminer cette partie par quelques mises en garde.
Tout d'abord, être «orienté objet» est devenu une nécessité marketing à défaut d'être une nécessité technique; toutes les méthodes utilisent maintenant, de près ou de loin, ce terme. Il importe de bien voir ce que cela recouvre. En particulier, l'importance exagérée accordée souvent à l'héritage a conduit à rajouter une dimension d'héritage à beaucoup de méthodes où cela n'était pas indispensable.
Ensuite, la méthodologie doit apporter une simplification par rapport au langage; il ne faut jamais oublier que le but premier est de lutter contre la complexité. Une méthode mal utilisée ou inadaptée au projet peut conduire à l'effet inverse. Il nous est ainsi arrivé de conduire des audits sur des projets où, après avoir peiné sur des pages et des pages de boîtes et de flèches, nous avons fini par demander à voir les spécifications Ada... qui exprimaient finalement beaucoup mieux la structure du projet que les diagrammes. En particulier, il est inutile de poursuivre l'application de la méthode au-delà du niveau sémantique du langage : à quoi bon tracer des arborescences de parcours et écrire du pseudo-code s'ils ne fournissent pas une vue plus synthétique que le programme Ada lui-même ?
Les méthodologies ont toujours été soumises à deux contraintes contradictoires : être limitatives pour fournir un cadre rigide, donc directif, aux développeurs, et être puissantes, donc permettre d'exprimer un maximum de choses. Le côté directif de la méthode est souvent mal perçu, et le programmeur est souvent tenté d’abandonner la méthode plutôt que de s’y plier. On peut résumer (un peu caricaturalement) ceci de la façon suivante :
L'histoire de la méthode HOOD par exemple est caractéristique : d'une approche strictement hiérarchisée en machines abstraites, on est passé progressivement à des «environnements» échappant à la hiérarchie, puis à des «types de données abstraits» qui échappent aux exigences des machines abstraites; et finalement, HOOD 4 a introduit une dimension de classification. 
Cette évolution élargit le champ d'application des méthodologies, en diminuant les contrôles qu'elles apportent. Si l'on décide d'adopter une méthodologie «puissante», donc vraisemblablement peu contraignante, il peut être souhaitable d'en limiter les possibilités pour améliorer les contrôles : on définira alors un «cadre d'utilisation» de la méthodologie, exprimant ces restrictions. Une liste soigneusement définie d’exceptions aux règles (et contrôlée par le responsable qualité) autorisera des violations dans certains cas au programmeur, et permettra d’éviter le syndrome ci-dessus.
=Maquettage et développement progressif=
Le développement progressif par maquettage ne constitue pas à proprement parler une méthode, tout au moins pas au sens des méthodes que nous avons vues dans les chapitres précédents. Il s'agit plutôt d'un contexte méthodologique compatible avec la plupart des méthodes, bien que plus particulièrement facile à mettre en œuvre avec les méthodes orientées objet.
Maquettage et prototypage.
Maquettage, prototypage, ces mots sont utilisés fréquemment en informatique, parfois de façon interchangeable, et généralement avec des significations mal définies. Rappelons donc leur définition dans le domaine général de l'industrie et voyons comment ils peuvent s'appliquer à l'informatique.
Un prototype est le premier exemplaire d'un produit destiné à être produit par la suite en grande série. Il est parfaitement fonctionnel et permet d'évaluer complètement ses possibilités; en revanche, sa technique de fabrication est particulière, relevant souvent de méthodes artisanales. Son but est de tester les réactions des consommateurs avant de mettre en place la production en série, stade où il ne sera plus possible de revenir en arrière pour corriger d'éventuels petits défauts. La notion qui s'en rapproche le plus dans le domaine de l'informatique est ce que l'on appelle une version bêta : fourniture du logiciel à un petit nombre d'utilisateurs sélectionnés afin de tester leurs réactions, diagnostiquer les erreurs résiduelles par une utilisation en vraie grandeur, et corriger des imperfections notamment en matière d'ergonomie. Comme pour un prototype industriel, il n'est plus question à ce niveau de remettre en cause la structure fondamentale du logiciel.
Toute autre est une maquette : il s'agit d'une version à petite échelle du produit, destinée à montrer l'apparence future de celui-ci et à vérifier auprès du client qu'il correspondra bien à sa demande. Son but est donc de vérifier la bonne orientation du développement, avant de poursuivre plus avant. La maquette est donc par nature incomplète, et ne vérifie pas une part importante du cahier des charges. Par exemple, un appareil électronique destiné à être embarqué à bord d'une fusée doit supporter des contraintes mécaniques et de vibrations draconiennes; cela n'empêche pas sa maquette de comporter des fils volants prêts à se détacher au moindre souffle! Dans le domaine du logiciel, une maquette permettra de vérifier l'interface utilisateur et les fonctionnalités fournies, mais pourra ne pas vérifier des contraintes du cahier des charges telles que le temps de réponse ou le volume de données manipulées. 
Ce qui différencie la maquette du prototype est donc que la maquette est par nature incomplète, et sert à valider une approche avant de poursuivre le développement. Au contraire, le prototype permet de vérifier qu'une première version complète du logiciel est satisfaisante, après développement, et permet seulement d'ajuster des défauts résiduels.
Maquettage rapide.
Il est bien connu que c'est lorsqu'un projet est terminé qu'il faudrait le commencer. Si l'on avait pu connaître dès le début toutes les difficultés qui ne sont apparues que plus tard, on aurait pu adopter une structure plus efficace. C'est pourquoi il est parfois intéressant de développer des maquettes préliminaires destinées à explorer le domaine de problème.
La réalisation de ces maquettes est en général gouvernée par des contraintes opposées à celles du produit final : le temps de développement doit être très faible (pour ne pas trop grever le coût du développement final), il n'y aura aucune phase de maintenance, aucune contrainte de documentation ni de fiabilité ni de performance... On utilisera donc souvent des langages interprétés comme SmallTalk ou même APL.
Bien entendu, il faut absolument jeter la maquette dès qu'elle a permis de faire avancer suffisamment la connaissance du domaine de problème. En effet, cette forme de développement présente un risque : les développeurs peuvent avoir le sentiment d'écrire deux fois le logiciel, et réagir en tentant de «faire grossir» l'implémentation maquette au lieu de le reconcevoir entièrement. Comme les impératifs de développement de la maquette sont différents de ceux du produit définitif (les compromis de départ correspondent à des contraintes radicalement différentes, parce qu'on ne se soucie pas de maintenance sur une maquette), la structure adoptée risque d'être inappropriée.
Le programmeur hésite souvent à effacer de son disque dur ce qui représente de nombreuses heures de travail. Il faut le convaincre qu'il conservera le résultat le plus important : le savoir-faire acquis; l'effort de dactylographie supplémentaire n'est pas du travail perdu, car il a servi à augmenter sa connaissance du domaine de problème. D'ailleurs, bon nombre des meilleurs logiciels du marché ont été écrits entièrement deux fois (parfois plus).
Maquettage progressif.
Nous avons vu (paragraphe Surmonter la complexité) que les grandes étapes du développement, quelle que soit la méthode utilisée, étaient constituées de niveaux horizontaux successifs. Par définition, un tel niveau est trop complexe pour pouvoir être réalisé directement (ou alors, c'est que l'on est arrivé à la dernière étape de raffinement). En revanche, il est possible de réaliser une version simplifiée de chaque module : une telle maquette devra se présenter vis-à-vis de l'extérieur comme le module définitif (les spécifications sont établies), mais son implémentation peut être extrêmement sommaire. Par exemple, un module destiné à gérer des millions d'enregistrements dans une base de données répartie sur un réseau peut être maquetté par un module ne sachant gérer que dix enregistrements dans un tableau en mémoire... Il n'empêche que les fonctionnalités sont présentes et permettent de vérifier la complétude des spécifications.
Nous parlerons de développement par maquettage progressif (à ne pas confondre avec le maquettage rapide) lorsque cette pratique est systématisée : à chaque étape du développement, on spécifie le module à concevoir, puis on en réalise une maquette exécutable : on dispose donc toujours, et dès les premières étapes, d'un programme complet, mais grossier. Au fur et à mesure du développement, les couches hautes sont remplacées par leur version définitive, mais implémentées au moyen de couches inférieures qui se trouvent elles-mêmes à l'état de maquettes, et ainsi de suite jusqu'à ce que l'ensemble du projet ait atteint l'état définitif. On pourrait donc également qualifier ces méthodes de «démaquettage progressif» : on évolue progressivement d'une version totalement «maquette» vers la version définitive. Ceci permet un passage en douceur de la spécification de haut niveau à l'implémentation, suivant l'idée de spécification progressive que l'on pratique avec SETL [Ros85].
Un outil indispensable : le paquetage ADPT.
Pour que l'approche par maquettage progressif soit intéressante, encore faut-il que le développement de la maquette n'entraîne qu'un effort négligeable par rapport au développement du module définitif. Le paquetage ADPT est l'outil indispensable pour spécifier des conceptions incomplètes, tout en conservant la possibilité de compiler en permanence l'état de la conception. Nous utiliserons ce paquetage dans nos exemples ultérieurs ; cette section n'est qu'une présentation générale de son fonctionnement.
L'idée première de ce paquetage provient du composant du domaine public TBD_PACKAGE (TBD = To Be Defined, À définir), écrit par Bryce Bardin, et que l'on peut se procurer sur les serveurs du domaine public (Public Ada Library aux États-Unis, serveur du CNAM en France). ADPT signifie « À Définir Plus Tard ». L'idée de base est de fournir un moyen de spécifier... que certaines choses ne sont pas encore spécifiées. Les perfectionnements que nous avons apportés à ce paquetage par rapport à la version originelle concernent les possibilités de trace et de spécification de durée d'exécution, qui en font un véritable outil de maquettage, y compris en ce qui concerne les propriétés temporelles. Voici la structure générale de ce paquetage :
package ADPT is
-- Type complètement indéfini
 type ADPT_Type is private;
 function ADPT_Fonction (Info : String := ""; 
 Durée : Duration := 0.0) 
 return ADPT_Type;
 ADPT_Valeur : constant ADPT_Type;
-- Type Enumératif
 type ADPT_Type_Enuméré is (ADPT_Valeur_Enumérée);
 function ADPT_Fonction (Info : String := ""; 
 Durée : Duration := 0.0)
 return ADPT_Type_Enuméré;
-- Type discret
 type ADPT_Type_Discret is new ADPT_Type_Enuméré;
 function ADPT_Fonction (Info : String := ""; 
 Durée : Duration := 0.0)
 return ADPT_Type_Discret;
 ADPT_Valeur_Discrète : constant ADPT_Type_Discret;
-- De même pour les type entiers, flottants, fixes,
-- tableau, articles, accès, étiquetés...
-- Autres déclarations
 ADPT_Condition : constant BOOLEAN := False;
 ADPT_Exception : exception;
 procedure ADPT_Procédure (Info : String := ""; 
 Durée : Duration := 0.0);
 procedure ADPT_Actions (Info : String := ""; 
 Durée : Duration := 0.0);
-- Ajustement du comportement
 type ADPT_Comportement is (Ignorer, Tracer, Piéger);
 ADPT_Comportement_Courant : ADPT_Comportement:= Ignorer;
private
end ADPT;
Le paquetage complet (spécification et corps) est donné en annexe.
On fournit différents types ADPT, qui expriment la connaissance plus ou moins précise que l'on peut avoir du futur type. Chaque type possède une valeur, et une fonction fournissant une valeur du type. Par exemple, si l'on n'a besoin que d'un type et que l'on n'a encore aucune idée du choix futur d'implémentation, on le fournit sous la forme :
type Mon_type is new ADPT_type;
Comme le type ADPT_type est en fait un type privé, aucune opération n'est encore disponible. Plus tard, on peut décider que ce type doit être un type discret. On change alors la définition en :
type Mon_type is new ADPT_type_discret;
On peut l'utiliser comme n'importe quel type discret, par exemple comme indice de tableau. On décide ensuite d'utiliser un type entier, mais l'on ne souhaite pas encore se préoccuper du problème des bornes. On définit alors :
type Mon_type is new ADPT_type_entier;
Ceci nous permet d'utiliser les opérations arithmétiques. Finalement, on définit les bornes réelles et l'on écrit :
type Mon_type is range 1..10;
De cette façon, on peut faire évoluer le programme depuis une structure très grossière jusqu'à l'implémentation finale, en faisant effectuer à chaque étape par le compilateur des vérifications sémantiques correspondant au niveau des choix d'implémentation qui ont (ou n'ont pas été) faits. Lorsque l'on pense que tout est défini dans un module, il suffit de retirer la clause with ADPT; et de recompiler. Si l'on a oublié quelque chose d'indéfini, le compilateur se chargera de le signaler. De même, on peut demander au gestionnaire de bibliothèque Ada la liste des unités qui dépendent encore de ADPT; ceci procure une bonne idée de l'état d'avancement de la définition du projet. Lorsque cette liste est vide (et que l'on a fourni tous les corps nécessaires), on est prêt pour la première exécution en vraie grandeur !
En plus des déclarations de types, le paquetage procure une exception (à utiliser lorsque l'on ne sait pas encore quelle exception lever dans une situation donnée), une constante booléenne appelée ADPT_condition qui permet d'écrire :
if ADPT_Condition then...
ainsi que deux procédures : ADPT_Procédure et ADPT_Actions. La première sert à prendre la place d'un appel de procédure lorsque la vraie procédure n'a pas encore été conçue. La seconde sert à repérer un endroit où le langage exige des instructions, mais où l'on n'a pas encore décidé quoi mettre. Ne jamais utiliser une instruction null pour cela! Il n'y aurait aucun moyen de vérifier que des actions indéfinies n'ont pas été oubliées par endroits.
On peut vouloir essayer le programme avant qu'il ne soit complètement défini. Pour cela, le comportement des sous-programmes peut être changé en modifiant la variable (publique) ADPT_comportement. Lorsqu'elle est mise à Ignorer (la valeur par défaut), les appels aux sous-programmes ADPT n'ont aucun effet. Lorsqu'elle est mise à Tracer, le message que l'on peut fournir (facultativement) à chacun des appels est imprimé, fournissant ainsi une trace du parcours du programme. Lorsqu'elle est mise à Piéger, une exception est levée si un sous-programme est appelé. Cette exception est déclarée à l'intérieur du corps de paquetage, de façon à interdire sa récupération (sauf par un traite-exception when others bien sûr). Ce dernier comportement sert à identifier des appels oubliés à des sous-programmes ADPT.
Toutes les opérations fournies possèdent un paramètre supplémentaire facultatif, Durée, représentant la durée du traitement effectué. La valeur par défaut est bien entendu 0. Ceci permet de simuler les temps d'exécution des procédures. L'idée est de permettre de maquetter également le budget temps. 
Lorsque l'on doit développer une application devant vérifier des contraintes temps réel, une façon de procéder consiste lors de la descente dans les niveaux d'abstraction à allouer à chaque procédure un «budget temps», un temps maximum à ne pas dépasser pour toute exécution. Lors de la phase d'analyse suivante, le budget temps est réparti entre les différentes opérations utilisées. Bien entendu, comme dans un vrai budget, on gardera une petite marge de sécurité à chaque répartition, et il sera possible de transférer du temps alloué depuis une procédure qui s'avère plus rapide que prévu vers une autre qui a du mal à tenir ses contraintes. Le paramètre Durée permet de maquetter ce budget temps : lorsque l'on implémente réellement une opération (c'est-à-dire que l'on passe d'une maquette à une implémentation réelle, mais qui utilise des maquettes de plus bas niveau), on répartit la durée qui était allouée à la maquette entre les durées de toutes les maquettes utilisées. Comme l'ensemble reste exécutable, il est possible de vérifier, notamment en fonction de diverses valeurs d'entrée, que le budget temps n'est jamais dépassé. Bien entendu, il faudra réajuster le budget si l'on découvre des dépassements! Mais ce n'est pas un moindre mérite de cette méthode que de permettre, dès les couches hautes de la conception, de vérifier au moins partiellement les propriétés temporelles du programme.
Notons enfin que l'utilisation du paquetage ADPT est très nettement préférable à une solution où l'on se contenterait de mettre des PUT_LINE (et des delay) un peu partout pour réaliser des corps prototypes. Cette dernière solution peut être extrêmement dangereuse : comment être sûr que tous les modules ont atteint leur état définitif et que l'on n'a pas oublié des PUT_LINE (ou pire des delay!) quelque part? En utilisant ADPT en revanche, il suffit de demander au gestionnaire de programme la liste des modules qui en dépendent. S'il n'en existe plus, tout va bien. S'il en existe encore (mais que l'on pense qu'en fait tous les modules ont atteint leur état final), il suffit d'enlever les clauses with ADPT des modules concernés, et de recompiler. Si jamais il restait encore des appels aux fonctionnalités du paquetage, le compilateur aura l'extrême obligeance de vous les signaler...
Ce paquetage ADPT est un outil extrêmement puissant, à toujours posséder dans sa bibliothèque Ada. Évidemment, il n'y a aucune raison de le standardiser (au sens ISO); on doit plutôt l'adapter en fonction de son goût personnel et de ses habitudes de programmation.
Avantages et inconvénients du maquettage progressif.
La notion de maquettage progressif constitue la mise en œuvre pratique de la notion de parcours horizontal du V de développement dont nous avions parlé en première partie (paragraphe Le parcours horizontal du V de développement). Son avantage principal réside dans l'absence de phase d'intégration finale : le projet est en permanence vérifié dans sa globalité. Ceci donne l'impression d'avancer toujours sur du terrain solide, puisqu'on ne fait pas un pas en avant sans avoir auparavant vérifié que cette progression était cohérente et s'intégrait aux besoins du reste du projet.
Cette démarche favorise également le développement par équipes (relativement) indépendantes : on donne à un groupe la réalisation effective d'un composant dont on lui fournit la maquette. Celle-ci constitue donc une définition opérationnelle (mais réduite) de la fonctionnalité à réaliser. Elle constitue le contrat de réalisation, et il est toujours possible de comparer le comportement du module complet à celui défini par la maquette. On peut également définir (et mettre au point) les programmes de tests unitaires sur la maquette, avant d'aborder la réalisation complète; la validation du module définitif consistera à exécuter correctement ces tests unitaires qui sont connus avant le début de la réalisation. On voit que dans ces conditions, il est aisé de conduire un développement indépendant, en ignorant le contexte global dans lequel le composant devra être intégré.
De plus, une fois définies les maquettes, chaque partie de l'équipe de développement peut s'occuper de sa partie en utilisant les corps maquettes des autres modules en cours de développement dont elle a besoin; on évite ainsi les goulots d'étranglement qui se produisent lorsqu'une partie du projet dépend de la disponibilité d'un module particulier.
Mais le développement progressif pose également quelques difficultés. Le plus gros problème est celui du suivi. Dans un développement en «chute d'eau» classique, il est aisé d'établir des plannings de développement, avec des points précis (appelés jalons) à atteindre à des dates données. Souvent, ces points sont vérifiés et ont des conséquences contractuelles et financières : on trouvera souvent dans des contrats de développement des clauses comme :
Avec la notion de parcours horizontal du V de développement, il n'est plus possible de définir de telles étapes. Tout au plus pourrait-on parler de pourcentage du projet implémenté «de façon définitive», par opposition aux éléments encore à l'état de maquette. Mais la définition du pourcentage et l'évaluation de l'état de la réalisation seraient fortement subjectives, et trop difficiles à définir pour permettre d'avoir des implications financières, comme le paiement d'avances sur contrat.
Indépendamment même des considérations financières, il est facile de répondre à un responsable qui s'enquiert de l'état de développement d'un projet : «Nous avons fini la conception générale, et nous sommes à environ 30% des spécifications détaillées.» Le responsable pourra se faire une idée de l'état d'avancement du projet. Il est plus difficile de répondre : «Nous avons implémenté le troisième niveau d'abstraction, et nous travaillons à l'implémentation du quatrième avec certaines maquettes de cinquième niveau opérationnelles.»
Une échappatoire consiste à définir une équivalence entre le développement de certains plans d'abstraction et les phases habituelles du développement, en particulier lorsque l'on dispose d'une méthode bien formalisée. On peut alors spécifier par contrat (en utilisant les critères de HOOD) :
Mais ceci ne constitue qu'une tentative de «raccrocher» le processus de développement progressif aux éléments classiques de la chute d'eau, dont on cherche au contraire à se séparer. On voit donc que le maquettage implique une remise en cause complète de tout le processus de gestion de projet, et l'on ne dispose pas encore de cadre bien admis permettant d'assurer ce suivi.
Cette difficulté à définir l'état d'avancement d'un projet par rapport à la réalisation finale peut paraître anecdotique, pour ne pas dire ridicule, aux développeurs individuels ou à ceux qui n'ont pas eu à développer des projets en milieu industriel; elle est en fait suffisamment forte pour interdire totalement le développement progressif dans certains contextes.