# Méthodes de génie logiciel avec Ada/Quatriè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 ]
Conduire un projet, ce n'est pas seulement adopter une méthode de conception et écrire du code. Un développement s'effectue en équipe, et même en utilisant une méthode rigoureuse, des choix fondamentaux dont dépendra le succès du futur produit doivent être effectués, dès avant le démarrage du projet et jusqu'aux dernières étapes de la réalisation.
En fait, il faut être convaincu que la solution à un problème informatique n'est jamais unique, et que toute décision a des avantages et des inconvénients. L'informaticien doit donc être avant tout une personne capable d'arbitrer des compromis. Hélas, trop souvent il se précipite sur la première idée qu'il trouve au lieu d'évaluer les différentes possibilités. Souvent, il ne se rend même pas compte qu'il a fait un choix, tant la solution adoptée lui paraît «évidente».
Dans cette partie, nous allons donc expliquer l'organisation générale d'une équipe de programmation, puis détailler les principaux choix intervenant à différents niveaux du développement, afin de faire prendre conscience de tous les points qu'il importe d'étudier soigneusement avant de se lancer sur son clavier!

# L'équipe de développement
## Les rôles à remplir

La réalisation d'un projet logiciel quelque peu important ne peut se faire qu'en équipe. Trop souvent, celle-ci ne comporte qu'un chef de projet (qui met parfois la main à la pâte) et des développeurs qui font la conception, le codage et la mise au point. En fait, dans toute équipe de développement, il existe un certain nombre de rôles qui doivent être remplis. Bien qu'ils puissent varier légèrement, on trouve généralement:
* Un chef de projet, qui doit coordonner l'équipe et à qui reviendra la responsabilité de trancher lorsque plusieurs choix techniques sont possibles et que l'unanimité ne peut être obtenue parmi les concepteurs.
* Un responsable financier, qui suivra l'état d'avancement du projet et l'évolution des dépenses par rapport au planning prévu (qu'il aura lui-même établi).
* Un gestionnaire de configuration, chargé de la réception des modules, de leur intégration, et de l'historique des différentes versions ainsi que de leur archivage.
* Un responsable documentation, pour suivre la production de la documentation, s'assurer de sa compréhensibilité et de son exactitude. Il sera également chargé de la rédaction de la documentation «utilisateur» du produit.
* Un responsable qualité, qui doit définir les critères de qualité du projet et vérifier leur bonne application.
* Un responsable composants logiciels, chargé aussi bien de mettre à disposition des autres membres les composants qui peuvent leur être nécessaires que d'identifier les éléments susceptibles de devenir des composants logiciels réutilisables par la suite.
* Un responsable communication qui assurera la bonne circulation de l'information dans l'équipe, qui tiendra à jour les comptes rendus des réunions, et auprès duquel les autres membres devront retrouver les informations dont ils auront besoin.
* Et bien entendu les développeurs proprement dits, chargés de la réalisation informatique du projet.
La taille de l'équipe de développement ne permettra pas en général d'affecter une personne différente à chacun de ces rôles. Il n'empêche que chacun d'entre eux doit être rempli par quelqu'un; plusieurs rôles peuvent donc être dévolus à la même personne.

## Affectation des différents rôles

Le chef de projet est généralement choisi par des instances supérieures, ainsi qu'un ensemble de personnes chargées de réaliser le projet. C'est la première responsabilité du chef de projet d'affecter les rôles aux différents membres de l'équipe. Bien que ceci dépende fortement de la personnalité des individus, on peut donner quelques directives générales à ce niveau, surtout quand la taille de l'équipe ne permet pas d'affecter une personne par rôle.
Le rôle du responsable financier peut être tenu par le chef de projet lui-même: surveillant «d'en haut» la marche générale du développement, il est en bonne position pour cela.
Le gestionnaire de configuration a un rôle très important, et souvent sous-estimé. Ce rôle peut souvent être tenu par le responsable qualité, qui peut également s'occuper de la gestion des composants logiciels. On évitera en revanche autant que possible de faire tenir ce rôle par quelqu'un qui serait également activement impliqué dans le développement: un certain recul par rapport à la conception est en effet nécessaire.
Le rôle du responsable documentation sera également impérativement tenu par quelqu'un qui n'est pas directement un développeur; en particulier, on prendra soin que la documentation utilisateur soit rédigée par quelqu'un qui ne connaît pas directement les modalités techniques de la réalisation. Trop souvent, les contraintes imposées aux utilisateurs résultent des problèmes techniques rencontrés par la réalisation. Le responsable documentation devra en particulier veiller à refuser des solutions «simplificatrices» au niveau de l'implémentation qui apporteraient des complications au niveau de l'utilisation. Ce rôle peut également être tenu par le responsable qualité... sous réserve qu'on ne lui ait pas déjà affecté trop d'autres rôles.
Selon la taille de l'équipe, le rôle du responsable communication est attribué au chef de projet, à sa (son) secrétaire, au responsable qualité...
En tout état de cause, c'est la responsabilité du chef de projet de s'assurer avant tout que chacun des rôles est tenu par quelqu'un de compétent pour ce faire, qu'aucun rôle n'est présenté ni perçu comme moins «noble» qu'un autre, et que chaque responsable a effectivement compris l'étendue de sa mission.

## Fonctionnement de l'équipe

Toute la difficulté du travail en équipe vient de ce qu'il est nécessaire d'établir des règles, des modalités de travail, des procédures à respecter, etc. Or le but final doit être l'efficacité de l'équipe dans son ensemble. Il importe donc de faire en sorte que le mode de travail établi soit une aide pour tous, et non une gêne. La première condition est que chacun des rôles soit rempli par quelqu'un de compétent. Ceci doit être fait de façon positive; en particulier, si l'équipe de développement a été déterminée a priori, cela nécessite d'identifier les compétences de chacun et de les ventiler au mieux sur les différents rôles, sans établir pour cela de hiérarchie de valeurs: tel qui se révèle fort piètre développeur peut faire un excellent gestionnaire de configuration.
De même, les différentes étapes nécessaires à l'acceptation d'un module ne doivent pas être présentées comme des sanctions, mais comme une reconnaissance d'un travail bien fait. Le responsable qualité doit en particulier faire preuve de psychologie, notamment lorsqu'un module ne peut être accepté: il doit fournir son aide au réalisateur en cause, non le pénaliser. En tout état de cause, il faut être sûr que dans l'équipe, tout le monde sait qui est responsable de quoi. L'équipe fonctionnera au mieux si chaque membre perçoit les rôles de chacun des autres membres comme des services complémentaires concourant au succès commun, s'il s'adresse au responsable concerné pour résoudre les problèmes de son ressort (sans chercher à les résoudre lui-même), et si réciproquement il est prêt à aider les autres membres pour les services qui relèvent de ses compétences propres.

# Le choix de la méthode

