Une méthode orientée objet pour les projets de taille moyenne
Nous avons vu dans la quatrième partie qu'il était possible de définir une méthode «maison» adaptée au développement d'un projet particulier.
C'est ce processus que nous allons tenter d'illustrer dans cette partie. Le but n'est donc pas tant de développer «une méthode de plus» que de montrer comment les idées que nous avons développées peuvent être mises en œuvre à l'intérieur d'un cadre méthodologique fondé sur les méthodes existantes.
=Cahier des charges de la méthode=
Énoncé.
Comme indiqué dans le titre de cette partie, nous voulons développer une méthode pour des projets de taille moyenne (soit, en première approximation, de 10000 à 100000 lignes de code). Nous visons donc une classe de problèmes qui requiert une véritable équipe de développement, mais de taille telle qu'il soit encore possible d'organiser des réunions rassemblant la totalité de l'équipe. Il s'agit d'une méthode de conception détaillée: on suppose que l'on dispose d'un cahier des charges (qui n'est pas forcément ni aussi complet, ni aussi précis que l'on pourrait le souhaiter) et que le but est d'obtenir un programme réalisant ce cahier des charges. En particulier, le problème des spécifications matérielles est supposé résolu, ou tout au moins suffisamment avancé pour permettre aux développeurs logiciels de ne pas s'en soucier.
La méthode doit servir pour des applications à longue durée de vie: la documentation, la compréhensibilité de la structure du projet par des personnes n'ayant pas participé au développement initial, sont donc primordiales. En revanche, on souhaite limiter les phénomènes d'avalanche documentaire observés avec des méthodes destinées aux problèmes plus importants: la documentation doit rester concise et abordable.
La méthode ne doit pas imposer de trop fortes contraintes: il faut qu'elle aide le développeur sans lui imposer un cadre trop rigide qui risquerait, en l'absence de directives «militaires», de l'en détourner. À priori, la méthode s'appliquera à tous les domaines de l'informatique. En particulier, elle doit pouvoir être mise en œuvre pour des applications de type «temps réel», hors peut-être celui du temps réel à très fortes contraintes de performances et/ou de sécurité pour lequel des méthodes spécifiques sont nécessaires.
Cette méthode doit permettre d'utiliser au mieux le langage Ada. En particulier, elle doit prendre en compte les nouvelles possibilités apportées par Ada 95. Le processus de développement utilisera le maquettage progressif; compte tenu de la taille visée, il est encore possible de gérer l'avancement du projet en travaillant ainsi; en revanche, la facilité d'intégration qu'il apporte est un grand facteur d'économie d'effort de développement. Enfin, nous souhaitons promouvoir la réutilisation et le développement de composants logiciels, ainsi que l'évolutivité, tout en maintenant la sécurité. Nous nous plaçons donc à un niveau intermédiaire entre la méthode de Booch, qui est trop imprécise pour être utilisée directement et la méthode HOOD qui vise des projets de taille supérieure et où la réutilisation n'est pas une critère fondamental.
Pourquoi avoir choisi ce niveau pour illustrer notre propos? Parce qu'il s'agit d'une taille de problèmes qui requiert l'utilisation d'une méthode, mais où les programmeurs peuvent encore être tentés de n'en utiliser que de façon informelle. Nous pensons donc que le «marché» de la méthode est important.
Quelques idées directrices.
Choix de la méthode de départ.
Nous l'avons déjà dit, mais on ne le répétera jamais assez: la complexité est l'ennemi n°1 de l'informaticien. Il faut arriver à un morcellement du travail tel que chaque élément pris individuellement reste toujours simple: soit qu'il s'agisse d'un petit morceau de projet aux spécifications parfaitement déterminées (on ne se préoccupe pas alors du contexte dans lequel il intervient), soit au contraire qu'il s'agisse d'un élément de plus haut niveau qui se contente d'utiliser des services de niveaux inférieurs, oubliant alors délibérément comment ceux-ci fonctionnent en interne. Le cloisonnement strict des rôles jouera donc un rôle fondamental dans la méthode. Or nous avons vu que ce cloisonnement ne peut être obtenu avec les méthodes orientées objet par classification: notre base de départ sera donc une méthode orientée objet par composition. Ceci n'exclut pas d'utiliser l'héritage, mais seulement dans les couches profondes de la conception. En fait, un des buts de la méthode est de permettre d'utiliser l'héritage fourni par Ada 95 lorsqu'il est utile, en évitant la complexité inhérente aux méthodes par classification. Nous définirons donc cette méthode comme une méthode orientée objet avec priorité à la composition. Et comme il est de bon ton de donner un nom aux méthodes, nous l'appellerons C-FOOD pour Composition-First Object Oriented Design.
Hiérarchisation.
La méthode doit permettre de définir des couches successives, clairement identifiées et avec une profondeur supérieure à 2! Trop souvent les méthodes passent directement du premier niveau d'analyse à l'implémentation. Nous chercherons au contraire à définir un processus qui favorise l'organisation du logiciel en niveaux suffisamment nombreux pour que la complexité de chacun reste dans les limites de ce qui est facilement compréhensible.
Documentation et méthodologie.
Nous avons précédemment expliqué que les documents qui servent au processus de conception ne sont pas les plus appropriés à la maintenance. Nous introduirons donc dans la méthode une phase de documentation de la solution obtenue, qui ne proviendra pas des documents de conception.
On cherchera à minimiser cette documentation: elle ne devra comprendre que ce qui est nécessaire à la compréhension de la structure du programme. On rendra au contraire le code aussi auto-documenté que possible, en ne laissant à la documentation externe que ce qui ne peut s'exprimer directement au niveau du langage. Nous utiliserons même Ada (avec, en particulier, l'aide du paquetage ADPT), comme moyen d'expression de nos conceptions.
Phases.
Toute méthode est un processus itératif; elle permet d'identifier des sous-problèmes, que l'on résout en leur appliquant à nouveau la méthode (cf. paragraphe Surmonter la complexité). Cependant, il existe des éléments à analyser au début du projet, avant de pouvoir lancer l'analyse itérative; et il est bon de tirer un bilan après achèvement de la conception. Ces étapes doivent être formalisées au niveau de la méthode, mais sont distinctes du processus général de conception itérative. Aussi définirons-nous trois phases dans notre méthode: la phase d'initialisation où nous mettons en place la structure du projet, la phase de conception itérative proprement dite et une phase de conclusion une fois celle-ci terminée.
=Description de la méthode=
Principes de la méthode.
La méthode que nous proposons ne fait plus de différence entre conception préliminaire, conception détaillée, analyse et codage. Elle définit une suite progressive de descriptions du problème, par niveau sémantique décroissant. Chaque étape est validée par des techniques de maquettage avant de passer à l'étape suivante. La méthode est itérative et s'arrête lorsque la description obtenue est complète, c'est-à-dire ne comporte plus de partie maquettée.
Elle est essentiellement fondée sur la méthode de Booch, le critère de décomposition horizontale étant l'objet en tant qu'abstraction d'un objet du monde réel et le critère vertical le niveau d'abstraction. Les changements que nous proposons visent à systématiser dans ce cadre le maquettage progressif et l'utilisation (ou la définition) de composants logiciels réutilisables, à préciser le processus de conception lui-même et à définir une politique de documentation. Nous avons de plus incorporé certaines idées provenant d'autres méthodes apparentées, de HOOD notamment.
Première phase: initialisation.
Comprendre le problème.
Avant même de songer à une éventualité de solution, il est indispensable de s'assurer que l'on a bien compris le problème. On dispose généralement d'une description, que nous appellerons pour simplifier le cahier des charges. Rien ne prouve a priori que ceux qui l'ont rédigé maîtrisaient eux-mêmes la complexité de ce qu'ils ont demandé. Il faut donc commencer par faire une revue de détail du cahier des charges, s'assurer que la description du travail à effectuer est claire et faire la chasse aux imprécisions, manques, non-dits et contradictions. On supposera au départ que ces imperfections existent; si l'on n'en trouve pas, c'est que notre compréhension du problème est insuffisante.
Toute zone d'ombre dans le cahier des charges doit être discutée en équipe par les développeurs et faire l'objet d'une demande de clarification auprès du client s'il apparaît qu'elle résulte effectivement d'un défaut du cahier des charges. Rien n'est plus dangereux en effet que de partir sur une interprétation «évidente», qui se révélera (mais trop tard) ne pas correspondre à ce qui était effectivement demandé. En particulier, penser que le client a rédigé le cahier des charges en utilisant son propre «fonds culturel». Des notions, des termes qui lui paraissent évidents peuvent être interprétés de façon différente par l'équipe de développement. On veillera donc en particulier à ce que tous les termes employés, surtout ceux qui bénéficient d'un sens communément accepté, soient définis de façon précise dans le cahier des charges.
L'analyse de premier niveau.
La plupart des méthodes ne font pas un cas particulier de la première étape. En particulier, dans les méthodes orientées objet, il faut considérer le système à concevoir comme un objet (au sens de la méthode), semblable à ceux qui seront produits par le déroulement de l'analyse. Cela conduit généralement à des difficultés; le cahier des charges est rarement exprimé en termes «orienté objet», mais plus souvent en termes fonctionnels. L'expression des exigences sous forme d'objets est alors rarement naturelle, conduisant à baptiser «objets» des éléments qui ne sont pas vraiment des abstractions.
Nous préférons reconnaître qu'en général, le premier niveau d'analyse s'exprime mieux sous forme fonctionnelle. Puisque le cahier des charges exprime un comportement, ce premier niveau d'analyse, qui se concrétisera par le programme principal, peut parfaitement décrire ce comportement. Pour sa réalisation, on utilisera les opérations d'objets qui sont les composants de premier niveau du système. On peut donc voir le programme principal comme une sorte d'automate, dont le fonctionnement met en œuvre, anime, des objets. Cette étape diffère donc de la conception itérative essentiellement en ce que le point de départ ne peut être considéré comme un objet. Elle conduit cependant à l'identification, puis à la définition, d'objets de la même façon que celle décrite dans la phase de conception itérative.
Deuxième phase: la conception itérative.
Nous allons maintenant aborder la phase principale du développement. Elle est itérative et, comme pour une boucle de programme, il faut définir les invariants de boucle et les conditions de terminaison.
Par «invariant de boucle», nous entendons qu'à chaque étape nous devons partir d'une description bien définie des objets à concevoir pour aboutir à la définition d'objets de plus bas niveau nécessaires à leur réalisation, décrits de la même manière. Une étape de la conception itérative correspond donc à un plan d'abstraction: à partir de spécifications existantes, réalisation du corps de l'objet à concevoir et définition des spécifications des objets nécessaires, que nous appellerons pour les différencier les objets utilisés. Le plan d'abstraction est ensuite vérifié en le compilant et éventuellement en fournissant des corps-maquettes permettant d'exécuter l'ensemble. Les objets utilisés deviennent alors les objets à concevoir de l'itération suivante.
Le processus itératif s'arrête soit quand les objets utilisés peuvent être réalisés directement sans faire appel à des objets de plus bas niveau, soit quand tous les objets utilisés existent déjà, dans le projet ou dans des bibliothèques d'objets réutilisables.
Cette description formelle du processus ne doit surtout pas faire croire qu'une fois la spécification d'un objet établi, elle est immuable; nous verrons même que notre méthode prévoit explicitement une phase de remise en cause des éléments de l'étape précédente. Il arrivera donc souvent qu'une étape ultérieure conduise à un changement d'interfaces d'objets déjà établis et provoque donc une modification d'implémentation d'un plan d'abstraction supérieur. C'est normal; mais quand cela se produit, il faut revenir en arrière, revérifier toutes les conséquences de la nouvelle spécification (éventuellement en remettant à jour les maquettes associées) avant de donner son aval à la nouvelle spécification. Le processus de conception doit donc être vu comme globalement descendant, mais peut présenter des oscillations locales, comme illustré dans la figure 38. Autrement dit, ce qui est important, c'est qu'à chaque remise en cause on remonte un peu moins haut et que l'on descende un peu plus bas.
Figure 38: Modèles de conception descendante
Notre hypothèse de départ en début d'itération est que nous disposons de définitions des objets à concevoir, syntaxiquement sous forme de spécifications Ada et sémantiquement sous la forme d'une documentation décrivant leurs propriétés. Ces spécifications sont supposées suffisantes pour réaliser le plan d'abstraction qui les a définies. Nous allons maintenant détailler la démarche de conception que nous proposons.
Reprendre les spécifications.
En entrée de cette étape, nous disposons de spécifications d'un objet à concevoir. En sortie, nous devons produire des spécifications révisées.
Un objet résultant d'une phase d'analyse précédente a été simplement défini spécifiquement, c'est-à-dire que l'on a identifié une certaine abstraction dans le but de résoudre un problème particulier. Celui qui a identifié l'objet en a nécessairement eu une vue étroite: celle limitée aux seules fonctionnalité nécessaires à la solution de son problème. La spécification qui nous sert en entrée doit donc être considérée comme un minimum à réaliser: elle correspond aux besoins immédiats de celui qui l'a définie.
La première étape consiste à étudier le problème, c'est-à-dire analyser l'objet du monde réel dont l'objet à concevoir est une abstraction. Nous utiliserons toujours le terme «objet», bien qu'il puisse s'agir d'une classe, ou même parfois d'unités purement fonctionnelles. Afin de ne pas être influencé par cette vue particulière, il est bon d'ignorer a priori le contexte dans lequel l'objet sera utilisé. Si la taille de l'équipe le permet, il est préférable que la réalisation de l'objet soit confiée à une personne qui n'a pas participé à sa définition. Le point important est de répondre à la question: cet objet à concevoir, qu'est-ce que c'est? Pour des objets de haut niveau, c'est-à-dire au début du projet, la réponse est loin d'être triviale et peut conditionner largement l'architecture de la solution. Il est bon alors de discuter du problème en réunion générale de toute l'équipe. On sera surpris de la différence de vues que des personnes différentes peuvent avoir d'objets apparemment simples.
Par exemple, dans un système informatique d'entreprise, on identifiera la nécessité d'un objet «client». Il faudra se demander ce qu'est réellement un client. Dans un projet où nous avons été amené à intervenir, nous avons pu constater que le terme «client» recouvrait des entités tellement différentes que nous avons dû abandonner totalement cette notion au profit de plusieurs autres dont la définition était moins controversée, comme «responsable légal», «prospect», «destinataire de livraison»...
Il faut conserver dans le cahier «historique» les minutes de ces réunions, avec les différentes idées qui y sont apparues. Il est fréquent qu'une direction rejetée lors de la discussion apparaisse plus tard comme une possibilité intéressante: l'archivage des discussions permet alors de repartir sur une nouvelle voie.
Cette étape produit la spécification Ada définitive de l'objet et une fiche de description de l'objet qui présente ce que l'objet est et ce qu'il fait du point de vue abstrait, c'est-à-dire sans référence à une quelconque implémentation possible. Si des ajustements ont été nécessaires, cela peut avoir une influence sur le niveau d'abstraction précédent; en particulier, cela peut entraîner des modifications dans le corps des objets du niveau supérieur. Ceci est normal. Un grand danger de la conception est de figer trop tôt certaines décisions: on continue sur la même voie sous prétexte de ne pas perturber l'existant et bien souvent on s'aperçoit, mais beaucoup plus tard, que la modification est absolument indispensable et la perturbation risque d'être beaucoup plus importante. Il vaut donc mieux admettre qu'il existe des (petites) «oscillations» des spécifications à la frontière entre deux niveaux d'abstraction. L'important avec notre méthode est que ces perturbations sont limitées au niveau immédiatement supérieur et ne «percolent» pas dans toute la hiérarchie du projet.
Définir le plan d'abstraction.
En entrée de cette étape, nous disposons des spécifications définitives d'un objet à concevoir. En sortie, nous devons produire le corps de cet objet et les spécifications des objets utilisés par ce corps. Rappelons que c'est l'ensemble constitué d'une implémentation (vue concrète du niveau N) et des spécifications des objets de plus bas niveau utilisés (vues abstraites du niveau N+1) que nous appelons un plan d'abstraction.
L'objet à concevoir étant maintenant correctement défini, l'étape va consister à identifier les sous-objets qui le composent. Ceci conduit à élaborer une stratégie informelle d'implémentation. Bien entendu, diverses solutions sont possibles; il convient d'en envisager plusieurs afin d'être à même de faire un choix. C'est la phase de plus pure création intellectuelle, donc celle la moins susceptible d'une formalisation. L'expérience et l'exemple restent les meilleurs moyens pour mener à bien cette étape.
Nous allons présenter les différents points importants dans l'ordre où normalement on les aborde. L'ordre séquentiel de la présentation ne doit pas être pris comme une obligation; en général, on se préoccupera plus ou moins simultanément de ces différents aspects, de même qu'il est impossible d'étudier séparément les différents objets du plan d'abstraction: l'analyse porte sur plusieurs d'entre eux simultanément.
Tester le plan d'abstraction.
En entrée de cette étape, on dispose de la spécification du plan d'abstraction. Il faut produire les corps maquettes des objets utilisés pour tester le plan d'abstraction et servir de référence ultérieure pour la conception.
La compilation du plan a permis de terminer l'implémentation de l'objet à concevoir et de la vérifier sur le plan syntaxique, ce qui donne déjà quelque espoir d'avoir évité les incohérences. Si le problème est suffisamment simple, on peut en rester là. Mais il faut souvent vérifier, au moins partiellement, le comportement dynamique du plan d'abstraction. Ceci s'obtient en écrivant des corps «maquettes» des objets utilisés grâce au paquetage ADPT. Une utilisation judicieuse des durées simulées sert à ajuster le «budget temps» des opérations. Cette maquette est conservée comme documentation dynamique de la façon dont le concepteur voyait le comportement de ses objets utilisés. Elle peut aussi servir à faire tourner l'ensemble du programme, notamment en cas de développement en équipe.
Le projet global peut être pris comme programme de test: on intègre tout le plan d'abstraction, y compris ses sous-implémentations maquettes, et l'on regarde si cela fonctionne. Si tout va bien, c'est parfait. Mais si l'on découvre des erreurs, il peut être difficile de les piéger dans le contexte de l'ensemble du projet. Aussi est-il préférable d'écrire des programmes de tests unitaires pour chaque plan d'abstraction. Ces programmes seront conservés comme documentation et pour effectuer des tests de non-régression par la suite. Il est possible (et même préférable) d'écrire le test unitaire avant l'implémentation du module testé. On peut vérifier le test au moyen de l'implémentation maquette de l'unité que l'on est sur le point de réaliser effectivement. Ceci garantit l'aspect «boîte noire» du test et permet de se prémunir contre des déviations dues à l'implémentation (ce qui n'empêche pas d'enrichir le test par la suite, suite à la découverte d'erreurs qu'il n'avait pas initialement diagnostiquées). Bien sûr, le test peut faire découvrir des erreurs; les spécifications et les divers documents précédents peuvent donc encore évoluer.
Documenter le plan d'abstraction.
En entrée de cette étape, nous disposons d'une description opérationnelle du plan d'abstraction. Nous devons produire la documentation associée.
Itération.
À ce point, les objets sont prêts à entrer à leur tour dans le cycle de réalisation. Rien n'oblige à les confier à ceux qui les ont définis. En fait, on augmentera les chances d'indépendance des modules en évitant que ceux qui définissent les objets les implémentent.
On gère dans le projet une liste d'objets à implémenter: à l'issue d'une itération, les développeurs rajoutent les nouveaux objets à la fin de la liste et prennent comme nouveau travail la tête de liste. Une telle façon de faire constituera un excellent test de la compréhensibilité des spécifications!
Troisième phase: récapitulation et analyse.
Une particularité de notre méthode est qu'il n'y a pas du tout de phase d'intégration, ou plus exactement que l'intégration a lieu en continu tout au long du projet: depuis le début, le système est complet (vu de l'extérieur), même s'il est uniquement constitué de maquettes grossières. Chaque étape va faire descendre le niveau des maquettes jusqu'à ce qu'elles aient totalement disparu: à ce point la réalisation est terminée. Mais à tout moment, le projet est dans un état stable, complet et vérifié.
Une fois le projet terminé, il est bon de se livrer à une récapitulation (debriefing) du projet. On en analyse les qualités et les faiblesses. Certains choix du début ont pu se révéler à l'usage non optimaux; le gain apporté par une autre solution n'était pas suffisant pour justifier un retour en arrière, mais il est bon pour l'avenir d'identifier les points sous-estimés lors du choix initial qui ont fait passer à côté d'une meilleure solution. On peut identifier des possibilités d'évolution pour une version ultérieure: des fonctionnalités qui n'étaient pas demandées, mais pourraient se révéler utiles plus tard; des modifications de structure qui n'ont pu être effectuées par manque de temps, mais que l'on pourrait entreprendre à l'occasion d'une évolution du logiciel, etc. Enfin, on se livre à une analyse de réutilisabilité. On cherche des modules développés de façon spécifique, mais qui seraient candidats à une généralisation ultérieure pour les transformer en composants réutilisables, selon le processus d'analyse a posteriori présenté dans la troisième partie de ce livre.
Résumé de la méthode.
Les phases de la méthode sont résumées dans le schéma de la figure 39.
Figure 39: Schéma des phases de la méthode
Critique de la méthode.
Il n'existe pas de méthode universelle, mais seulement différentes formes de compromis, qui peuvent se révéler plus ou moins adaptés aux besoins. Que peut-on dire de la méthode que nous venons de présenter?
C'est une méthode orientée objet par composition, qui autorise des hiérarchies de types au moment du choix des représentations, mais où l'héritage n'a qu'une faible importance. Elle s'applique bien aux projets manipulant de nombreuses entités du monde réel; elle peut être moins adaptée à ceux essentiellement procéduraux, comme des compilateurs ou des modélisations mathématiques, pour lesquels des techniques de programmation structurée sont plus adaptées. Elle accorde la plus grande attention à la découpe en couches soigneusement isolées, grâce à la notion de plan d'abstraction. Ceci autorise une technique d'implémentation par démaquettage progressif, qui fait disparaître la phase d'intégration en tant que telle.
Le langage Ada est utilisé comme moyen d'expression des décisions de conception à toutes les phases du projet. Ceci peut surprendre en donnant l'impression de «coder» tout de suite. Le but est d'avoir un langage d'expression unique depuis la conception jusqu'à la réalisation; cela n'est possible que grâce au très haut niveau d'abstraction fourni par Ada. L'utilisation de la méthode avec un langage de plus bas niveau serait difficile, voire dangereuse, car il ne serait pas possible d'exprimer des décisions de conception sans faire en même temps des décisions d'implémentation – ce que l'on veut éviter.
La méthode essaie de limiter la quantité de documentation, notamment en fournissant autant que possible du code auto-documenté. Ceci n'est possible que parce que la méthode ne vise que les projets de taille moyenne; cette politique peut ne pas être applicable à des projets de taille plus importante. Enfin, la méthode ne formalise pas les contraintes temporelles au-delà de la possibilité de gérer un «budget temps». Elle n'est donc pas adaptée au développement de projets temps réel critiques.
En résumé, son domaine d'efficacité maximum est celui de la réalisation d'applications informatiques courantes, sans contraintes de certifications ni de performances trop exigeantes et de taille moyenne... ce qui représente tout de même une bonne part des développements logiciels.
Exercices.
=Exemple d'utilisation de la méthode=
Ce chapitre décrit un exemple d'utilisation de la méthode que nous venons de définir. Généralement, les exemples publiés dans les «bons ouvrages» proposent directement la solution parfaite, avec beaucoup d'explications montrant comment on l'a obtenue naturellement. En fait, la conception de logiciel, comme toute activité créatrice, comporte des tâtonnements et des erreurs. Nous avons essayé dans cet exemple de suivre la conception d'une application en n'occultant pas les erreurs initiales, mais au contraire en les montrant et en expliquant comment elles ont été identifiées et corrigées pour aboutir à la solution. De même, nous avons montré les hésitations que nous avons éprouvées lors de choix entre plusieurs solutions et essayé d'expliciter le processus qui nous a amené à nos décisions de conception. Il importe donc de lire le chapitre en entier avant de décider que l'auteur raconte n'importe quoi... En revanche, nous invitons le lecteur à réfléchir par lui-même aux solutions possibles à chaque étape avant de lire celles que nous proposons. Peut-être en trouvera-t-il qui ne figurent pas ici; qu'il n'en déduise pas que les siennes sont mauvaises, mais simplement que cela montre la richesse des choix possibles et la nécessité de réfléchir avant de se précipiter sur la solution «évidente».
D'autre part, les contraintes propres à un livre nous empêchent de fournir tous les documents et de suivre précisément toutes les étapes qui sont nécessaires à un développement industriel. Que le développeur industriel n'en profite pas pour croire qu'ils sont moins nécessaires que le reste!
Phase initiale.
Comprendre le problème.
La célèbre société «L'Automatique de Distribution Alimentaire» (A.D.A.) nous a chargé de réaliser le logiciel de contrôle d'un distributeur automatique de boissons. L'appareil distribue du café (avec ou sans lait, avec ou sans sucre), du thé, des boissons froides... Il accepte toutes les pièces et rend la monnaie. Les différentes boissons n'ont pas forcément le même prix. Le logiciel doit permettre de changer aisément le tarif des boissons et être conçu de façon réutilisable pour éviter de trop gros changements en cas d'évolution des éléments matériels sous-jacents.
Nous notons qu'un tel cahier des charges est «ouvert»: tout le monde connaît ce genre d'appareil, aussi ne nous a-t-on fourni que peu de détails sur les contraintes. Bien que ceci nous laisse relativement libre, c'est par nature dangereux: la quantité de «non-dit culturel» est maximale et notre vue risque de ne pas correspondre à celle du client. Dans un projet réel, il faudrait prévoir une réunion avec le client à la fin de la phase de conception initiale, afin de vérifier que la structure adoptée correspond bien à celle attendue.
Choix fondamentaux.
Bien entendu, un exemple livresque ne saurait avoir les mêmes contraintes qu'un projet industriel. Disons que le contexte de développement correspondrait aux choix fondamentaux suivants:
Analyse de premier niveau.
Une première discussion du problème aboutit à la formulation suivante:
Tester le plan d'abstraction.
Nous avons maintenant un schéma général qui paraît satisfaisant. Les spécifications que nous venons d'établir peuvent être compilées pour vérifier qu'elles sont cohérentes. Nous pouvons vérifier dynamiquement le plan d'abstraction en fournissant des corps maquettes tels que :
with ADPT; 
package body Unité_Fabrication is
 procedure Confectionner (Le_produit : Produit) is
 use ADPT;
 begin
 ADPT_Action ("Confection du produit " &
 Produit'IMAGE (Le_produit));
 end Confectionner;
end Unité_Fabrication;
On trouvera en annexe les corps maquettes des autres paquetages. On obtient ainsi une première version exécutable modélisant le comportement du programme à haut niveau, sans avoir encore décidé des modalités de réalisation des couches inférieures.
Documenter le plan d'abstraction.
Sans fournir une documentation aussi complète que l'exigerait un produit industriel, nous allons résumer les caractéristiques de la solution obtenue.
Le distributeur est constitué de quatre objets principaux: l'unité de fabrication qui s'occupe de la fabrication effective des produits, le monnayeur qui gère tout ce qui est lié à la notion d'argent, le menu qui s'occupe de tout ce qui est dialogue avec l'utilisateur et le tarif qui relie les produits à leur prix. La figure 40 schématise cette architecture. Remarquer que nous avons représenté les produits et l'argent sous forme de bus logiciels: la structure réelle est ainsi beaucoup plus apparente. On remarque également qu'il n'existe aucune dépendance entre les quatre modules principaux; ceci correspond bien au fait qu'ils constituent des sous-systèmes indépendants, dont il serait aisé de confier le développement à des équipes séparées. Cette indépendance provient évidemment du fait que ces objets correspondent directement à des sous-systèmes physiques de l'appareil, qui sont eux-mêmes indépendants.
Figure 40: Architecture du distributeur de boissons
Deuxième itération.
Arrivé à ce stade, nous avons une définition satisfaisante, vérifiée, de tous les objets au premier niveau d'abstraction; il faut maintenant les reprendre un à un et les implémenter effectivement, c'est-à-dire examiner de façon détaillée comment on peut les réaliser. Les objets que nous avons empilés (c'est-à-dire ceux pour lesquels nous ne disposons pas encore de l'implémentation définitive) sont :
Notre but n'étant pas d'écrire ici l'intégralité du logiciel, nous n'allons pas détailler l'implémentation de tous ces objets jusqu'au niveau final, d'autant que certains sont liés au matériel. Nous allons juste en reprendre quelques-uns, pour montrer des exemples (terminaux et non terminaux) du processus d'itération de la méthode.
Le produit.
Il est temps maintenant de décider de la représentation effective du Produit. Nous avons déjà vu que ce devait être un type discret. Le premier niveau de maquettage n'a fait apparaître aucun besoin nouveau: il s'agit d'un simple identifiant totalement neutre; nous avons vu qu'à la limite, on pouvait assimiler le produit à un numéro de bouton. Un type énumératif serait parfaitement acceptable, puisque nous n'avons besoin d'aucune opération arithmétique, mais présente cependant un inconvénient: il est plus difficile à faire évoluer, par exemple si l'on crée un nouvel appareil disposant d'un nombre supérieur de boutons. Nous choisirons donc de le représenter par un type entier. Les bornes peuvent être données par une constante du paquetage, ou dans un paquetage global définissant la configuration matérielle, en particulier le nombre de boutons (et donc le nombre de produits pouvant être distribués).
package Définition_Produit is
 Max_produit : constant := 10;
 type Produit is range 1..Max_produit;
end Définition_Produit;
Récapitulation et analyse.
Les objets que nous avons identifiés sont extrêmement généraux et cette structure peut servir à réaliser n'importe quel automate simple. Le monnayeur est directement réutilisable pour tout appareil devant gérer de l'argent (parcmètre, machine à sous...). L'unité de fabrication n'est liée à aucune particularité des boissons: c'est seulement au niveau des distributeurs d'ingrédients qu'apparaît une spécialisation.
Mais le point le plus important est l'évolutivité : comment réagirait le programme en cas de changements dans les distributeurs physiques, pourrait-on le réutiliser pour d'autres appareils? Un changement de commande des appareils électromécaniques n'impliquerait que des changements dans le corps de l'unité qui les modélise. L'ajout d'un nouvel appareil nécessiterait l'écriture d'une nouvelle classe dérivée; on pourrait alors l'introduire dans la configuration et l'utiliser dans des recettes sans autre modification. De même, le monnayeur pourrait être remplacé par un appareil plus perfectionné : lecteur de carte de crédit par exemple. Cela ne changerait rien au reste du programme. Pour changer le prix des boissons ou la composition des produits, il suffit de changer une table dans le système, certainement une PROM amovible, permettant de remettre à jour le distributeur sans changer le programme.
Remarquons que cet exemple est indiscutablement orienté objet: tous les éléments manipulés par le programme sont des abstractions correspondant directement aux modules matériels de l'appareil. Cependant, l'héritage ne joue pas un rôle prépondérant: nous l'avons utilisé là où il était utile, mais, n'ayant pas raisonné par classification mais par composition, il n'apparaît pas dans l'organisation générale du projet.
Nous terminerons par une remarque plus philosophique. Le lecteur a peut-être eu par endroits le sentiment que certains états d'âme étaient abusifs; qu'il existait une solution «bestiale» qu'il suffisait d'appliquer sans se poser tant de questions. Comme nous avons utilisé Ada comme langage d'implémentation, il pourrait en tirer la conclusion qu'Ada . En fait le problème n'est pas là. Spécifier complètement les comportements même dans les cas limites, étudier systématiquement plusieurs possibilités même lorsqu'une solution paraît évidente, chercher à définir des entités réutilisables doivent (devraient ?) être un souci permanent du développeur quel que soit le langage utilisé. Il se trouve simplement que le pouvoir d'expression d'Ada permet d'exprimer et de maquetter ces différentes solutions, là où d'autres langages n'offrent qu'une seule possibilité. Le programmeur Ada aura donc tendance à se poser plus de questions; mais dans un contexte de génie logiciel, ceci doit être vu comme un avantage important du langage.