# Méthodes de génie logiciel avec Ada/Deuxième partie

* Avant propos, préface, introduction
* Une présentation rapide du langage Ada
* Langages et méthodes
* Méthodes avec Ada
* Composants logiciels
* Organisation de projet et choix fondamentaux
* Une méthode orientée objet pour les projets
* Conclusion
* Annexes
* Bibliographie
* Glossaire
* Avant propos, préface, introduction
* Une présentation rapide du langage Ada
* Langages et méthodes
* Méthodes avec Ada
* Composants logiciels
* Organisation de projet et choix fondamentaux
* Une méthode orientée objet pour les projets
* Conclusion
* Annexes
* Bibliographie
* Glossaire
[ Modifier le sommaire ]
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.
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 :
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.
    La clause is separate permet de compiler séparément un corps de sous-programme, tout en gardant toutes les propriétés (notamment la visibilité et le contrôle des types) qu'il aurait s'il se trouvait effectivement à la place de la clause.
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 :
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 :
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.
    Ce paquetage n'est en fait qu'une pré-instanciation du paquetage générique Text_IO.Integer_IO, qui permet d'avoir des entrées-sorties sur n'importe quel type entier. Il n'était pas obligatoire en Ada 83.
Nous allons donc utiliser ce composant :
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.
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.
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.
* ↑ Qui n'a jamais entendu des remarques du genre : «Pas la peine de remettre à jour la variable X, elle est écrasée par le prochain module»?
* ↑ Le même reproche peut d'ailleurs être adressé à C, à FORTRAN (avec les COMMON), etc.
* ↑ Ada, pour ceux qui ne l'auraient pas reconnu !

## 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 « collections de données » et des « collections de sous-programmes ». 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.
* Collection de données
* Collection de sous-programmes

### 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 :
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 :
Il peut ensuite, dans l'ancienne procédure ainsi que dans toutes celles qui en ont besoin, écrire :
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 « reprendre la main ».

## Exercices

* Analyser en programmation structurée un système d'affichage de la vitesse d'un véhicule. On suppose que l'on dispose d'un dispositif physique qui fournit une impulsion à chaque tour de roue.
* Analyser en programmation structurée un programme qui compte le nombre de lettres, de mots et de phrases d'un fichier texte.
* Expliquer pourquoi la programmation structurée est très utilisée pour les systèmes à fortes contraintes temps réel et/ou de haute sécurité.

# 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.
    Nous proposons d'appeler «orientée objet» toute méthode dont le critère de décomposition horizontale est l'objet, en tant qu'abstraction d'un objet du monde réel.
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 :
* L'encapsulation. L'objet n'est ni une structure de programme, ni une structure de données, mais regroupe en une même entité à la fois les attributs (données) et les opérations (sous-programmes) associés. De plus, la structure interne (implémentation) doit pouvoir être cachée afin de garantir l'indépendance de la vue externe (abstraite) par rapport à la vue interne.
* L'indépendance temporelle. Les objets étant définis par eux-mêmes, on peut définir le comportement d'un objet indépendamment du contexte dans lequel il est appelé.
* L'indépendance spatiale, ou localisation. Tous les aspects relatifs à une même entité sont physiquement dans le même module. Il est donc plus facile de faire des composants autonomes.
* ↑ Appelées méthodes dans le jargon orienté 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.
* Machine abstraite
* Type de donnée abstrait
* Gestionnaires de données
* Classification des opérations sur objets

### 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 :
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 :
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.
    Le type Compteur utilise le mot clé limited dans sa déclaration d'article. Un tel type est appelé «intrinsèquement limité» : l'affectation est interdite même là où l'on a la visibilité sur le type complet, et le langage garantit que tous les passages se feront par adresse; il n'y a donc jamais duplication. Les autopointeurs ne sont pas limités aux tâches : on peut les utiliser pour lier n'importe quels types.
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 :
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 :
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 :
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.
    Il existe également une opération Initialize, appelée lors de la création de l'objet, et une procédure Finalize, appelée lors de la destruction de l'objet; cette dernière permet de récupérer l'espace mémoire alloué.
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.
* ↑ Ce point a été identifié par M. Gauthier, dont on trouvera les travaux dans [Gau94].

### 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.
* ↑ Titre obligeamment emprunté à un article de M. Gauthier [Gau92].
* ↑ Ce qui explique qu'on ait pu utiliser si longtemps – et avec succès – la programmation structurée, jusqu'à l'explosion de complexité des logiciels que nous connaissons actuellement.

### 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
### Principes de la méthode

* Notion de niveaux d'abstraction
* La COO 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 :
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 Ecriture :
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 :
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é :
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.
* ↑ Dans le programme réel qui nous a inspiré cet exemple, environ 80% du code est lié à l'interface utilisateur!

## 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.
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.
* ↑ Tous les objets appartenant à une classe possèdent les mêmes propriétés, ils forment donc une classe d’équivalence au sens mathématique - d’où le nom.

### 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.
* Types étiquetés
* Extension de type et héritage; redéfinition de méthodes
* Classes, polymorphisme et liaison dynamique
* Liaison avec le parent
* Types abstraits
* Dissimulation d'information
* Facettes multiples

### 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.
* Gestionnaires de données hétérogènes
* Implémentations multiples
* Objets répartis