Dans la deuxième partie de cet ouvrage, nous avons vu différentes méthodes de développement. Comme il n'existe jamais de remède miracle en informatique (pas plus qu'ailleurs), aucune méthode ne peut, à elle seule, résoudre toutes les classes de problèmes. Lors d'un nouveau projet se pose donc inévitablement le problème du choix de la méthode la plus appropriée. Ce choix est d'autant plus crucial qu'il conditionnera pour une grande part le succès ou l'échec du système. Enfin, choisir la «bonne» méthode n'est pas suffisant: il faut aussi choisir une méthodologie associée et s'assurer qu'elle est acceptée et respectée par ceux qui doivent l'utiliser.
Le choix d'une méthodologie doit donc s'effectuer en deux étapes: déterminer tout d'abord la grande classe de méthode: structurée, entités-relations, orientée objet par composition ou classification; choisir ensuite la méthodologie proprement dite pour mettre en œuvre la méthode. Il existe deux possibilités à ce niveau: soit adopter une méthodologie existante, soit concevoir une méthodologie «maison». Quelle que soit la solution choisie, il faudra veiller à bien définir la méthodologie adoptée et à l'appliquer rigoureusement. Le but étant bien entendu d'éviter la troisième solution: annoncer que l'on suit telle ou telle méthodologie, mais laisser chaque membre de l'équipe l'interpréter ou apporter ses propres «variations» à sa guise (même si c'est pour des motifs -apparemment- tout à fait justifiés); car alors, c'est la porte ouverte aux incohérences et aux difficultés de maintenance.

## Critères de choix d'une méthode

Il existe certes des critères objectifs de choix de méthode; mais là plus qu'ailleurs, d'autres aspects, notamment psychologiques, entrent en ligne de compte: habitudes ou compétences existantes, disponibilité d'outils, formation des personnels... et mode. Nous ne présenterons ici que les critères objectifs; il appartient au chef de projet de les pondérer en fonction des autres facteurs, notamment de la culture d'entreprise. Certaines sociétés par exemple ont un savoir-faire issu directement de l'automatique. Une méthode par machines abstraites, soutenue par une méthodologie telle que Buhr, sera plus adaptée. D'autres ont une longue expérience en composition ou en classification: la tendance naturelle sera de poursuivre dans cette voie, surtout qu'il y a alors plus de chances de pouvoir réutiliser des parties de logiciels ou des composants provenant de développements antérieurs.
* ↑ Le seul critère qu'il soit bon d'ignorer. Hélas, il est parfois plus important dans certaines décisions techniques qu'on ne pourrait le croire.

### Programmation structurée

Un des principaux avantages des méthodes structurées est qu'il y est plus facile de prévoir les temps d'exécution, puisque l'organisation du logiciel reflète l'ordre d'exécution. On les préférera donc pour des logiciels ayant des fortes contraintes de prédictibilité des temps de réponse. On pourra également être amené à les utiliser lorsque le cahier des charges est purement fonctionnel: il peut alors être plus simple d'implémenter directement celui-ci que de l'adapter à la structure requise par d'autres méthodes. De toute façon, on veillera à ne les utiliser qu'avec des logiciels de taille modeste et pour lesquels la réutilisabilité n'est pas un critère primordial.

### Méthode orientée objet

Les méthodes orientées objet sont les méthodes favorites actuellement de par leur aptitude à maîtriser la complexité et à produire des modules réutilisables. On les préférera sauf raison particulière justifiant une autre méthode.
Reste le problème du critère de décomposition verticale principal: composition ou classification. L'effet de mode conduisant à des «guerres de religion», il est particulièrement délicat de démêler les critères de choix réellement objectifs. Les considérations sur la complexité des dépendances font préférer la composition pour les logiciels de grande taille et à longue durée de vie. La composition se prête également mieux au développement par maquettage progressif que nous avons exposé précédemment, puisque celui-ci est fondé sur la notion de plans d'abstraction. Inversement, si le projet nécessite une écriture rapide et relève du domaine de la recherche, la plus grande dynamicité et la facilité de modification des couches hautes peuvent faire préférer la classification. En fait, il semblerait que les facteurs psychologiques soient prédominants dans ce choix: certaines personnes semblent raisonner spontanément par composition, alors que d'autres adoptent d'instinct une approche par classification. Nous avons pu ainsi assister à de véritables dialogues de sourds entre concepteurs qui se représentaient mentalement leurs objets de façon opposée... Quelle que soit la solution adoptée, il est donc primordial que chacun des membres de l'équipe comprenne l'approche choisie et l'adopte, même si cela ne correspond pas à son mode naturel de pensée.

### Entités-relations

Les méthodes entités-relations tendent à donner la place prépondérante dans la conception aux données, les traitements n'intervenant ensuite que par rapport aux données. De plus, le modèle implicite (mais non obligatoire) est celui des bases de données relationnelles. On utilisera donc plutôt ces méthodes lorsque les données sont prépondérantes, ou lorsqu'une base de données doit constituer le cœur du système informatique.
Ces caractéristiques correspondent souvent aux applications de gestion, mais pas nécessairement. C'est le profil de l'application, et non le fait qu'elle soit classée (ou non) «gestion», qui doit amener à décider d'utiliser ces méthodes.

## Choix d'une méthodologie existante

La solution la plus simple, une fois une méthode choisie, consiste à adopter une méthodologie existante. La méthodologie est publiée, on dispose donc de manuels de référence. On trouvera facilement des stages de formation pour les membres de l'équipe qui ne la connaîtraient pas; enfin, il existe des outils de génie logiciel disponibles commercialement pour la mise en œuvre.
La taille du projet joue un rôle prépondérant dans le choix. D'une façon générale, plus une méthodologie est rigoureuse, détaillée et précise, plus elle est également lourde à mettre en œuvre. La quantité de documentation exigée par HOOD par exemple ne se justifierait pas pour un projet de 10 000 lignes de code (une méthode de Booch «de base» est alors suffisante), alors qu'elle devient indispensable au-delà de 100 000 lignes. Certaines méthodologies sont très (trop?) générales. Par exemple, OMT peut exprimer aussi bien des diagrammes de composition que de classification. Il est parfois souhaitable de fixer des limites à l'utilisation de certaines méthodologies afin de restreindre l'étendue des possibilités, donc de renforcer les contrôles. On préférera, surtout dans de grands projets, des méthodes définissant clairement une dimension verticale, telles que Rose ou HOOD, à d'autres qui tendent à représenter tous les objets du programme «à plat» et offrent moins de possibilités de contrôler la complexité par une découpe en niveaux étanches.
N'oublions pas non plus que si l'on peut choisir une méthodologie au niveau d'un projet, il est souvent nécessaire d'acheter les outils correspondants, et que la direction financière souhaiterait pouvoir les amortir sur plusieurs projets... Il convient donc de pondérer les exigences d'un projet particulier par celles des autres projets typiques de l'entreprise, afin de permettre de réutiliser aussi le matériel... et les conceptions elles-mêmes.

## Définition d'une méthodologie d'entreprise

L'absence d'une méthodologie correspondant bien aux besoins spécifiques d'un projet ou de l'entreprise en général, parfois le prix des outils, peuvent conduire à la décision de définir une méthodologie propre. Bien sûr, cela suppose d'avoir un ou des projets de taille suffisante pour amortir l'effort supplémentaire de définition de la méthode. Dans ce cas, la meilleure solution consiste souvent à repartir d'une «grande» méthodologie bien établie, et à l'adapter localement aux besoins ou contraintes particuliers. Il s'agira donc généralement non de méthodologies complètement nouvelles, mais de «panachages» d'idées empruntées à plusieurs d'entre elles. Nous montrerons dans la cinquième partie comment l'on peut définir une telle méthodologie.
Si le fonds de départ est suffisamment proche d'une méthodologie commerciale, il est parfois possible d'utiliser ou d'adapter des outils existants. Sinon, il faudra développer ses propres outils, ce qui viendra grever lourdement le budget destiné à la définition de la méthodologie. Il existe cependant des «méta-ateliers» de génie logiciel, qui fournissent une base commune permettant à l'utilisateur de créer les outils supports de n'importe quelle méthode.

## Réticences et difficultés

Quelle que soit la méthodologie adoptée, elle doit être comprise et acceptée par ses utilisateurs, sous peine de mauvaise mise en œuvre, avec des résultats inverses de ceux que l'on en attendait. Citons quelques raisons pouvant conduire à un problème d'acceptation:
* La méthode n'est pas bien comprise. Elle exige par exemple le remplissage de documents de conception dont le concepteur ne perçoit pas toujours l'utilité. On court alors le risque que les documents soient remplis machinalement, parce que le chef l'exige, mais ils seront alors de peu d'aide pour les programmeurs de maintenance qui les reliront par la suite. La solution à ce problème réside bien entendu dans une formation adéquate. Il peut arriver également que le chef ait «trop» bien compris la méthode, et insiste sur son application au-delà du raisonnable et aux dépens des autres aspects du projet.
* La méthode ne correspond pas aux besoins. Une méthode orientée plutôt temps réel mettra l'accent sur les propriétés temporelles, alors que les méthodes orientées gestion accordent plus d'importance aux données. Une méthode mal choisie va donc focaliser l'attention du programmeur sur des points qui ne correspondent pas à ses préoccupations principales. Une méthode trop lourde par rapport à la taille du projet sera également difficilement acceptée, mais inversement une méthode insuffisante pour un projet complexe risque de mettre en danger la faisabilité même du programme.
* L'utilité de la méthode n'est pas perçue. En particulier, les programmeurs qui ont eu l'habitude de travailler sans méthode bien définie, dans des petites équipes (quand ce ne sont pas des programmeurs individuels), ont du mal à se plier à la démarche contraignante d'une méthode formalisée. Il convient donc de leur en faire sentir la nécessité, par exemple en les faisant travailler quelque temps à la maintenance d'un projet existant.
* La méthode est mal utilisée. Les gens formés à une ancienne méthode tendent à propager leurs habitudes précédentes dans la nouvelle. Il est fréquent de rencontrer ainsi des dérives fonctionnelles dans des projets censés être orientés objet. Cela provoque des problèmes de conception, ou rend les outils incapables de fournir le service escompté. Là encore, une formation appropriée est le meilleur antidote.
En conclusion, mieux vaut une méthode moins bonne sur le papier, mais comprise, appliquée et acceptée par l'équipe de développement, qu'une méthode théoriquement parfaite, mais mise en œuvre de façon inappropriée.
* ↑ C'est souvent le cas des débutants frais émoulus de l'université.
* ↑ C'est également vrai des langages de programmation. On dit qu'un bon programmeur FORTRAN est capable d'écrire du FORTRAN dans n'importe quel langage...

## Exercices

* Quelle méthode serait la plus appropriée pour développer des interfaces graphiques? Un logiciel de transactions bancaires? Un programme de simulation de physique nucléaire? Défendez vos réponses.
* Le choix d'une méthodologie ressemble au choix de composants logiciels. Appliquer le raisonnement du paragraphe Identification à la décision de choisir une méthodologie existante ou, au contraire, d'en définir une nouvelle.

# Politiques de projet

Dans ce chapitre, nous allons aborder les différents éléments qui interviennent dans la phase préliminaire que l'on pourrait baptiser le projet avant le démarrage du projet. Soulignons que dans les projets réels, celle-ci est souvent négligée: les choix correspondants sont alors effectués au hasard, sans réelle étude des différentes possibilités et des compromis nécessaires... avec le risque d'aboutir à des solutions fort éloignées de l'optimum.

## Choix du langage de programmation

Le langage de programmation utilisé dans un projet doit résulter d'un choix délibéré. Même si l'auteur de ces lignes a une préférence particulière, il faut bien reconnaître que de nombreuses contraintes peuvent influencer le choix du langage: temps de développement par rapport à la durée de vie du projet, compatibilité avec l'existant ou héritage provenant de projets plus anciens... L'important étant (encore une fois) d'éviter les choix implicites: ceux qui ne sont motivés par aucune décision rationnelle et dont l'origine ne peut être tracée. Nous discutions ainsi un jour avec un chef de projet dont le développement s'effectuait en C++. Le dialogue s'établit à peu près comme ceci:
    Moi: Pourquoi avez-vous choisi C++ pour ce développement?
    Lui: Parce que c'est ce que M. Xxx avait mis dans la proposition.
    Moi: Et pourquoi M. Xxx l'avait-il mis dans la proposition?
    Lui: ??? Oh, il l'avait mis dans la proposition...
Je ne pus obtenir aucune autre justification pour ce choix technologique qui gouvernait tout l'avenir du projet (au demeurant fort important) que le fait que M. Xxx avait estimé que cela ferait bien dans la proposition!
* ↑ Le langage Ada, vous n'aviez pas remarqué ?

### Le langage principal

En général, il est préférable de n'utiliser qu'un seul langage pour le développement: moins de compétences nécessaires, uniformité du projet, pas besoin d'outils multilangages... Cependant, certaines contraintes particulières peuvent nécessiter d'utiliser plusieurs langages, et nous y reviendrons dans la section suivante. Mais même dans ce cas, l'un d'entre eux doit être choisi comme langage principal, les autres n'ayant qu'un rôle annexe. En effet, si du point de vue technique rien ne s'opposerait au mélange des langages, nous avons vu en première partie que le langage de développement amène avec lui une certaine philosophie, et le mélange des philosophies serait beaucoup plus difficile à gérer que le mélange des langages.
Unique ou principal, un langage doit être choisi, qui imposera sa façon de faire à l'ensemble du projet. Nous avons présenté tout au long de ce livre les avantages d'Ada pour la mise en œuvre des principes du génie logiciel. Voyons donc quelques raisons qui peuvent conduire à ne pas choisir Ada.
* ↑ Des systèmes comme VMS, ou des environnements de compilation comme GCC mélangent très facilement les langages. Seul UNIX privilégie abusivement un langage (C) au détriment des autres.
* Logiciels jetables
* Domaines très spécialisés
* Utilisation d'éléments ou d'outils existants

### Cohabitation de plusieurs langages

Il existe des cas où il est raisonnable d'utiliser plusieurs langages pour un même développement logiciel. Citons par exemple:
* Reprise de bibliothèques ou de composants logiciels existants, développés dans d'autres langages. En particulier, il se peut que certains algorithmes critiques de sécurité aient été validés (souvent au moyen de méthodes formelles). On préférera alors les réutiliser plutôt que de les récrire et de devoir passer par toutes les phases de certification, toujours longues et coûteuses.
* Présence d'algorithmes relevant d'une technique particulière: interfaçage avec des bases de données, raisonnement formel ou déductif.
Dans ces cas, on a un langage principal qui fait appel à des services écrits dans d'autres langages. Dans le cas où ce langage principal est Ada, on connaît deux techniques pour assurer la liaison: l'interfaçage direct et les bulles.
L'interfaçage direct consiste à appeler directement des sous-programmes écrits dans les autres langages. On donne une spécification Ada, mais on déclare simplement au moyen d'un pragma Import que les corps correspondants ont été écrits dans l'autre langage. Ceci règle les problèmes de convention d'appel, mais ne garantit pas nécessairement la cohérence des données. Aussi Ada fournit-il le paquetage Interfaces, qui sert de base à des paquetages enfants, un par langage, décrivant en termes Ada les types de données prédéfinis de ces autres langages. La norme a standardisé les paquetages Interfaces.C, Interfaces.FORTRAN et Interfaces.COBOL. Grâce à cela, l'écriture des spécifications Ada correspondant à des bibliothèque s d'autres langages est grandement facilitée.
    Le pragma Import remplace le pragma Interface d'Ada 83. Son principal avantage est de s'appliquer non seulement à des sous-programmes, mais aussi à d'autres entités: variables, constantes, classes... Le pragma Export permet symétriquement de mettre des entités Ada à disposition d'autres langages.
Le terme de bulle a été avancé par Pitette [Pit87,Pit88] pour décrire la technique d'interfaçage du système AdLog (CR2A), visant à permettre à des applications Ada d'accéder à de la programmation logique. L'idée de base est que certains problèmes particuliers relevaient de techniques déductives, et que le langage Prolog était alors le plus approprié pour les résoudre. Cependant, un programme réel se limite rarement à ses seules parties déductives: il faut au moins une interface utilisateur. De même, certains systèmes de nature «temps réel» font appel par endroits à des techniques déductives: l'exemple typique étant un système de contrôle aérien utilisant une base de règles pour reconnaître les avions d'après leur signature radar. AdLog permet d'écrire ces algorithmes déductifs en Prolog, et un précompilateur associé les traduit en structures de données Ada. Celles-ci sont alors exécutées par un interpréteur Prolog, lui-même écrit en Ada. Ainsi, un programme physiquement «tout Ada» peut par moments faire appel à des résolutions isolées, les «bulles», écrites en Prolog (et mises au point par un interpréteur Prolog classiques).
D'autres interfaçages utilisent des techniques voisines sans employer la même dénomination, notamment SAME [Iso94]. L'idée de base est qu'une application de gestion comporte des parties purement procédurales couplées à des requêtes à des bases de données. SAME définit un langage, SAMEdL (SAME Definition Language), dont la syntaxe est proche d'Ada, mais la sémantique dérivée de SQL. Ce langage est facilement accessible à des spécialistes des bases de données chargés de réaliser la partie «requêtes». Un outil génère automatiquement l'interface vers le système de base de données et produit des paquetages dont les spécifications constituent des vues «Ada» des requêtes. La partie impérative peut alors être réalisée en Ada par des concepteurs ne connaissant pas nécessairement les subtilités des bases de données.

## Organisation des bibliothèques de programme

La bibliothèque de programme joue un rôle central pour le développement en Ada. Son organisation est de la responsabilité du gestionnaire de configuration. Il devra étudier les différentes possibilités offertes par le compilateur et décider d'une structure adaptée au projet. Par exemple, si le compilateur dispose à la fois des notions de liens et de sous-bibliothèque, on peut décider d'avoir une bibliothèque principale pour les développements spécifiques du projet, et une autre (qui sera liée à la première) pour les composants logiciels stables et immuables. De plus, la bibliothèque principale contiendra les modules officiellement acceptés (après contrôle par le responsable qualité), alors que chaque développeur bénéficiera d'une sous-bibliothèque pour ses développements propres. Ainsi, chacun peut bénéficier des modules «officiels» de ses voisins et travailler sur ses propres modules en développement sans perturber les autres membres.
Bien entendu, ceci n'est qu'un exemple, et d'autres organisations sont possibles selon les besoins du projet et les possibilités du compilateur; en particulier, le mécanisme des liens, des familles et/ou des sous-bibliothèques peut être mis à profit pour gérer les versions successives du logiciel, les différentes options de configuration (ou les différentes cibles)... Certains environnements de développement offrent des possibilités extrêmement évoluées en la matière. Il n'existe qu'un seul point absolu à retenir: l'utilisation des possibilités doit être décidée, dès le début du projet, et appliquée de façon uniforme par tous les membres de l'équipe. Bien entendu, comme tout choix fondamental, ces décisions d'organisation doivent être justifiées et référencées dans le document conservant les raisons des choix de conception.

## Style et restrictions d'usage

Le style d'un projet se doit d'être uniforme: cela facilite la maintenance en permettant à n'importe qui d'intervenir sur n'importe quelle partie d'un projet sans risquer d'être dépaysé. De nombreuses études ont été publiées sur le sujet; en ce qui concerne Ada, la meilleure base de départ est l'«Ada Quality and Style Guide» [Spc94], édité aux États-Unis par le Software Productivity Consortium et mis dans le domaine public, ce qui permet de l'utiliser sans problème. Il donne de nombreux conseils et présente les solutions les plus fréquemment retenues. Il convient cependant de l'«instancier», c'est-à-dire de choisir les modalités pratiques à utiliser en fonction des grandes lignes qu'il présente. Quelques points qu'il importe de fixer sont:
* les détails de la présentation: indentation, retraits... Le plus simple est d'adopter la présentation du manuel de référence.
* la convention d'utilisation des minuscules et des majuscules: généralement, on met les mots-clés en minuscules et les identificateurs avec une majuscule initiale. On peut décider de mettre les identificateurs globaux, ou tout au moins ceux reconnus comme particulièrement importants, entièrement en majuscules.
* les conventions de nommage. Il est par exemple fréquent de nommer les types avec des identificateurs commençant par «T_», ou se terminant par «_type».
* la décision d'utiliser (ou d'interdire) les clauses use. Ce point est l'objet d'un vif débat dans la communauté Ada, aussi y reviendrons-nous dans la partie concernant les règles de nommage.
* la forme des commentaires et leurs normes d'utilisation: commentaires standardisés en tête ou en fin de module rappelant les grandes fonctionnalités, l'auteur, l'historique, etc.; commentaires utilisés pour repérer ou séparer les différents sous-programmes; placement des commentaires par rapport à la séquence de code à laquelle ils se rapportent.
Suivant le domaine du projet, il peut également être nécessaire d'interdire l'utilisation de telle ou telle possibilité du langage. Par exemple, un logiciel devant fonctionner 24 heures sur 24 interdira l'usage de toute allocation dynamique, car même si l'on peut assurer que toute variable allouée est rendue au bout d'un temps fini, cela ne garantit pas que la mémoire ne s'épuisera pas, à cause des effets de fragmentation mémoire. Une telle restriction ne s'applique parfois pas aux phases de démarrage de l'application, où l'on peut créer des variables de façon dynamique, mais ces variables dureront de façon permanente. On rencontre également des restrictions sur l'emploi du parallélisme, totales (pas de tasking) ou partielles (pas de rendez-vous, ou au contraire pas de types protégés). Certaines contraintes de validabilité peuvent exiger également l'interdiction de fonctionnalités de haut niveau. Depuis Ada 95, il existe un pragma Restriction qui permet de faire vérifier par le compilateur la bonne application de ces restrictions d'usage. On peut ainsi écrire:
Il existe également des outils permettant de vérifier la bonne application des règles de style, ou de remettre un code «aux normes». La forme la plus courante de ce genre d'outil est le «reformateur» (ou «pretty printer»), fourni avec la plupart des compilateurs Ada, qui redonne une présentation correcte à un programme. Comme en matière de style l'uniformité est plus importante que les détails du style adopté, il est parfaitement raisonnable d'adopter la politique des outils dont on dispose. Le compilateur GNAT possède par exemple une option de compilation qui refuse toute unité dont la présentation ne correspond pas au style... adopté par les auteurs du compilateur. Bien qu'initialement destinée à l'écriture du compilateur lui-même, on peut parfaitement décider d'adopter cette présentation, d'ailleurs fort acceptable, pour la seule raison que l'on dispose du moyen de la faire vérifier automatiquement.

## Politiques de gestion des erreurs
### Principes généraux

Lorsque l'on parle de gestion des erreurs, l'on pense immédiatement «exceptions». C'est bien normal, puisqu'Ada a popularisé cette puissante fonctionnalité qui fait défaut à beaucoup de langages, même si le concept figurait déjà dans des langages antérieurs. Notons au passage que la plupart des langages ultérieurs (notamment C++ et Eiffel) ont également adopté des fonctionnalités de déroutement sur événements exceptionnels. On trouvera dans [Goo88] un résumé des principales contraintes liées à l'utilisation des exceptions. Il ne faudrait cependant pas croire que les exceptions soient suffisantes pour gérer les erreurs d'exécution: elles constituent seulement un mécanisme linguistique indispensable pour implémenter des politiques de gestion d'erreurs, qui doivent être définies en fonction du contexte du projet.
Pour bien comprendre le problème, revenons sur ce qu'est une situation exceptionnelle. Un sous-programme est chargé de rendre un certain service, pour un client (le sous-programme qui l'appelle). Or il est parfois impossible de rendre le service demandé: lecture s'il n'y a plus de données, inversion de matrice singulière, panne de périphérique... Ce sont ces cas que nous appelons des cas exceptionnels. Noter que ce ne sont pas nécessairement des erreurs: le fait d'atteindre une fin de fichier, par exemple, est une condition exceptionnelle (la boucle normale de traitement ne peut se poursuivre) sans être forcément une erreur (le cas est prévu dans le logiciel et constitue une situation normale). On peut dire qu'une condition anormale cesse d'être une erreur pour n'être plus qu'une exception lorsque la condition est propagée à un niveau qui l'a prévue et qui sait comment réagir pour la corriger ou la faire disparaître. La gestion des cas exceptionnels comporte trois aspects distincts: le diagnostic, le signalement et le traitement.
* Diagnostic
* Signalement
* Traitement
* Politiques

### Politique de correction locale

Cette stratégie consiste à essayer de remédier au problème à la source: le service qui diagnostique un problème tente d'y remédier tout seul en interne: il n'y a donc pas signalement. Cette stratégie est fréquemment employée dans les langages sans exceptions: une fonction recherchant une sous-chaîne dans une chaîne renverra la valeur 0 si la sous-chaîne n'est pas trouvée, une fonction mathématique imprimera un message d'erreur au terminal en cas d'argument incorrect, etc. Comme il n'y a pas signalement, il ne peut y avoir traitement: le client n'a aucun moyen d'influer sur la réponse à un incident, ni même de savoir qu'un incident s'est produit. Diagnostic, signalement et traitement sont donc totalement figés à l'intérieur de la procédure appelée.

### Politique du code de retour

La première technique est celle du code de retour: on renvoie une valeur particulière qui indique si le traitement a pu être effectué. Le diagnostic est donc interne. Plusieurs formes sont possibles: utilisation d'un paramètre out:
valeur renvoyée par une fonction:
ou utilisation d'une variable globale:
La première forme, qui paraît plus logique, est peu utilisée en pratique, car elle oblige l'utilisateur à déclarer une variable supplémentaire, et ne peut être utilisée (en Ada) avec des fonctions.
    Les fonctions Ada ne peuvent avoir que des paramètres in. La raison de cette contrainte supplémentaire est purement méthodologique.
La deuxième forme est utilisée quasiment systématiquement en C, où l'absence de typage rend cette forme parfois commode. Ainsi, une fonction peut renvoyer un pointeur, ou la valeur 0 (qui est également la valeur logique fausse!) si l'élément n'a pas été trouvé. L'inconvénient de cette façon de faire est qu'elle impose l'utilisation d'une fonction (par opposition à une procédure).
La troisième technique permet de garder les fonctionnalités sous forme de procédures ou de fonctions au choix de l'utilisateur, en n'imposant pas de déclaration de variable supplémentaire; en revanche elle est difficilement utilisable en présence de parallélisme, car entre le moment où une tâche appelle la procédure Traitement et le moment où elle va lire la variable Résultat, une autre tâche a pu appeler la procédure, et rien ne garantit que le résultat lu corresponde à la bonne exécution! Et pourtant, cette technique est largement utilisée pour les interfaces, en particulier avec POSIX qui renvoie toujours les résultats dans la variable ERRNO.
L'avantage du signalement par code de retour est qu'il est synchrone: l'utilisateur est maître de l'endroit où il effectue le test de bon achèvement du composant utilisé, et ce test apparaît explicitement dans le texte du programme. Le problème est que rien n'oblige l'utilisateur à effectuer le test, qu'il est très facile de l'oublier, et qu'alors le programme continuera en croyant qu'une certaine action a été effectuée, alors qu'il n'en est rien. Bien sûr, la forme «fonction» oblige l'utilisateur à effectuer le test, mais conduit souvent à une imbrication de if très lourde et pénible, non seulement à écrire, mais aussi à maintenir. Remarquer qu'en C, il est possible d'appeler une fonction comme si c'était une procédure, et que le résultat est alors perdu: l'utilisateur aura tendance à le faire pour des appels dont il est sûr qu'ils ne peuvent pas échouer... ce qui conduit à des catastrophes si le service échoue quand même pour des circonstances qui avaient échappé au programmeur.
* ↑ Et alors ? pensera le lecteur (avec raison). Il se trouve que l'état d'esprit de ceux qui utilisent cette technique les conduit en général à minimiser le nombre de lignes écrites.
* ↑ Ce qui a amené la norme IEEE 1003.1c, définissant le parallélisme, à définir ERRNO comme une fonction, contredisant ainsi la norme 1003.1a qui ne définit que les aspects séquentiels...

### Politique du déroutement

La politique du déroutement consiste à appeler une procédure, fournie par l'utilisateur, lors de la survenue d'événements exceptionnels. On peut la voir comme une politique de traitement local, mais où le traitement est fourni par l'utilisateur au lieu d'être imposé d'office. Certains langages offrent cette politique en standard: en PL/I par exemple, l'utilisateur peut annoncer son intention de traiter les cas de division par zéro en déclarant:
ON ZERO_DIVIDE CALL MA_PROCEDURE;
En cas d'anomalie, le compilateur appelle la procédure déclarée, qui est censée remédier au problème, à la suite de quoi on reprend le traitement normal. Il est facile d'implémenter cette politique en Ada. Il suffit de déclarer:
On active le traitement en l'enregistrant par la procédure Activer (équivalent de la clause ON de PL/I) et on le désactive par Désactiver; la fonction Traitement_Courant renvoie un pointeur sur la procédure de traitement courant, ce qui permet de la conserver et de la restaurer plus tard. En cas de condition anormale, on appelle la procédure Signaler, qui appellera la procédure de traitement active. On peut prévoir une procédure par défaut (ou lever Program_Error) si aucune procédure de traitement n'a été prévue. On peut perfectionner ce paquetage en fournissant un paramètre à Signaler et aux procédures de traitement pour indiquer la cause de l'erreur: ce peut être une chaîne de caractères, un identificateur d'exception (Exception_ID, déclaré dans le paquetage Ada.Exceptions), etc.
Avec cette politique, le diagnostic est interne, le signalement est asynchrone: celui qui fournit la procédure ne sait pas a priori quand celle-ci sera appelée, et le traitement est interne: il s'effectue depuis le sous-programme appelé. Noter que la procédure de traitement peut décider de lever une exception, et que le composant doit y être préparé (et la laisser filer). Une difficulté est qu'il faut impérativement prévoir de «débrancher» la procédure dès que l'on sort de la zone où celle-ci s'applique, sous peine de voir un traitement prévu pour travailler dans un certain contexte appelé dans un contexte totalement différent. Enfin, l'activation étant exécutable, il n'est pas possible en lisant un morceau de programme de savoir quel est le traitement actif: il faut reconstituer tout l'historique de l'exécution du code.
Une variante de cette politique a été proposée par Kruchten [Kru89, Kru90], sous le nom de politique sans exception. Elle consiste à transformer les composants en génériques auxquels on passe une procédure utilisateur, appelée par le composant en cas de problème:
Le signalement s'effectue comme dans le cas précédent par appel de la procédure fournie. Cette variante présente l'avantage que l'attachement d'un traitement est spécifique du composant, statique, et permanent dans la portée de l'instanciation. Il n'y a donc plus le problème de l'oubli du «débranchement» du traitement. Une solution intermédiaire consiste à fournir à la procédure appelée un pointeur sur une procédure à appeler en cas de problème:

### Politique du contrat

Cette technique, qui a été développée dans [Mey90], vise à empêcher les erreurs de se produire, en définissant un contrat entre l'appelé et l'appelant permettant de garantir la bonne fin du service rendu. L'idée de base est que le diagnostic doit toujours être externe. Chaque module déclare les préconditions qui doivent être respectées pour garantir son bon fonctionnement; moyennant le respect des préconditions, celui qui fait le composant garantit la vérification de la post-assertion. Pour une pile par exemple, la précondition de l'opération Dépiler est que la pile est non vide. L'opération Dépiler peut garantir la fourniture de la valeur du sommet de pile (post-assertion) à la condition que la précondition soit vérifiée. La vérification du bon respect des préconditions est à la charge de l'appelant, en conséquence de quoi aucune erreur ne peut se produire. On doit donc
Avec cette politique, le diagnostic est synchrone, puisque programmé par l'appelant, et confondu avec le signalement ; diagnostic, signalement et traitement sont entièrement à la charge de l'appelant. Ce que ne précise pas le modèle du contrat, c'est le comportement de la procédure Dépiler si on l'appelle malgré tout avec une pile vide: le résultat peut alors « légalement » être n'importe quoi : si l'appelant ne respecte pas sa part du contrat, alors l'appelé n'est plus tenu à rien. Cette méthode est donc basée sur un principe de confiance mutuelle : à l'intérieur du composant, on suppose que les pré-assertions tiennent, et on ne se préoccupe pas de ce qui pourrait se passer dans le cas contraire. C'est à l'appelant de vérifier avant l'appel le bon respect des préconditions, et s'il ne respecte pas son contrat, tant pis pour lui...
Hélas, ce système est totalement opposé au principe de programmation fiable. Si suite à une erreur dans un paramètre d'appel, votre composant a pour effet de «planter» totalement le programme, avec pour conséquence que l'avion dont c'était le système de contrôle va s'écraser avec ses 300 passagers sur les Champs-Élysées un jour de 14 juillet, votre responsabilité est dégagée : il ne fallait pas vous appeler de cette façon. Oui, certes, mais n'aurait-on pas pu spécifier aussi le comportement en cas d'appel incorrect, de façon à permettre de signaler une erreur, lancer une procédure de secours pour passer le système en mode dégradé, et éviter une catastrophe ?
Aussi cette politique ne dispense-t-elle pas d'une programmation défensive: les seules garanties que l'on a lors de l'appel d'un composant sont celles fournies par le langage. Ceci conduit généralement à une redondance de tests, puisque le composant vérifie que l'appelant a respecté sa part de contrat. En revanche, les signalements sont simplifiés, puisque tout diagnostic correspond à une faute de programmation de l'appelant: on se contentera en général de lever l'exception Program_Error. Cette exception ne sera en principe pas traitée par l'appelant, sauf pour sortir correctement du programme, en fermant tous les fichiers par exemple... et en demandant à l'utilisateur d'envoyer un rapport d'anomalie !
Les assertions jouent un rôle important dans cette politique, ce qui a amené Eiffel a introduire cette notion dans le langage lui-même. Cela n'a pas été jugé nécessaire en Ada, car il est extrêmement aisé d'écrire un paquetage fournissant cette fonctionnalité, au moins dans les cas simples. Les cas plus sophistiqués sont plutôt du niveau d'outils d'environnement, comme Anna [Luc90], qui permettent de rajouter des contrôles d'assertions à l'aide de commentaires spéciaux traités par un préprocesseur.
* ↑ Exemple dont on a tellement abusé que l'on est en droit de se demander s'il est possible de travailler formellement sur autre chose que des piles.

### Politique par exceptions simples

Cette politique, entièrement fondée sur le système d'exceptions du langage, consiste à signaler l'erreur par levée d'exception. L'utilisateur traite les erreurs dans des traite-exceptions. Le diagnostic est interne, le signalement est asynchrone, et le traitement est externe à la procédure qui effectue le diagnostic. Elle possède de nombreux avantages: sécurité, impossibilité d'ignorer la présence de l'anomalie, possibilité de sortir de nombreux niveaux imbriqués directement... C'est la seule possible pour des composants logiciels de base.
Elle présente cependant certains inconvénients lorsqu'elle est utilisée systématiquement à travers de nombreuses couches [Kru89, Kru90]. Le principe d'encapsulation veut que l'implémentation d'une fonctionnalité soit invisible à l'appelant ; or si celle-ci appelle des couches de plus bas niveau susceptibles de lever des exceptions, il faut empêcher la propagation de celles-ci, puisqu'elles proviennent d'un niveau invisible à l'utilisateur. Ceci conduit à une multiplication de traite-exceptions, en particulier dans les paquetages «relais» qui n'ont d'autre fonction que d'encapsuler, suivant le schéma ci-dessous :
S'il y a consignation des erreurs, le problème est pire: en effet dans ce cas on exige que tout traite-exception consigne l'anomalie. Lorsqu'une exception traverse ainsi de nombreuses couches, on se trouve envahi par une avalanche de consignations qui ne signalent en fait que le passage de l'exception.

### Politique Oméga

Cette politique a été également proposée par Kruchten [Kru89, Kru90], et s'applique principalement aux types de données abstraits. Elle consiste à introduire un état supplémentaire décrivant si la donnée est valide ou invalide. L'état invalide est appelé «Oméga», par référence à la valeur OM du langage SETL [Sch86]. Toute opération portant sur des données «Oméga» fournit un résultat «Oméga». Une fonction permet de savoir si une valeur est dans l'état «Oméga»; une variante non exclusive consiste à lever une exception dans ce cas. Un type entier ainsi protégé s'exprime comme:
L'intérêt de cette méthode est qu'il n'est plus nécessaire de prévoir des traite-exceptions sur toutes les phases de calcul intermédiaires; le signalement est synchrone (par appel de la procédure Erreur_si_Oméga, ou de la fonction Est_Oméga si on ne veut pas d'exception), mais le traitement est externe. La fonction Vers_Integer lève l'exception Erreur_Oméga si on l'appelle sur la valeur «Oméga», ce qui garantit qu'il est impossible de faire quoi que ce soit d'une valeur incorrecte; on évite donc ainsi l'insécurité généralement liée au signalement synchrone (si on oublie d'effectuer le test).

### Politique de gestion centralisée

Une autre possibilité pour limiter les phénomènes de cascade d'erreurs est la gestion centralisée des erreurs: lors de toute anomalie, au lieu de lever une exception, on appelle une procédure chargée de signaler l'erreur. Ceci peut s'exprimer sous la forme:
La procédure Erreur stocke le message, consigne l'anomalie si c'est demandé par le projet, puis lève l'exception Erreur_Traitée. Les traite-exceptions de la chaîne d'appel traitent les autres exceptions par appel de la procédure Erreur, mais laissent filer l'exception Erreur_Traitée, ou interrogent la fonction Message_Erreur pour connaître la cause de l'erreur d'origine. L'erreur n'est consignée qu'une fois, au moment de l'appel, et il n'est pas nécessaire de transformer une erreur provenant de couches profondes, car tous les modules ont la visibilité du paquetage Gestion_Erreurs.
Cette politique a les mêmes caractéristiques que celle par exceptions simples: diagnostic interne et signalement asynchrone. Elle évite cependant les effets de cascade en divisant le problème en deux: le traitement systématique est interne et préalable au signalement, ce qui laisse la possibilité d'un traitement complémentaire externe, dépendant du contexte de l'appelant.

### Récapitulatif des différentes politiques

Le tableau de la figure 35 récapitule et compare les principales caractéristiques des politiques que nous avons mentionnées. Il rappelle la répartition des responsabilités entre le client (l'appelant) et le fournisseur de service (l'appelé). Toutes les combinaisons ne sont bien entendu pas possibles: par exemple, un traitement interne implique nécessairement un diagnostic interne.
Figure 35: Les différentes politiques d'erreurs :
| | Diagnostic | Signalement | Traitement |
| Correction locale | Interne | Asynchrone | Interne |
| Code de retour | Interne | Synchrone | Externe |
| Déroutement | Interne | Asynchrone | Interne |
| Contrat | Externe | Synchrone | Externe |
| Exception simple | Interne | Asynchrone | Externe |
| Oméga | Interne | Synchrone | Externe |
| Gestion centralisée | Interne | Asynchrone | Interne/Externe |

## Exercices

* Imaginer un cas (justifié) où le langage principal ne serait pas Ada, mais où Ada serait utilisé comme langage secondaire.
* Justifier les choix de présentation vérifiés automatiquement par le compilateur GNAT (voir la documentation fournie avec le compilateur).
* Estimer le coût relatif des différentes politiques de gestion d'erreur du point de vue des performances. Écrire ensuite des programmes de test et comparer les mesures aux prévisions.

# Les choix de la phase de réalisation

Les choix que nous avons vus jusqu'à présent sont typiquement effectués au démarrage du projet. Mais tout au long de la conception et du codage, on est encore amené à prendre des décisions; là encore, il importe que celles-ci résultent d'une analyse délibérée et non de ce qui passait par la tête du programmeur à ce moment.
Dans le chapitre précédent, nous avions situé les problèmes généraux et montré comment Ada permettait d'y répondre; en revanche, nombre de choix exposés dans ce chapitre ne s'offrent qu'avec Ada. Est-ce qu'Ada pose plus de problèmes? Non, c'est seulement qu'Ada est un langage plus riche, et que le programmeur dispose de nombreuses possibilités là où d'autres langages lui imposent une seule façon de faire. La «simplicité» dont se vantent certains langages n'est en fait qu'un moyen de cacher la pauvreté des moyens offerts.

## Identificateurs et règles de nommage

Choisir des bons identificateurs est un point important qui est loin d'être évident. Ada offre de nombreuses possibilités, encore faut-il les utiliser à bon escient. Remarquons que tous les caractères d'un identificateur Ada sont significatifs et qu'il n'existe pas de limitation à leur longueur.
Nous allons commencer par un exemple et, exceptionnellement, nous utiliserons des identificateurs anglais. Nous verrons plus loin que l'utilisation d'identificateurs en français complique encore le problème!
Supposons que nous ayons quelque chose qui s'appelle un «contexte» (context), que nous voulions empiler (push) à l'entrée d'une procédure, pour pouvoir le restaurer à la sortie. Une expression informelle du problème serait:
    Empiler l'ancien contexte (push the old context)
Si nous voulons que l'expression Ada ressemble à cette phrase, nous pouvons le faire de différentes façons:
Ainsi que les combinaisons des précédents :
En (1), nous avons un paquetage appelé Context contenant une procédure Push, et une variable Old. Dans les autres exemples, nous ne nommons pas le paquetage; nous supposons que la clause use a été utilisée. En (2), la procédure s'appelle Push et la variable Old_Context, alors qu'en (3) la procédure s'appelle Push_Context et la variable Old. En (4), la procédure s'appelle également Push, mais la variable s'appelle Old et nous profitons d'une association nommée pour exprimer que nous empilons un contexte. En (5), la procédure s'appelle Push et la variable Old, mais nous utilisons la qualification par Context, dont nous supposons que c'est le type de la variable Old. En (6), la procédure s'appelle Push_Old_Context et ce nom porte toute l'information ; le nom de la variable (X) est sans importance.
Quel est le meilleur choix? Si vous n'êtes pas convaincu que ce n'est pas évident, faites un sondage parmi vos collègues ; il est vraisemblable que chaque solution aura la préférence de quelqu'un...
* ↑ ... ou presque. Si le compilateur a le droit de mettre une limite, celle-ci ne peut être inférieure à 200 caractères, ce qui est plus que suffisant !

### Considérations générales

Une règle fréquente est que le nom d'une entité doit être «parlant» et «suffisamment long». Un indice clair de nom mal choisi est quand le programmeur ressent le besoin de mettre un commentaire avec la déclaration d'une entité pour décrire ce à quoi elle sert. Les noms doivent se comprendre d'eux-mêmes! Bien qu'il y ait une forte part de subjectivité dans l'appréciation de la lisibilité d'un nom, rappelons quelques règles générales:
* Lorsqu'un nom est formé de plusieurs «mots», utiliser le trait bas (caractère '_') pour séparer les mots. L'usage de majuscules dans ce but peut conduire à des ambiguïtés, comme ce fichier de nombres entiers dont le nom était FichierDentiers... : En Ada, le trait bas est toujours autorisé dans les identificateurs. Dans beaucoup d'autres langages, il peut l'être ou non à la discrétion des compilateurs, ce qui interdit son usage dans les composants portables. Les utilisateurs de langages à syntaxe «C» (notamment Java) tendent à éviter l’utilisation du trait bas, alors même qu’il est autorisé par le langage.
* Les procédures expriment des actions, et doivent être désignées par des verbes; les fonctions retournent des valeurs et doivent utiliser des noms caractéristiques de la valeur retournée, ou des formes verbales de la forme «Est_...» pour les fonctions à résultat booléen. De même, les variables doivent avoir des noms caractéristiques de la valeur contenue.
* Et bien sûr, il faut éviter les abréviations.
Il existe même des outils vérifiant que tous les noms d'un programme ont au moins cinq lettres et appartiennent à un dictionnaire. Ceci fonctionne correctement en général, mais n'est pas une règle suffisante. Une formulation plus subtile serait:
    Un nom doit porter toute l'information nécessaire au lecteur pour comprendre sa signification et pas plus.
En rajouter plus qu'il n'est nécessaire a un effet néfaste sur la lisibilité. Par exemple, Line_Length est un identificateur excellent, mais The_Length_Of_The_Line_That_I_Am_Considering est absolument désastreux. Une autre règle «évidente» est:
    Un nom doit exprimer la nature de l'entité désignée.
Ceci est moins simple qu'il n'y paraît. Par exemple, [Boo84] utilise un paquetage Complex qui déclare le type Number ; ceci fait de Complex.Number un nom très parlant. Mais le paquetage lui-même n'est pas un complexe; le vrai complexe, c'est le type Number, ce qui est totalement trompeur. L'erreur provient ici de ce que le choix suppose a priori une forme d'utilisation du nom. Dans un composant logiciel, le concepteur ignore comment le nom sera utilisé. Ce qui nous amène à la règle suivante :
    Bien qu'une des formes d'utilisation du nom puisse être préférable, toutes les autres doivent être acceptables.
Avec l'exemple précédent, nous pouvons appeler le paquetage Complex_Handler et le type Complex_Number. On aurait alors les déclarations suivantes :
Noter que nous favorisons ici l'utilisateur de la clause use.
* ↑ Nous utiliserons ce terme pour dénoter tout ce qui peut avoir un nom en Ada, plutôt que le terme «objet» qui a trop de significations.
* ↑ Cet exemple n'a pas été inventé pour le «gag» : il figure effectivement dans la norme Pascal.

### Utilisation de la surcharge

Doit-on choisir un nom spécifique pour chaque opération, ou doit-on essayer de réutiliser des noms existants en profitant du mécanisme de surcharge? Dans notre exemple initial, est-il préférable de nommer la procédure Push (qui pourrait être surchargée pour des piles d'autres choses que des contextes), ou Push_Context, qui serait unique dans le système? Le but de la surcharge est l'abstraction: se concentrer sur les similitudes en ignorant pour un temps les différences. La notion d'empilement est la même, qu'il s'agisse de contextes ou d'entiers; le nom Push est donc préférable.
Prenons un autre exemple. Supposons que nous ayons une procédure qui efface (clear) l'écran et une autre qui réinitialise (clear également) une variable. Les spécifications suivantes sont autorisées par le langage:
Ici, les deux Clear ne désignent pas la même notion. Dans ce cas, nous recommandons d'appeler la première Clear_Screen. On pourrait garder le nom Clear pour la seconde, car l'argument obligatoire montrerait clairement ce que la procédure réinitialise. Nous pouvons en déduire la règle:
    Les opérations spécifiques doivent porter des noms spécifiques; les opérations générales doivent utiliser des noms généraux.

### La question de la langue

Faut-il donner aux entités un nom anglais ou utiliser le français (ou l'allemand, ou ...)? La réponse dépend du contexte général du projet. En faveur du français, on trouvera essentiellement le fait qu'il est plus compréhensible... pour les Français, qui parlent souvent assez mal les langues étrangères. Bien que ce soit l'inclinaison naturelle de beaucoup de programmeurs, cela a cependant un certain nombre d'inconvénients. Lorsque l'on développe un composant logiciel réutilisable, l'anglais est quasiment obligatoire: sinon, le composant serait inexportable en dehors du marché français, sauf à maintenir deux versions de chaque composant. La maintenance peut être simplifiée avec un outil qui traduit automatiquement les identificateurs au moyen d'un dictionnaire, mais l'effort reste important. Une solution plus pratique est de ne dédoubler que la spécification (la seule qui importe à l'utilisateur). Le corps n'utilise que les noms français, avec quelques renames pour assurer la compatibilité avec la spécification. Les seules modifications au corps en cas de changement de langue de la spécification seraient dues à la concordance des corps de sous-programmes avec leurs spécifications.
Cette question est moins cruciale pour les développements spécifiques, puisque le problème de l'exportation des sources ne se pose pas. Mais il existe d'autres raisons; l'utilisation du français diminue la lisibilité, tout au moins pour les personnes maîtrisant suffisamment l'anglais, du fait qu'Ada utilise l'anglais pour les mots réservés. L'instruction suivante est presque du langage naturel en anglais :
alors que sa traduction rappelle la nature « informatique » de la formulation :
Enfin, il faut bien reconnaître que la structure de l'anglais se prête mieux aux formulations concises et que le fait de mettre les adjectifs avant les noms correspond mieux à la notion de qualification. Si Complex.Number « sonne » bien en anglais, on ne saurait en dire autant de Complexe.Nombre... Tandis que Nombre.Complexe sonne correct en français, respectant l'ordre des mots de la langue, et ayant l'avantage que Nombre soit en premier pour regrouper tous les types de nombres.
* ↑ Un tel outil (Adasubst) est librement disponible depuis le site Adalog (http://www.adalog.fr)

### Discussion de l'exemple

Après avoir vu ces différentes règles, critiquons les différentes possibilités de notre premier exemple.
* (1) se présente bien et est facile à lire. Mais il implique que le paquetage s'appelle Context, or le paquetage n'est pas lui-même le contexte, en violation de notre première règle. Le paquetage serait plutôt un gestionnaire de contexte (Context_Handler); mais écrire Context_Handler.Push(Old) détruirait l'expression «naturelle» du problème. De plus, si l'utilisateur n'emploie pas une notation complète, Push(Old) n'exprimerait pas toute l'information.
* (2) et (3) ont bonne allure. Noter que si l'on utilise une notation complète, cela donnera Context_Handler.Push(Old_Context), ce qui est encore acceptable. L'argument en faveur de la surcharge jouera en faveur de (2).
* (3) et (5) souffrent du même problème que (1): si l'utilisateur n'emploie pas la notation prévue par le concepteur, ils se réduisent à Push(Old).
* (4) fait une hypothèse inadmissible sur l'utilisation de la procédure; rien ne dit que l'on souhaite empiler un «vieux» contexte. Si l'utilisateur veut empiler un contexte temporaire, la formulation deviendrait Push_Old_Context(Temporary), ce qui serait contradictoire.
Nous ne discuterons pas de toute la combinatoire des solutions précédentes, mais notons tout de même que (8) ne saurait être qualifié de lisible!

### Démarche

Compte tenu des remarques précédentes, on voit que le choix d'un nom, surtout pour les entités exportées, ne doit pas être fait à la légère. Nous suggérons que le concepteur considère les points suivants pour faire son choix:
* Choix du langage. L'anglais de préférence pour les composants commerciaux. Pour les composants d'entreprise, suivre la règle générale de l'entreprise.
* Identifier la nature de l'entité que l'on nomme pour choisir un nom pertinent pour cette entité, indépendamment du contexte.
* Identifier si l'opération est générale ou spécifique pour décider d'un nom unique ou tenter de réutiliser d'autres noms existants exprimant la même fonctionnalité sur des types différents.
* Faire des essais de nommage avec les différentes possibilités (avec ou sans utilisation du nom complet, avec ou sans utilisation des paramètres nommés) pour vérifier qu'aucune combinaison n'est inacceptable.

### Utilisation de la clause «use»

L'utilisation ou l'interdiction de la clause use est un vaste sujet de débat dans la communauté Ada [Bry87] [Boo86] [Ros87]. Nous allons tenter de le présenter succinctement et d'en tirer quelques conclusions.
    Les entités définies dans la partie visible d'un paquetage ne peuvent être utilisées qu'en les préfixant avec le nom du paquetage. La clause use ouvre la visibilité et permet de les nommer directement. Ada 95 a ajouté la clause use type qui rend les opérateurs d'un type (tels que "+" et "-") visibles, sans ouvrir la visibilité aux autres éléments. Une clause use (ou use type) peut figurer dans n'importe quelle partie déclarative et n'a d'effet que pour la durée de vie de cette portée.
Notons tout d'abord que le débat n'est pas tant celui de la clause use que celui de savoir si l'on doit utiliser des noms complets (préfixés par le nom du paquetage) ou des noms simples. En effet, rien n'empêche d'utiliser les noms complets même en présence d'une clause use, tout comme il est possible (par surnommage) d'utiliser des noms simples sans clause use. Disons simplement que si un projet choisit d'imposer des noms complets partout, alors il doit interdire les clauses use car cela permet de faire vérifier par le compilateur la bonne application de la règle.
* Les raisons d'être de la clause «use»
* Quelques bonnes raisons d'utiliser les noms complets
* Quelques bonnes raisons d'utiliser les noms simples
* Critères de choix

## Choix des types

La définition des types est une des étapes les plus importantes de l'écriture d'une application en Ada. C'est la rigueur du typage qui permet d'assurer le maximum de contrôle par le compilateur; mais inversement, le programmeur (nouvellement) converti à Ada tend à définir trop de types, provoquant ainsi une multiplication des conversions nuisant en définitive à la lisibilité.
Prenons un exemple typique: soit une procédure Aller_en, destinée à positionner le curseur en un point de l'écran. Cette procédure admet à l'évidence deux paramètres, correspondant aux coordonnées. Une première solution consiste à définir des types différents pour les abscisses et les ordonnées:
Cette solution présente l'avantage de faire vérifier par le compilateur que l'on ne mélange pas les coordonnées. Ainsi si l'on écrit:
le compilateur diagnostiquera que les paramètres ont été inversés dès la première compilation.
L'inconvénient de cette solution est que l'on a souvent à effectuer des calculs mixtes portant à la fois sur des lignes et des colonnes. Par exemple, pour calculer le périmètre d'un rectangle, nous devons évaluer l'expression 2*((Y2-Y1)+(X2-X1)). Quel est le type de ce calcul? Certainement ni une abscisse, ni une ordonnée. Ce ne serait pas homogène du point de vue physique et de plus le résultat peut prendre des valeurs en dehors de l'intervalle défini pour nos coordonnées. Le programmeur se rabat souvent dans de tels cas sur un type prédéfini, comme Integer et la formule devient 2*(Integer(Y2-Y1)+Integer(X2-X1)). Mais rien n'assure que le type Integer convienne et en particulier qu'il comporte suffisamment de valeurs.
La seconde solution consiste à définir un seul type «coordonnées», d'intervalle suffisamment grand pour tenir non seulement les valeurs d'abscisse et d'ordonnée, mais encore les résultats des calculs intermédiaires entre abscisses et ordonnées. Pour améliorer les vérifications, nous définirons Abscisse et Ordonnée comme des sous-types de Coordonnées:
Ici, nous perdons toute vérification à la compilation d'éventuelles inversions entre abscisses et ordonnées, puisque précisément cette solution présente l'avantage de permettre de les mélanger dans les calculs. Il reste cependant certains garde-fous. D'une part, l'utilisation des sous-types nous garantit que l'exception Constraint_Error sera levée si jamais une valeur trop grande (ou négative) était passée à l'une des coordonnées. D'autre part, l'utilisation de la notation nommée rend beaucoup plus improbable un mélange de coordonnées. On voit mal un programmeur écrire (à moins de le faire vraiment exprès):
Notons une fois de plus l'importance du choix de bons identificateurs! Cette sécurité disparaîtrait si nous appelions nos paramètres formels A et B au lieu de X et Y.
La règle la plus simple à appliquer est qu'il faut utiliser des types différents pour représenter des entités réellement différentes. Une prolifération de conversions doit amener à se demander s'il ne serait pas judicieux de fusionner certains types.

### Types discrets

Le terme «type discret» recouvre les types énumératifs et les types entiers, c'est-à-dire les types que l'on utilise chaque fois que l'on souhaite représenter un élément du monde réel caractérisé par un nombre fini de valeurs distinctes. Si l'on souhaite simplement représenter différentes valeurs, les types énumératifs s'imposent: ils sont plus lisibles (leur nom indique à quoi ils correspondent) et plus sûrs, notamment grâce au fait que les instructions case vérifient toujours que toutes les valeurs possibles ont été mentionnées: en cas de rajout d'une valeur au type suite à une modification du logiciel, le compilateur vous préviendra gentiment de tous les endroits où vous avez oublié d'ajuster les case correspondants. Enfin, le fait de devoir énumérer toutes les valeurs oblige à réfléchir dès le départ à l'ensemble des valeurs possibles.
    Ada dispose d'attributs permettant de transformer facilement des énumératifs en chaînes de caractères, ainsi que de sous-programmes d'entrées-sorties qui leur sont applicables. Ceci les rend beaucoup plus utilisables qu'en Pascal.
On réservera les types entiers au cas où l'on souhaite vraiment une sémantique de nombre, c'est-à-dire lorsque la variable est utilisée pour compter ou lorsque l'on a besoin d'effectuer des opérations arithmétiques. Et même alors, on évitera ABSOLUMENT l'utilisation du type Integer, sauf dans quelques cas que nous mentionnerons par la suite. Pourquoi l'avoir écrit si gros? Parce qu'à l'usage, il s'avère que c'est une des habitudes les plus difficiles à faire perdre aux nouveaux programmeurs Ada.
    Attention: le type Integer peut parfois réapparaître de façon insidieuse. Si par exemple l'on écrit: for I in 1..10 loop ... le compilateur ne dispose d'aucune information sur le type de I et choisit donc Integer, faute de mieux. Il est toujours possible (et préférable) de donner explicitement le type: for I in Age range 1..10 loop ... Ceci s'applique également aux indices des types tableau. Pour une fois qu'Ada avait tenté d'économiser quelques touches à taper au programmeur, l'inconvénient se révèle supérieur au gain...
Le choix d'un type de données s'effectue en étudiant le domaine de problème; c'est vrai des types entiers comme des autres. En particulier, on doit toujours penser qu'un type entier est limité par la machine et que l'utilisation d'un type prédéfini ne dispense pas d'étudier le problème des bornes ; ceci s'applique à tous les langages, mais seul Ada fournit le moyen de choisir les types de façon indépendante de la machine. On distingue plusieurs formes caractéristiques de déclarations de types en fonction des contraintes:
* Le modèle impose des contraintes absolues. Celles-ci sont directement reproduites dans la déclaration:type Jour_de_l_Année is range 1..366;
* Le modèle n'impose que des contraintes minimales; le programmeur souhaite une implémentation matérielle efficace couvrant au moins les limites données:
    Le type de base (obtenu par l'attribut 'Base) est le type machine sous-jacent choisi par le compilateur. Il est donc plus vaste que le type d'origine. Cette utilisation de l'attribut 'Base est nouvelle en Ada 95.
* Le programmeur souhaite imposer des limites liées au matériel, par exemple pour des interfaçages. On définira le type en conséquence et on l'assortira d'une clause de représentation:
    En Ada 95, le paquetage Interfaces contient en standard la définition de tous les types machines supportés par le compilateur, sous la forme Integer_8, Unsigned_16, etc. Le programmeur n'a donc plus à les récrire.
* Le programmeur a besoin d'un type entier, non lié à une sémantique particulière, sans contrainte d'efficacité... En fait, si c'était possible, il lui faudrait l'ensemble (infini) des entiers relatifs, au sens mathématique. Il se contentera du plus grand type possible:type Grand_Entier is range System.Min_Int..System.Max_Int;
    Le paquetage System est présent sur toutes les implémentations et contient des déclarations de constantes permettant de connaître les caractéristiques de la machine sur laquelle on s'exécute. Par exemple, Min_Int et Max_Int sont la plus petite et la plus grande valeur entière supportées par l'implémentation.
De plus, on a le choix entre les types signés, comme dans les déclarations ci-dessus et les types modulaires. On préférera les premiers en général, car ils offrent plus de sécurité (notamment en cas de débordement). On utilisera les types modulaires lorsque l'on a effectivement besoin d'une arithmétique circulaire, ou pour un type de très bas niveau, car ils permettent des manipulations supplémentaires au niveau du bit. Enfin il existe quelques cas où l'on doit utiliser Integer:
* Pour indexer le type String. Ce type a, par définition, Integer (ou plutôt son sous-type Positive) comme type d'index. Toutes les variables servant à manipuler les chaînes seront donc de type Integer.
* Lorsque le besoin est celui d'un type de taille raisonnable, adapté aux possibilités de la machine; en particulier comme indice de tableau ou comme limite lorsque l'on n'a rien de mieux sous la main. Integer est censé être le type entier «naturel» de la machine et il doit permettre une indexation efficace. On préférera l'utiliser sous forme de type dérivé, pour éviter les mélanges:

### Types réels

De même que l'on n'utilise pas directement le type Integer en Ada, on ne saurait utiliser Float pour tous les calculs «réels». Ada offre toute une palette de types numériques et il faut choisir le plus approprié au problème à résoudre. Voici quelques grandes lignes pour orienter ce choix:
* Décider d'abord si les grandeurs se représentent mieux sur une échelle linéaire (types point fixe) ou logarithmique (types point flottant). Noter que pour un même nombre de bits, la précision est meilleure avec les points flottants au voisinage de 0, mais qu'au contraire elle est meilleure pour les grandes valeurs avec les points fixes . En particulier, toute représentation du temps (pour lequel la notion de zéro est arbitraire) se définit normalement avec des points fixes.
* Dans le cas d'une échelle linéaire, décider si l'on a un modèle de calculs approchés, ou si le besoin est celui d'une arithmétique exacte (comme avec des entiers), mais avec un pas différent de 1. Dans le premier cas, on utilisera des points fixes normaux:type Volts is delta 0.01 range 0.0 .. 380.0; Dans le second, si le pas souhaité est une puissance de 10, on utilisera des décimaux:type Francs is delta 0.01 digits 10; Autrement, on mettra une clause de représentation pour garantir le bon pas:
* Dans le cas d'une échelle logarithmique, on peut souhaiter une précision qui soit fonction des capacités de la machine ou au contraire une précision indépendante de la machine. Par exemple, si l'on fait un programme graphique, la précision des calculs n'a pas grande importance et l'on souhaite utiliser les flottants normaux de la machine. Bien sûr, on préservera l'abstraction en n'utilisant pas directement le type Float, mais un type dérivé:
    Noter qu'il est alors possible de connaître (de façon portable) la précision a posteriori des calculs grâce aux attributs tels que Coordonnée'Digits.
Dans d'autres cas, la précision minimale est obligatoire. Si l'on calcule par exemple l'angle de rentrée d'une navette dans l'atmosphère, la précision demandée est vitale: trop à plat, la navette rebondira sur les couches hautes, trop inclinée elle brûlera. Une analyse fine peut conduire à la conclusion que pour obtenir une précision de 10$^{-3}$ sur le résultat, les données doivent être représentées avec une précision de 10$^{-7}$. On déclarera donc:
    Noter que selon la représentation interne de la machine, cette précision peut tenir ou non sur 32 bits; le compilateur choisira donc une représentation interne en simple ou double précision, selon la machine. On aura peut-être plus de précision (ce qui ne sert à rien puisqu'on pilote des vérins dont la précision est limitée), mais la précision minimale est garantie.
On voit qu'Ada offre toute une palette de possibilités dans le choix des types numériques; on évitera de se précipiter sur le type Float comme on le ferait dans d'autres langages.
* ↑ C'est pour n'avoir pas suivi ce conseil que le programme (écrit en C !) qui contrôlait le départ des missiles Patriot pendant la guerre du Golfe a raté un Skud qui causa la mort de plus d'une dizaine de personnes.

### Chaînes de caractères

La notion de chaîne de caractères est moins évidente qu'il n'y paraît. Là encore, une analyse fine des besoins amène à reconnaître plusieurs modes d'utilisation des chaînes de caractères, avec des contraintes différentes. C'est pourquoi Ada offre plusieurs modèles pour répondre aux différentes utilisations; noter que les différents paquetages correspondant à ces possibilités offrent des sous-programmes de manipulation identiques (grâce à la surcharge) et que l'utilisateur n'a donc à apprendre qu'un seul mode d'emploi. Voici ces différents modèles.
* Les messages. Il s'agit de chaînes destinées à dialoguer avec l'utilisateur et qui ne sont donc pas des représentations d'entités de plus haut niveau. Le type prédéfini String est parfaitement approprié. Le paquetage Ada.Strings.Fixed fournit toutes les fonctionnalités habituelles: déplacement, recopie, transcodage, recherche, etc.
* Des types de données abstraits, que l'on choisit de représenter au moyen de chaînes de caractères. Si les propriétés du type abstrait conduisent à la conclusion que toutes les chaînes ont la même longueur, on utilise simplement un sous-type de String. Si au contraire on a besoin de chaînes de longueur variable, on instancie le paquetage Generic_Bounded_Length (qui est fourni dans le paquetage Ada.Strings.Bounded). Ce paquetage fournit un type de chaîne de caractère de longueur variable, dont la taille maximale est donnée à l'instanciation. Bien entendu, la taille maximale choisie doit résulter d'une analyse des propriétés du type de donnée abstrait. Par exemple, on peut choisir de représenter le nom d'une personne par une chaîne variable et décider que la longueur maximale sera de 15 caractères:
Nous avons fourni le sous-type Nom pour exporter un identificateur significatif pour l'utilisateur. Ceci ne rend pas les opérations définies dans le paquetage directement visibles; mais ces opérations ne sont utiles qu'aux personnes effectuant des recherches, modifications, etc., sur des noms. En appelant l'instanciation Operations_Nom, cela permet à ces utilisateurs de marquer les endroits où ils travaillent effectivement sur des noms grâce à une clause use Opérations_Nom.
* Le stockage de caractères. Le besoin n'est pas à proprement parler celui d'une «chaîne», mais plutôt d'un tableau de caractères que le programme manipule. En général, la taille de ce tableau est virtuellement infinie; un exemple typique est celui d'un programme de traitement de texte qui stocke tout le document dans un seul grand tableau en mémoire. On utilise alors le type Unbounded_String (ou plutôt un type dérivé), du paquetage Ada.Strings.Unbounded:type Document is new Unbounded_String;
    Attention: la limite des Unbounded_String (il en faut bien une) est donnée par Natural'Last, c'est-à-dire 2_147_483_647 pour une implémentation de Integer sur 32 bits (ce qui devrait suffire amplement), mais seulement 32_767 pour une implémentation 16 bits, ce qui peut s'avérer insuffisant. On risque donc d'avoir une non-portabilité à ce niveau.
Enfin, il se peut que d'autres besoins conduisent à la définition d'un paquetage «maison» de chaînes de caractères, avec des caractéristiques différentes. Au nom de l'uniformité et de la diminution de la complexité, on veillera à fournir les mêmes fonctionnalités, avec les mêmes noms, que dans les paquetages faisant partie de l'environnement standard.

### Types articles

On utilisera des types articles chaque fois qu'une entité est composée de plusieurs autres éléments. Le principal choix (non évident) est de décider si l'on doit utiliser un type étiqueté ou un type article à discriminants lorsque l'on veut rassembler dans un même type (ou famille de types) des valeurs polymorphes, c'est-à-dire ne comportant pas toutes les mêmes éléments.
Dans un type à discriminant, les différentes variantes sont définies statiquement lors de la déclaration initiale du type; l'ajout par la suite de structures non prévues au départ nécessite des changements importants dans l'ensemble du logiciel. En revanche, ce même aspect statique garantit un maximum de contrôle. Il n'est pas possible d'effectuer des modifications qui remettraient en cause la validité d'une partie de logiciel déjà validée. Enfin, il est possible (si le type l'a autorisé) de déclarer des variables non contraintes, qui peuvent donc contenir différentes variantes au cours du temps.
Les type étiquetés offrent un maximum de souplesse; ils peuvent être enrichis après coup sans toucher au type d'origine. En revanche, ces modifications peuvent affecter (via le mécanisme de liaison dynamique) le comportement des modules déjà écrits. La liaison dynamique, de par son principe même, peut empêcher toute certification exhaustive des modules. Enfin, les objets se voient affecter un type effectif au moment de leur élaboration et ne peuvent en changer par la suite.
Conceptuellement, on préférera les types à discriminants si les différentes variantes constituent de simples paramètres, ou des options, d'un seul type. En particulier, si ces options sont susceptibles d'évoluer au cours du temps, il faudra utiliser des variables non contraintes qui ne sont possibles qu'avec les types à discriminants. On préférera les types étiquetés si l'on pense avoir affaire à des types différents, mais appartenant à une même famille dont on veut exprimer dans le langage la nature commune. Ces principes peuvent être pondérés par des considérations pragmatiques: là où la validation et la sécurité sont prédominants, les types à discriminants sont préférables. Inversement, pour des logiciels à moindres contraintes sécuritaires, mais dont on prévoit une évolution plus importante dans le temps, les types étiquetés apportent plus de souplesse.
Enfin, ces deux options ne sont pas exclusives: il est possible d'avoir des types étiquetés à discriminants. Il sera rare de les utiliser pour des parties variables, mais il est tout à fait raisonnable de définir des types étiquetés dont certains éléments, des tables par exemple, ont une taille paramétrable au moyen de discriminants.

### Pointeurs

Les pointeurs sont un outil indispensable dans certains cas, mais l'expérience montre qu'ils sont une cause importante de difficultés. Celles-ci peuvent être causées d'une part par le fait de travailler sur des références, plus difficiles à appréhender par le programmeur que les variables directes (le programmeur doit gérer dans sa tête le niveau d'indirection supplémentaire); d'autre part, le mécanisme d'allocation et surtout de désallocation dynamique est difficile à gérer: démontrer l'absence de «fuites de mémoire» (variables allouées et jamais désallouées) est techniquement si ardu que de nombreux systèmes temps réel devant fonctionner en permanence préfèrent interdire l'utilisation de toute allocation dynamique. L'expérience nous a amené à formuler la «loi» suivante:
    La probabilité de «bugs» dans un logiciel est directement proportionnelle à la densité des pointeurs.
On utilise moins les pointeurs en Ada que dans d'autres langages, car on dispose d'autres moyens pour réaliser certaines structures. Notons en particulier que:
* Il est possible d'avoir des structures de données, notamment des tableaux, dont la taille n'est connue qu'à l'exécution sans utiliser pour autant l'allocation dynamique.
* Le mécanisme de la programmation orientée objet n'est pas lié à la notion de pointeur.
* Les génériques offrent une alternative à l'utilisation de pointeurs pour fournir à un sous-programme ou à un paquetage des références à des éléments qui leur sont extérieurs (sous-programmes, variables) .
Inversement, un cas exige des pointeurs et de l'allocation dynamique: la construction de structures de données liées et dynamiques: arbres, listes, etc., et plus généralement lorsque l'on souhaite définir un type de donnée abstrait à sémantique de référence (rappelons cependant que ce n'est pas la seule solution possible).
On utilise aussi parfois des pointeurs pour éviter de copier des données volumineuses: imaginons une table de symboles par exemple. Il serait très pénalisant de renvoyer toute l'information associée à un symbole, surtout si l'utilisateur n'a besoin que d'une information spécifique. Il est donc préférable de renvoyer une référence sur l'entrée dans la table. Dans d'autres langages, ceci a une conséquence fâcheuse: l'utilisateur peut modifier le contenu de la table en utilisant directement la référence. C'est pourquoi Ada fournit des pointeurs sur constantes: de tels pointeurs peuvent désigner aussi bien des variables que des constantes, mais ne peuvent être utilisés pour modifier l'objet désigné. Une table des symboles pourrait ainsi s'écrire:
Côté utilisateur on aura:
On peut donc garantir l'intégrité de la table, tout en renvoyant des références sur les structures internes. Notons que certaines formes apparentées à des pointeurs (paramètres accès, autopointeurs, attribut 'Access) permettent d'établir des références et de créer des entités logiquement reliées sans nécessiter d'allocation dynamique; les règles du langage garantissent l'impossibilité de conserver des pointeurs sur des entités ayant disparu.
On limitera donc l'utilisation des pointeurs et plus spécialement de l'allocation dynamique, aux seuls cas où elle est réellement indispensable. On fera usage si possible des autres possibilités et on prendra garde de ne pas se laisser entraîner à mettre des pointeurs partout par mimétisme avec des langages ne disposant pas de structures de données aussi puissantes que celles d'Ada.

### Utilisation de Unchecked_Conversion

Ayant abondamment vanté les mérites du typage fort, on peut se demander pourquoi Ada autorise le détypage au moyen de Unchecked_Conversion. En fait, il s'agit d'une fonctionnalité indispensable lors de changements de niveaux d'abstraction, qui imposent de voir la même donnée selon deux vues différentes. Prenons un exemple.
Un nombre flottant est considéré, dans la plupart des applications, comme une entité autonome. Cependant, certains algorithmes numériques fins nécessitent d'accéder séparément à la mantisse ou à l'exposant du nombre. Il convient donc de fournir deux vues du même type: soit comme un tout, soit comme une entité composite comprenant signe, mantisse et exposant. Nous pouvons fournir ce service de la façon suivante:
Remarquez le processus: on décrit la vue que l'on veut obtenir (le type article) et sa représentation interne (les clauses de représentation). Ensuite, Unchecked_Conversion nous permet de voir la même donnée selon l'une ou l'autre vue. À partir de là, la fonction Exposant n'a plus qu'à retourner la partie exposant d'un flottant considéré comme un article. Noter au passage l'aspect descriptif de cette façon de faire: c'est le compilateur qui devra se débrouiller pour gérer les masques et les décalages nécessaires à extraire l'information... ce qui n'est pas un petit service rendu au programmeur qui n'a plus à se soucier de cette «cuisine».
Notons enfin qu'il existait un cas fréquent d'utilisation de Unchecked_Conversion qui a disparu avec Ada 95: celui où l'on devait voir des données de haut niveau comme des suites d'octets, pour stockage sur disque ou envoi sur un réseau par exemple. Les nouveaux attributs 'Read et 'Write fournissent désormais cette conversion de vue; citons également l'attribut 'Valid qui permet de vérifier qu'une valeur en provenance de l'extérieur (entrée-sortie, autre langage) est bien conforme aux exigences du typage Ada, ainsi que le paquetage Ada.Storage_IO qui effectue des pseudo-entrées-sorties vers des tableaux d'octets.
* ↑ Les numériciens ont tellement besoin de ces fonctionnalités de manière portable (ce qui n'est pas le cas de notre solution) qu'Ada 95 offre de nouveaux attributs fournissant directement ces services.

### Conclusion sur l'utilisation des types

Choisir le bon type pour représenter une entité n'est pas chose aisée: entre le désir de modéliser de façon précise les entités du monde réel, la nécessité d'éviter une prolifération abusive des types, les contraintes d'évolutivité et d'efficacité, les solutions sont nombreuses et il faut rechercher le meilleur compromis. Il est donc important de prendre le temps de réfléchir soigneusement avant de définir un type. Eventuellement, l'utilisation du paquetage ADPT et des types associés permet de poursuivre le projet en différant la décision jusqu'à un moment où le problème et les contraintes éventuelles seront mieux connus. Il ne faudrait cependant pas en déduire qu'Ada crée de nouvelles difficultés; bien sûr, dans les langages où le seul type entier est Integer, le problème du choix d'un type entier ne se pose pas! Il n'empêche que le problème de la définition du type le plus approprié existe toujours. Simplement, ces autres langages n'offrent aucun outil pour le résoudre et il reste entièrement à la charge du programmeur.

## Choix liés aux génériques

Faut-il ou non utiliser des génériques? Encore un point où l'on se pose des questions avec Ada que l'on ne se posait pas avant... puisque la fonctionnalité n'existait pas. Et pourtant, les génériques sont bien utiles, puisqu'ils ont été adoptés (sous des formes plus ou moins différentes) par des langages comme Eiffel et C++. Les points à considérer sont d'ordre économique, méthodologique et de performance.

### Point de vue économique

Toutes choses égales d'ailleurs, le développement d'un générique prend plus de temps et d'effort que son équivalent non générique... tant qu'il n'y en a qu'un. Ce surcoût est rapidement amorti dès que le générique est utilisé plusieurs fois. Inversement, il est moins coûteux d'écrire directement un générique que de transformer par la suite une unité qui n'a pas été prévue pour. Voici quelques règles empiriques pour guider ce choix:
* Un module dont on n'a aucune raison de penser qu'il puisse se généraliser n'est pas mis sous forme de générique.
* Un module qui a toutes les chances d'être généralisé est écrit directement sous forme de générique.
* Un module dont on ne pense pas qu'il puisse être généralisé, au moins à court terme, est écrit sous forme non générique, mais on prendra des précautions pour faciliter un éventuel passage ultérieur en générique.
* Il vaut mieux adapter un module déjà développé en le passant en générique que de récrire entièrement une entité voisine, surtout si l'on est tenté de faire des duplications à partir de l'existant.
* ↑ Sauf dans le cas d'une première version développée sous forme non générique pour faciliter la mise au point, mais dont la structure a été pensée dès le départ pour devenir générique par la suite.

### Point de vue méthodologique

* Héritage et généricité
* Généricité et pointeurs sur sous-programmes

### Point de vue des performances

On a souvent peur de la duplication de code engendrée par l'utilisation des génériques; mais si l'on écrit explicitement deux fois la même chose, il y a certainement duplication, alors que certains compilateurs sont capables de partager du code entre instanciations. Autrement dit, ce n'est pas l'utilisation du générique qui augmente la taille du code, c'est le fait d'avoir deux modules voisins et l'utilisation d'un générique ne peut être pire (mais peut être plus économique) que d'écrire deux fois le module. De plus, le risque de duplication peut être considérablement réduit par une découpe judicieuse de la structure. Retenons une règle générale:
    On ne doit mettre en générique que les parties qui dépendent spécifiquement des paramètres génériques.
En particulier, si des sous-programmes sont nécessaires pour la réalisation de l'unité générique, mais qu'ils ne dépendent pas eux-mêmes des paramètres génériques, on aura intérêt à les regrouper dans des paquetages (non génériques) externes. Le générique se trouvera être en fait le point d'entrée d'un petit sous-système. Dans certains cas, on peut aboutir à une structure où la partie générique n'est plus qu'une «peau», généralement destinée à préserver le typage Ada. Supposons par exemple que nous voulions faire une interface de boîte aux lettres. Nous voulons bien sûr respecter le typage Ada, nous fournissons donc une interface:
En fait le traitement est totalement indépendant du type de données, puisque pour les besoins de la boîte aux lettres, l'élément à envoyer n'est qu'une suite d'octets... Nous définissons donc le paquetage (non générique) suivant, qui fera l'essentiel du travail:
Le corps générique va utiliser le paquetage Ada.Storage_IO pour convertir le type de haut niveau en une suite d'octets, puis appeler les fonctions de Messagerie_Bas_Niveau:
Pourquoi ne pas mettre directement Messagerie_Bas_Niveau à la disposition de l'utilisateur? Précisément parce qu'à ce niveau, toute vérification de type a disparu. Si c'est nécessaire pour l'implémentation, il est toujours préférable de le cacher à l'utilisateur.
Noter qu'en Ada 83, il n'existait pas de moyen de cacher physiquement le paquetage Messagerie_Bas_Niveau et que l'on n'avait donc pas de garantie que l'utilisateur n'y avait pas accès, à moins de tout rassembler en un seul paquetage. En Ada 95, le mécanisme de bibliothèques hiérarchiques nous permet ce contrôle tout en bénéficiant de compilations séparées. Une meilleure organisation est donc la suivante:
Le paquetage Boîtes_Aux_Lettres ne sert plus que d'enveloppe; Une_Boîte est un enfant public, utilisable par tout le monde, mais Messagerie_Bas_Niveau est un enfant privé, appelable uniquement depuis Une_Boîte. Les propriétés de fermeture du sous-système sont donc garanties. Le paquetage général Boîtes_Aux_Lettres est quand même nécessaire, car un enfant de générique ne peut être que générique; Messagerie_Bas_Niveau ne pourrait donc être un enfant (même privé) de Une_Boîte.
En termes de vitesse d'exécution, l'utilisation de génériques ne devrait pas provoquer de pénalité, sauf dans le cas de génériques partagés; mais c'est alors un effet de la règle habituelle que les gains en espace mémoire se paient en temps d'exécution. Certains compilateurs disposent d'ailleurs d'options permettant à l'utilisateur de choisir entre partage et duplication.
* ↑ Se méfier en particulier de la notion de «macros» présente dans d'autres langages, qui conduit à des duplications de code que l'on a tendance à oublier.
* ↑ Ce conditionnel est là pour rappeler au lecteur que toute considération d'efficacité doit faire l'objet d'une mesure.

## Choix liés au parallélisme

Lorsque l'analyse du système conduit à la définition de processus parallèles, on retrouvera naturellement des tâches correspondantes au niveau de l'implémentation. Ceci dit, l'usage des tâches n'est pas limité à ces tâches structurelles: elles peuvent se révéler un excellent moyen de structuration. Inversement, l'enthousiasme né de la disponibilité de cet outil qui n'existe que dans peu d'autres langages peut conduire à des abus: il faut donc définir les conditions où l'usage de tâches peut être bénéfique.

### Principes généraux

Le modèle Ada des tâches en fait fondamentalement des serveurs, fournissant par leurs entrées des services à leurs clients. En général, l'état normal d'un serveur sera l'état bloqué, en attente de demande de service; lors de la réception d'une demande, le serveur deviendra actif, provoquant au besoin le réveil d'autres serveurs, puis au bout d'un certain temps l'ensemble tendra à retourner vers l'état bloqué. On peut donc voir l'ensemble des tâches comme une suite d'engrenages, qu'un mouvement de balancier vient mettre en mouvement, puis qui retourne à son état stable... jusqu'au prochain évènement.
De telles tâches sont généralement peu coûteuses pour le système: une tâche à l'état bloqué ne prend aucune puissance de calcul. En revanche, elle occupe un certain espace mémoire, au moins l'espace de sa pile.
    L'utilisateur peut spécifier au moyen du pragma Storage_Size l'espace mémoire d'une tâche. Ce pragma, comme l'attribut d'Ada 83 qu'il remplace, peut être dynamique: on peut donc ajuster à l'exécution l'espace des tâches.
Inversement, les tâches actives, c'est-à-dire celles qui «tournent» sans se bloquer, consomment de la puissance de calcul inutilement. On en a besoin par exemple pour faire des scrutations (polling) de périphériques qui ne disposent pas de possibilités d'interruptions: le seul moyen de savoir qu'une donnée est arrivée est alors d'aller les interroger périodiquement. On veillera cependant à insérer un delay (même faible) dans la boucle pour éviter que la tâche n'accapare l'unité centrale sur un monoprocesseur.
On pourra enfin avoir une, et au plus une, tâche en boucle réellement continue; ce sera par exemple la boucle principale d'un exécutif périodique. On peut en autoriser une dans un système temps réel, car il faut bien «occuper l'UC» quand le système ne traite aucun événement; on ne peut en autoriser qu'une, car en l'absence de suspension (ou de partage de temps) sur un monoprocesseur, une autre tâche en boucle permanente ne prendrait jamais la main.
Les tâches constituent un outil très commode et, sauf contrainte particulière, il ne faut pas chercher à les éviter délibérément. Inversement, certains ont parfois tendance à en mettre partout. Les remarques que nous avons faites sur le minimum de complexité s'appliquent à la décomposition en tâches: un petit nombre de tâches fait décroître la complexité, un trop grand nombre fait perdre ce bénéfice à cause de la multiplication des communications. Il importe donc de choisir une granularité raisonnable, entre les deux extrêmes que sont la tâche unique qui fait tout et une tâche par service. En particulier, lorsque plusieurs services sont logiquement reliés, il est préférable de les faire traiter par une seule tâche, même si cela provoque de petites sérialisations.

### Tâches comme unités de structuration

On peut être parfois amené à utiliser des tâches comme unités de structuration, indépendamment de tout aspect «parallélisme». En fait, il est assez naturel d'utiliser des tâches (au sens Ada) pour modéliser des «tâches à accomplir» qui n'ont pas de rapports dans le temps. Imaginons par exemple que nous devions remplir deux formulaires A et B. Ceux-ci comportent des calculs et il faut parfois reporter des calculs intermédiaires d'une feuille sur l'autre pour pouvoir poursuivre (Figure 36). Il est bien sûr possible d'écrire un programme séquentiel effectuant les calculs; mais il faudra déterminer soigneusement dans quel ordre s'occuper par moments des calculs de A et par moments des calculs de B. Il est de plus impossible d'obtenir une structure «objet», avec des modules distincts pour représenter chacune des feuilles.
C'est possible en revanche si l'on représente chacune des feuilles par une tâche qui effectue les calculs nécessaires, qui envoie à l'autre les résultats intermédiaires et qui se met en attente lorsqu'elle a besoin de données en provenance de l'autre feuille. Le programmeur n'a plus à se soucier de l'ordre exact des calculs: il résultera de l'interaction entre les tâches. On voit qu'ici, l'utilisation des tâches apporte des améliorations d'ordre structurel (un objet du programme par objet du monde réel) et fonctionnel (plus besoin de se préoccuper des dépendances temporelles). Le parallélisme ne joue aucun rôle.

### Synchronisations et communications: rendez-vous ou objets protégés?

En Ada 83, le rendez-vous était le seul moyen de synchronisation et de communication. Ada 95 a rajouté à celui-ci les objets protégés. Il ne faudrait surtout pas croire que ceux-ci remplacent les rendez-vous: ils comblent plutôt certains besoins.
Le rendez-vous est un mécanisme de haut niveau. Facile à comprendre, il modélise bien les interactions entre objets du monde réel. C'est cependant un mécanisme riche; de nombreux besoins réclament un mécanisme plus fruste. Par exemple, il est facile d'utiliser une tâche et des rendez-vous pour contrôler l'accès à une variable partagée par plusieurs tâches. C'est cependant un mécanisme lourd par rapport à la nature du problème et qui peut conduire à une multiplication abusive des tâches s'il y a beaucoup de variables partagées. De même, pour réaliser par exemple des objets gardés, il est nécessaire de fournir des verrous de bas niveau, de type «sémaphores». Bien qu'il soit possible de les réaliser au moyen de tâches, cela conduit à des inversions d'abstractions: implémentation d'outils de bas niveau au moyen d'outils de plus haut niveau. Les objets protégés ont donc été rajoutés pour satisfaire ce besoin: fournir un moyen de synchronisation de bas niveau, très efficace, mais aux possibilités restreintes. Bien que syntaxiquement rien ne l'y oblige, il est clair que le corps d'une opération protégée ne doit pas faire plus en pratique que changer l'état de quelques variables.
Si les inversions d'abstraction sont fâcheuses, il ne faudrait pas pour autant tomber dans le travers inverse: tout réaliser au moyen d'outils élémentaires, alors que l'on dispose d'outils de plus haut niveau. On réservera donc l'emploi des objets protégés aux mécanismes simples, rapides et de bas niveau: protection de variable, barrière, sémaphore... Il s'agira en général d'abstractions d'objets informatiques. On utilisera plutôt les rendez-vous pour les communications de haut niveau entre tâches appartenant au domaine de problème.

## Choix liés aux situations exceptionnelles

Au niveau de la politique de projet, nous avons décidé d'une politique de gestion des erreurs. Mais toutes les situations exceptionnelles ne sont pas nécessairement des erreurs. Il convient donc de décider individuellement, pour chaque sous-programme, quoi faire dans l'éventualité d'une situation exceptionnelle ne permettant pas de fournir le service demandé. Les différents points à considérer sont les suivants [Goo88]:
* Y a-t-il des cas exceptionnels? Comme nous l'avons vu à propos des composants logiciels, il est souvent possible (mais pas toujours souhaitable) de compléter la sémantique au lieu de lever une exception. Par exemple, si nous avons une fonction de recherche d'une sous-chaîne dans une chaîne, il est souvent plus simple, même pour l'utilisateur, de renvoyer une valeur conventionnelle lorsque la chaîne recherchée n'est pas présente, plutôt que de lever une exception.
* En cas de condition exceptionnelle, faut-il lever une exception, ou utiliser le mécanisme défini par le projet pour gérer les erreurs? Tout se joue ici sur la distinction entre «erreur» et «exception», ainsi qu'entre composant réutilisable et module spécifique du projet.
* Faut-il fournir une fonction d'interrogation pour savoir si l'exécution du sous-programme est possible? Il faut pour cela qu'une telle fonction ait une utilité et que sa complexité soit faible devant celle de la fonctionnalité testée. Il est par exemple utile de fournir une fonction testant la fin de fichier avant lecture; mais une fonction qui fournirait un prédicat pour savoir si une matrice est inversible serait peu utile et presque aussi complexe que l'inversion de matrice elle-même.
* Faut-il fournir une fonction d'interrogation permettant, en cas d'anomalie, d'obtenir plus de détails sur la cause? Une telle fonction peut être très utile, mais ne s'applique qu'aux anomalies «sophistiquées»; de plus, il faut prendre des précautions pour assurer la réentrance en cas d'utilisation du parallélisme.
    Ada 95 offre des outils permettant d'avoir plus d'information sur la cause d'une exception, comme d'attacher un message spécifique à la levée d'une exception. De plus, les attributs de tâches permettent de stocker des valeurs dans le contexte de chaque tâche et donc d'écrire des fonctions d'interrogations réentrantes.
* ↑ Noter que l'habitude est de renvoyer 0, mais il serait souvent plus commode pour les algorithmes de renvoyer une valeur supérieure à l'index du dernier élément.

## Conclusion

Tout au long de cette quatrième partie, nous avons insisté sur l'importance de faire des choix rationnels à tous les niveaux du développement. Bien que ceci puisse paraître contraignant, nous terminerons en rappelant que cette démarche est avant tout rentable. Ne rien laisser au hasard conduit à des logiciels mieux construits, plus portables, plus évolutifs, plus sûrs et en un mot de meilleure qualité. Le génie logiciel est avant tout un état d'esprit; il nécessite des méthodes, des outils de haut niveau – en particulier un langage adapté – mais la clé permettant le développement de logiciels toujours plus complexes se situe avant tout dans la tête des développeurs

## Exercices

* Écrire un algorithme d'intégration d'une fonction entre deux bornes. En faire ensuite un générique auquel on passe la fonction en formel, puis une procédure à laquelle on passe un pointeur sur la fonction. Comparer les performances.
* Étudier la structure d'un mécanisme de fenêtrage qui serait fondé sur le parallélisme et non sur le mécanisme des callbacks.