# Méthodes de génie logiciel avec Ada/Version imprimable

* Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
* Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
* Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.
Une version à jour et éditable de ce livre est disponible sur Wikilivres, une bibliothèque de livres pédagogiques, à l'URL : https://fr.wikibooks.org/wiki/M%C3%A9thodes_de_g%C3%A9nie_logiciel_avec_Ada

# Avant propos

* 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 ]

# Avant propos à la version WikiLivres

Sorti en juin 1995 dans le sillon de la norme Ada 95, le livre Méthodes de génie logiciel avec Ada 95 écrit par Jean-Pierre Rosen, était épuisé. Pourquoi n'était-il pas réédité malgré de nombreuses demandes ? De l'aveu même de son auteur le livre était à mettre au goût du jour des derniers langages (Java…) et méthodologies (UML…) : « Si de nombreuses personnes m'ont amicalement poussé à mettre à jour cet ouvrage, il ne semble pas que le marché potentiel soit de nature à intéresser un éditeur. Alors autant le mettre à disposition de tout le monde. » (voir texte complet dans la préface ci-après).
L'idée s'est fait jour de le placer sur Internet en mode collaboratif pour qu'il soit enrichi.
Initialement publié en 2004 sous Licence de Libre Diffusion des Documents (LLDD version 1), Jean-Pierre Rosen a donné son autorisation pour une publication sur WikiLivres sous Gnu Free Documentation Licence (GFDL).

# Préface à la deuxième édition

Lorsque vers 2004 il me prit l'envie de relire la première édition de cet ouvrage, parue en 1995, je fus frappé de ce qu'il était à la fois obsolète et toujours d'actualité. Obsolète, car de nouveaux langages, de nouvelles méthodes étaient apparus : on ne comprendrait pas aujourd'hui qu'un livre traitant des rapports entre langages de programmation et méthodes ne traite ni de Java, ni d'UML. Mais toujours d'actualité, car si les modes changent, les problèmes restent les mêmes. Les nouveaux outils permettent-ils vraiment de développer mieux qu'il y a dix ans ? On est en droit d'en douter.
C'est pourquoi je décidai à l'époque de remettre à jour ce livre, en conservant l'essentiel de la version précédente, mais avec des modifications portant sur les langages et méthodes apparus postérieurement à l'édition originale, et en supprimant des méthodes qui avaient disparu, englouties dans la vague UML. Hélas, les contraintes de la vie professionnelle ne m'ont pas permis de mener ce projet à bout.
Aussi, plutôt que de laisser perdre ce travail, il m'est apparu plus intéressant de diffuser cet ouvrage sous forme de document libre. Si de nombreuses personnes m'avaient amicalement poussé à mettre à jour cet ouvrage, il ne semble pas que le marché potentiel soit de nature à intéresser un éditeur. Alors autant le mettre à disposition de tout le monde.
Le logiciel libre, qui s'est considérablement développé ces dernières années, est un processus fondamentalement coopératif ; chacun peut apporter sa contribution au développement. Le WikiLivre en est l'équivalent pour les publications, et j'espère que les lecteurs voudront bien me faire part de leurs remarques, de leurs critiques et de leur suggestions d'amélioration, ou mieux, apporter directement leurs modifications. Grâce au WikiLivre, cet ouvrage pourra devenir un document vivant, évoluant au gré de l'apparition de nouvelles méthodes et de nouveaux langages. Lecteurs qui bénéficiez du libre accès à ce livre, il n'appartient qu'à vous qu'il soit également un peu le vôtre !

# Introduction

On pourrait définir le génie logiciel comme la conjonction de méthodes, de règles d'organisation et d'outils. Les outils sont nombreux et abondamment vantés par leurs fabricants. De multiples ouvrages décrivent des méthodes, certaines très célèbres, d'autres plus confidentielles. Et pourtant, dans la pratique, il semble que le génie logiciel ne soit pas toujours entré dans les mœurs des informaticiens. D'où provient ce décalage entre théorie et pratique ?
Au-delà des considérations théoriques, le génie logiciel est d'abord un état d'esprit, demandant rigueur, méthode… et modestie, puisque toute la difficulté d'une « belle » conception est de faire en sorte que n'importe qui puisse la reprendre après son concepteur initial. Le but de ce livre est de faire sentir au lecteur l'importance de la démarche « génie logiciel » et de mener une réflexion sur la nature des méthodes, leurs origines, leurs points communs et leurs différences, et leurs rapports aux langages de programmation.
Dans ce cadre, nous avons naturellement adopté le langage Ada comme moyen d'expression. Pourquoi ? Parce que c'est le seul langage qui ait été conçu dès le départ en fonction d'un cahier des charges dicté par les impératifs du génie logiciel. Nous verrons tout au long de ce livre que chacune des fonctionnalités, chacun des contrôles apportés par ce langage trouvent leur origine dans un principe méthodologique. Mieux : l'explication même des mécanismes du langage est souvent un exemple parlant d'un principe de génie logiciel sous-jacent. Cette question du langage de programmation est souvent prétexte à des débats quasi religieux. Soyons clair : la solution aux difficultés du développement logiciel ne saurait provenir seulement d'un langage de programmation, quel qu'il soit. Seuls l'utilisation de méthodes de conception rigoureuses et le développement de composants logiciels réutilisables peuvent maîtriser la complexité. Mais certains en ont un peu hâtivement conclu que le langage n'avait aucune importance, du moment que l'on adoptait une « bonne » méthode. En fait, son rôle est beaucoup plus important qu'on ne le croit généralement. D'une part, ses possibilités influencent, qu'on le veuille ou non, la conception ; d'autre part, un langage adapté doit poursuivre et vérifier la méthode, pour faciliter le passage de la conception au codage et détecter les incohérences possibles. C'est typiquement ce qu'Ada permet, qu'apprécient ses zélateurs et que lui reprochent ses détracteurs : il ne laisse pas passer les fautes de conception et n'autorise pas la programmation indisciplinée.
Ce livre s'adresse donc aux responsables de projet, responsables qualité et développeurs, mais aussi aux enseignants et étudiants qui souhaitent un panorama des principes du génie logiciel, des méthodes qui l'accompagnent et de la façon de les mettre en œuvre jusqu'au niveau du codage. Ceux qui ne connaissent pas Ada y découvriront qu'un langage de programmation peut être un véritable outil de génie logiciel, et nous espérons qu'ils auront alors à cœur de l'essayer, ce qu'ils peuvent faire aisément maintenant qu'il existe un compilateur libre ; quant à ceux qui le connaissent déjà, ils découvriront les nouvelles possibilités d'Ada 95 ainsi que la façon de l'utiliser au mieux dans un contexte de génie logiciel.
Dans la première partie, nous discuterons des rôles respectifs des méthodes de conception et des langages. Nous verrons comment Ada peut apporter des modifications profondes à la façon d'organiser le développement du logiciel, d'autant plus que les nouvelles fonctionnalités apportées par la révision 95 ont permis de combler les dernières lacunes qu'il comportait vis-à-vis de certaines méthodes. La deuxième partie présente les principales variétés de méthodes de conception utilisées aujourd'hui, et le support actif qu'Ada leur apporte. La troisième partie se consacre au problème plus spécifique du développement et de l'utilisation de composants logiciels. La quatrième aborde les problèmes de l'organisation générale du développement et des choix qui interviennent à tous les stades du projet. Enfin la cinquième partie présente un cadre méthodologique général qui met en pratique, à partir d'une méthode traditionnelle, les principes exposés dans le reste du livre. Chaque chapitre est accompagné d'exercices, sujets de réflexion ou mise en pratique de la théorie.
Si une certaine connaissance de la programmation et d'au moins un langage est nécessaire, il n'est nullement besoin de connaître Ada pour lire ce livre. Un préambule présente rapidement le langage ; nous y avons séparé les fonctionnalités de la version 83 de celles de la version 95, pour permettre un accès plus facile à ceux qui, connaissant déjà l'ancien langage, ne sont intéressés que par les nouveautés. Un glossaire et un index aideront également les nouveaux venus à mieux appréhender le livre.
Les exemples de programme, ainsi que les références dans le texte à des éléments du langage (instructions, variables...) sont en caractères courrier; lorsqu'il s'agit de mots clés du langage, ils sont en caractères gras.
Le problème de la langue est toujours présent dans un ouvrage d'informatique, domaine souvent envahi de jargon anglophone. Nous utilisons le français autant que possible, sans aller jusqu'à employer certaines locutions qui, bien que recommandées officiellement, nous ont paru exagérées : disons-le franchement, nous n'avons jamais pu nous habituer à dire « bogue » pour « bug ». Les termes purement Ada ont été repris du lexique qui sert à la traduction, en cours à l'heure où nous écrivons ces lignes, de la norme Ada 95 en français.
Si, à la fin de cet ouvrage, le lecteur est convaincu de l'importance des méthodes de conception et de la démarche générale du génie logiciel ; s'il a pris conscience que le développement informatique est une longue suite de choix demandant d'arbitrer des compromis parmi des exigences contradictoires ; si notamment son choix du langage de programmation résulte d'une volonté consciente de prolonger le cadre méthodologique et non du simple hasard des disponibilités de compilateurs sur sa machine ; s'il décide de mettre en place une politique de réutilisation à l'échelle de son entreprise ; alors nous sommes convaincu que le langage choisi sera souvent Ada, et nous penserons que le but de cet ouvrage aura été atteint.
Je terminerai en remerciant, selon la tradition, ceux dont les avis, conseils, critiques et remarques ont contribué à faire de ce livre ce qu'il est. Je dois tout d'abord, et à de multiples titres, une mention spéciale à Michel Gauthier, de l'université de Limoges ; les nombreuses références à ses publications montrent bien ce que je lui dois. Mes remerciements vont ensuite à mon assistante de tous les jours, Catherine Supper, et à ceux qui ont bien voulu consacrer une partie de leur temps toujours précieux à relire les premières versions de cet ouvrage : Alain Detrie, Michael Feldmann, Jean-Alain Hernandez et Patrice Jano. Je dois également une mention spéciale à tous les membres d'Ada-France : nombre des idées de ce livre proviennent des discussions que nous avons eues lors de nos réunions. Je ne saurais oublier non plus tous mes élèves, depuis mes débuts à l'ENST jusqu'aux participants des séminaires Adalog : ils ne se rendront jamais compte à quel point une question apparemment naïve peut obliger un enseignant à faire progresser sa propre réflexion sur un sujet.
Je tiens enfin à remercier mon portable, un petit Compaq Aero, pour m'avoir permis de travailler à ce livre dans les endroits les plus divers, depuis les fauteuils d'avion (sans oublier, hélas ! les salles d'attente) jusque dans le lit conjugal ; je remercie ma femme de m'avoir encouragé durant tout ce temps, ainsi que d'avoir bien voulu tolérer la présence dudit portable dans ledit lit conjugal ; enfin je remercie mes enfants de m'avoir parfois laissé utiliser mon ordinateur malgré la présence sur son disque dur de jeux dont ils voyaient beaucoup plus clairement l'utilité !
* ↑ Disponible sur http://gnuada.sourceforge.net, ou pour une version n'autorisant que l'écriture de logiciels personnels ou libres, http://libre.adacore.com

# Préambule

* 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 ]
Le langage que nous utilisons dans ce livre est l'Ada d'aujourd'hui, Ada 95. Celui-ci étant relativement récent, nous avons pensé qu'il serait utile de fournir aux anciens utilisateurs des précisions sur les nouveautés apportées par la révision de la norme.
Aussi avons-nous divisé cette présentation en deux parties. La première explique simplement le «noyau de base», minimum nécessaire pour comprendre le reste de ce livre; presque tous ses éléments figuraient déjà dans Ada 83, et les utilisateurs actuels d'Ada pourront la survoler rapidement. La deuxième partie est plus technique et présente avec plus de détails les grandes lignes des nouveautés importantes. Sa lecture n'est pas nécessaire à la compréhension du reste de l'ouvrage, et ceux qui ne connaissent pas déjà le langage pourront la sauter.
Ce préambule ne cherche pas l'exhaustivité; pour une étude complète du langage, nous renvoyons le lecteur à la bibliographie en fin d'ouvrage.

# Origine d'Ada
## Historique

En janvier 1975 le ministère américain de la Défense (DoD) a constitué un comité d'experts, le High Order Language Working Group (HOLWG), avec pour mission de trouver une approche systématique aux problèmes de qualité et de coûts des logiciels militaires. La plus grosse part de ces logiciels, ou tout du moins là où résidaient les plus gros frais de maintenance, était dans le domaine des systèmes temps réel embarqués (embedded), c'est-à-dire des systèmes informatiques intégrés dans un ensemble électromécanique plus vaste : systèmes d'armes, de radar, de commutation...
Développer un langage de programmation universel apparut alors comme une solution à beaucoup de ces problèmes, et le HOLWG produisit une succession de cahiers des charges spécifiant les caractéristiques souhaitables d'un tel langage. Au printemps de 1977 dix-sept organismes répondirent à l'appel d'offres, parmi lesquels quatre furent retenus pour une pré-étude : Softech, Intermetrics, SRI et Cii-Honeywell-Bull. Les propositions furent évaluées de façon anonyme, et en mars 1978, il ne restait plus en lice qu'Intermetrics et Honeywell-Bull, l'équipe française dirigée par Jean Ichbiah remportant l'appel d'offre un an plus tard. Le langage fut baptisé Ada, du nom d'Ada Augusta Byron.
    Ada Augusta Byron, comtesse de Lovelace (1815-1851), était la fille du Lord Byron, le fameux poète anglais. Elle était passionnée de mathématiques, chose scandaleuse pour une femme à son époque et qui lui attira bien des inimitiés. Amie et disciple de Charles Babbage, elle eut l'intuition de la possibilité d'utiliser des suites d'instructions codées d'après le modèle des métiers Jacquard. Elle écrivit des programmes pour calculer les nombres de Bernouilli sur la machine de Babbage, mais elle ne put les exécuter puisque la machine ne fut jamais construite. Ses programmes furent recodés bien plus tard en PL/1 et fonctionnèrent, paraît-il, du premier coup. Elle est donc considérée comme le premier programmeur de l'histoire. (cf. Wikipédia)
Le document proposé en 1979 était en fait beaucoup trop vague pour permettre l'établissement d'un standard rigoureux. Une première version révisée fut produite en juillet 1980, et l'on pensait à l'époque que le langage pourrait être normalisé avant la fin de cette même année. Il faut comprendre que la qualité de ce document était au moins aussi bonne que celle de la plupart des normes des autres langages de programmation. Mais pour Ada, il fallait faire beaucoup mieux ! En effet, le document comportait encore nombre de petites imprécisions, de possibilités d'interprétations divergentes ou de dépendances excessives à l'implémentation. L'exigence de portabilité nécessitait une définition beaucoup plus précise, et il fallut attendre 1983 pour obtenir la standardisation par l'ANSI. La standardisation internationale exigeait que la norme fût déposée simultanément en deux des trois langues officielles de l'ISO qui sont l'anglais, le français et le russe. Pour des raisons qui leur appartiennent, les américains préférèrent financer la traduction française plutôt que russe, et la normalisation internationale fut suspendue à la parution de la norme française. La traduction fut effectuée avec un soin extrême : il fallut attendre 1987 pour obtenir une norme satisfaisante [Afn87] et la standardisation ISO [Iso87]. Ada est également une norme européenne (C.E.N. 28652), allemande (DIN 66268), suédoise (SS 63-6115), tchèque, japonaise...
Il était important de définir rigoureusement le langage ; encore fallait-il que les compilateurs respectent la norme ! Pour cela, le DoD a déposé le nom «Ada», et l'autorisation d'utiliser ce nom pour un compilateur fut subordonnée à la confrontation avec succès du compilateur à une suite de validations. Le premier compilateur Ada ainsi validé fut Ada/ED, réalisé par l'équipe de New York University. De nombreux compilateurs ont été validés depuis. Quelques années plus tard, le DoD a abandonné ses droits sur la marque, considérant que cette contrainte n'était plus nécessaire. Il a en revanche déposé une marque de certification, utilisable uniquement par les compilateurs validés. La notion de validation est maintenant tellement liée au langage Ada qu'il n'existe pas de compilateur non validé.
Il est de règle, cinq ans après la parution d'une norme, soit de confirmer le standard tel quel, soit de décider de mettre en route une révision. On savait dès le début que celle-ci serait nécessaire : certains pays n'avaient voté la norme d'origine que pour ne pas ralentir le processus de standardisation et à la condition qu'à la première occasion, on réglerait certains problèmes restés en suspens, principalement celui du jeu de caractères (Ada 83 utilisait l'ASCII 7 bits, qui est notoirement insuffisant pour les langues européennes). Plusieurs autres facteurs concouraient à la nécessité d'une révision. D'abord, comme toute œuvre humaine, le standard comportait des imprécisions, des omissions et des ambiguïtés. À l'ISO, un comité de maintenance, l'ARG (Ada Rapporteur Group),est chargé de régler ces problèmes ; une révision de la norme se devait d'incorporer ses décisions. Ensuite, la mise en œuvre pratique du langage a montré certaines petites faiblesses, ou la nécessité de certains mécanismes dus à l'évolution des modes de programmation (programmation orientée objet notamment). Enfin, il y avait un désir de reformuler certaines règles afin de les rendre plus faciles à interpréter.
La décision fut donc prise en 1988 de démarrer le processus de révision. Le DoD fournit encore une fois le support financier nécessaire, et la révision fut conduite par l'Ada 9X Project Office, dirigé par Chris Anderson, qui confia la réalisation technique à une équipe d'Intermetrics conduite par Tucker Taft. Dans la grande tradition d'ouverture d'Ada, un appel fut lancé aux utilisateurs pour connaître leurs desiderata. La réponse fut très importante, et une synthèse en fut tirée, regroupant par thèmes les principales tendances des demandes, et leur attribuant un degré de priorité. De cette synthèse on tira un document, le Mapping Document, destiné à décrire les transformations nécessaires pour satisfaire les points identifiés à l'étape précédente. Ce document, encore très touffu,comportait quasiment tout ce qu'il était envisageable de modifier, c'est-à-dire nettement plus qu'il n'était souhaitable. Sur la base de ce document, une première réunion de l'ISO se tint au printemps 1992 dans la région de Francfort. Cette réunion marathon (50 participants de 12 pays travaillèrent de façon quasi ininterrompue, pendant une semaine, de 9h du matin à 11h du soir) permit d'aboutir à un accord unanime sur les éléments qu'il convenait de changer dans le langage. D'autres réunions internationales, à Salem puis à Boston (USA), permirent de préciser les détails de la révision. Une dernière réunion au mois de mars 1994 à Villars (Suisse) figea définitivement le langage. Les documents ne subirent alors plus que des changements éditoriaux pendant que le processus bureaucratique suivait son chemin, pour aboutir à l'enregistrement officiel par l'ISO le 15 février 1995.
Mais le temps continua à passer... 1995 plus cinq ans, vint le temps de décider de l'opportunité d'une nouvelle révision. En fait, dès novembre 1999 l'ISO prit la décision de ne pas commencer d'effort de révision. Bien entendu, la norme 1995 comportait des imprécisions et des erreurs qu'il fallait tout de même corriger. Il fut donc décidé de simplement faire paraître des corrigendums techniques pour tenir la définition du langage à jour. Un tel corrigendum technique a été officiellement approuvé le 1er juin 2001.
Un corrigendum technique ne peut que corriger des erreurs de définition du langage, il ne peut rien modifier fondamentalement, ni apporter d'améliorations. L'apparition de Java (avec la notion d'interfaces), certains problèmes pratiques apparus à l'usage, le besoin de nouvelles bibliothèques (conteneurs) demandaient des ajouts plus conséquents que ceux autorisés par le corrigendum. Il fut donc décidé début 2001 de préparer un amendement à la norme. Un amendement permet de rajouter de nouvelles fonctionnalités au langage, mais reste d'ampleur très inférieure à une révision. Le texte de l'amendement a été figé à la fin de 2005, ce qui fait que le langage est appelé Ada 2005, même si la parution officielle par l'ISO n'a été proclamée que le 9 mars 2007.
* ↑ American National Standard Institute
* ↑ Organisation Internationale de Normalisation / International Organisation for Standardization / МЕЖДУНАРОДНАЯ ОРГАНИЗАЦИЯ ПО СТАНДАРТИЗАЦИИ. Noter que le sigle «ISO» n'est l'acronyme de son nom dans aucune de ses langues officielles !
* ↑ En septembre 1994, on recensait 806 compilateurs validés par 52 compagnies.
* ↑ En fait, des mesures ont été prises dès 1992 pour permettre d'étendre le jeu de caractères sans attendre la révision de la norme.
* ↑ Allemagne, Belgique, Canada, Grande-Bretagne, Espagne, France, Japon, Pays-Bas, Russie, Suède, Suisse, USA.

## Objectifs du langage

Contrairement à beaucoup de langages de programmation, Ada n'est pas le fruit des idées personnelles de quelque grand nom de l'informatique, mais a été conçu pour répondre à un cahier des charges précis, dont l'idée directrice était de diminuer le coût des logiciels, en tenant compte de tous les aspects du cycle de vie. Le langage est donc bâti autour de quelques idées-forces :
* Privilégier la facilité de maintenance sur la facilité d'écriture : le coût de codage représente environ 6% du coût global d'un logiciel ; la maintenance représente plus de 60%.
* Fournir un contrôle de type extrêmement rigoureux : plus une erreur est diagnostiquée tôt, moins elle est coûteuse à corriger. Le langage fournit des outils permettant de diagnostiquer beaucoup d'erreurs de cohérence dès la compilation.
* Permettre une programmation intrinsèquement sûre : des contrôles nombreux à l'exécution permettent de diagnostiquer les erreurs dynamiques. Un programme doit être capable de traiter toutes les situations anormales, y compris celles résultant d'erreurs du programme (autovérifications, fonctionnement en mode dégradé).
* Offrir un support aux méthodologies de programmation : le langage doit faciliter la mise en œuvre des méthodes du génie logiciel, sans exclusive et sans omission.
* Être portable entre machines d'architectures différentes : les programmes doivent donner des résultats identiques, ou au moins équivalents, sur des machines différentes, y compris en ce qui concerne la précision des calculs numériques.
* Permettre des implémentations efficaces et donner accès à des interfaces de bas niveau : exigence indispensable à la réalisation notamment de systèmes « temps réel ».
* Offrir un support à une industrie du composant logiciel : en fournissant des interfaces standard et des garanties de portabilité sur toutes les machines, Ada permet la création d'entreprises spécialisées en composants logiciels, tandis que d'autres réalisent des produits finis (applications) par assemblage de ces composants.

## Au-delà du langage

La définition d'un nouveau langage est une condition nécessaire, mais non suffisante, pour entrer dans une ère de production industrielle de logiciel. Aussi ne peut-il exister de compilateur Ada sans un environnement de programmation associé ; en particulier, le langage définit des règles très strictes concernant les dépendances entre unités de compilation, pour garantir la cohérence globale des différentes unités constituant un programme Ada. Ceci nécessite au moins un outil de gestion de projet associé. En fait, parallèlement à la définition du langage, se poursuivait un effort de définition de l'APSE (Ada Programming Support Environment) qui a abouti au rapport STONEMAN. Celui-ci définit les outils logiciels qui doivent faire partie d'un environnement de programmation Ada.
Aujourd'hui, il existe non seulement des compilateurs de qualité industrielle, mais aussi des outils d'environnement de haut niveau : metteurs au point symboliques, outils de gestion de projet, de documentation et de support méthodologique se sont multipliés. Les objectifs d'Ada sont ceux de tout le mouvement du génie logiciel, et le langage n'est qu'un élément qui s'intègre dans un ensemble, notamment méthodologique, plus vaste.

## Exercices

* Étudier comment ont été développés les langages COBOL, C, Pascal et Ada. Quels sont ceux qui avaient un cahier des charges au départ, et comment l'historique explique-t-il les particularités de chacun ?
* Une particularité d'Ada est d'avoir été normalisé avant l'apparition du premier compilateur. Expliquer en quoi ceci est un facteur important pour la qualité de la définition du langage.

# Présentation du langage

Ada est un langage algorithmique d'une puissance d'expression considérable, dérivé de Pascal dont il a retenu les structures de contrôle et certains types de données, mais avec en plus des possibilités d'encapsulation de données, de modularité, de modélisation de tâches parallèles, et de traitement des situations exceptionnelles. Ada reste un langage procédural « classique » (contrairement à LISP, SNOBOL, SETL ou Prolog par exemple), mais c'est un langage qui a un spectre sémantique assez vaste, et dont les apports originaux sont surtout du domaine de l'ingénierie du logiciel : fiabilité, maintenabilité, modularité, portabilité, réutilisabilité de composants logiciels.
Ada est-il un langage orienté objet ? Cette question a fait couler beaucoup d'encre. Ada 83 offrait un excellent support aux méthodes objet par composition mais n'avait pas de structure réalisant directement l'héritage ; il s'est ensuivi une querelle de mots pour savoir si Ada était « orienté objet » ou non, et l'utilisation parfois du terme « basé objet » (?) pour décrire les langages tels qu'Ada qui fournissaient indiscutablement la notion d'objet, tout en se passant fort bien de l'héritage... Ce débat n'a plus lieu d'être : la version 95 a apporté le support de l'héritage et des méthodes par classification. Nous discuterons complètement de ces questions dans la deuxième partie du livre, mais retenons que, maintenant, Ada est indiscutablement un langage orienté objet.

## Un cadre général proche de Pascal

Lors de l'appel d'offres qui conduisit à la définition d'Ada, il était spécifié que la proposition de langage devait être fondée sur la base d'un « grand » langage classique (Pascal, PL/1, Algol...). Il est significatif que les quatre langages finalistes avaient tous choisi Pascal comme base de départ.
Le programme ci-dessous est un premier exemple qui imprime « Bonjour », « Bon après-midi » ou « Bonsoir » en fonction de l'heure de la journée.
Il n'y a pas de construction spéciale pour désigner le programme principal : c'est une procédure comme les autres. La clause with qui est en tête permet d'utiliser deux paquetages (packages) qui fournissent respectivement l'accès aux fonctionnalités d'entrées-sorties (Text_IO) et à l'utilisation du temps (Calendar).
De façon générale, toute unité de compilation (structure pouvant être compilée séparément, comme les sous-programmes et paquetages) doit citer au début les autres unités qu'elle utilise au moyen d'une clause with. La clause use est une simplification d'écriture dont nous reparlerons plus tard.
Les instructions de base sont inspirées de celles de Pascal (Figure 0), tout en tenant compte de ses imperfections reconnues : toutes les instructions se terminent par un point-virgule et les instructions structurées se terminent par un mot clé, éliminant ainsi le besoin de blocs begin..end (ou des accolades {..} de C).
L'ordre des déclarations n'est pas imposé : il est donc possible de regrouper les types, variables et constantes qui sont logiquement reliés. Signalons aussi la présence de deux originalités fort utiles : les pragmas et les attributs. Un pragma permet de donner des indications au compilateur ; par exemple, si la performance est plus importante pour une application que l'espace mémoire, on pourra en informer l'optimiseur au moyen du pragma :
Les attributs permettent d'obtenir des renseignements (pouvant dépendre de l'implémentation) sur des objets, des types, des sous-programmes... Si l'on souhaite par exemple connaître la taille en bits d'une variable (attribut 'Size), on écrira :
Nous ne décrirons pas ici tous les pragmas et attributs, mais nous donnerons des précisions lorsque nous en rencontrerons dans le cours du livre.

## Sous-programmes

Les procédures et fonctions (que l'on appelle collectivement des sous-programmes) se présentent de façon similaire à Pascal : un en-tête, suivi de déclarations, puis d'instructions (Figure 1).
* Procédure
* Fonction
* Paramètres
Il n'existe aucune limite au type retourné par une fonction : ce peut être un tableau, un article, une tâche... Le mode de passage des paramètres fait référence à l'usage qui en est fait : les paramètres peuvent ainsi être déclarés in (lecture seulement), out (écriture seulement), ou in out (lecture et écriture). Ceci remplace avantageusement la notion de « passage par valeur » ou de « passage par variable ». Bien sûr, tous les sous-programmes sont récursifs (et réentrants, puisque Ada permet le parallélisme).
Plusieurs sous-programmes peuvent porter le même nom s'ils diffèrent par le type de leurs arguments. Cette possibilité s'appelle la surcharge et permet de donner des noms identiques à des sous-programmes effectuant la même fonction logique sur des types différents : par exemple, toutes les procédures d'impression s'appellent Put quel que soit le type de ce que l'on imprime.

## Exceptions

La notion d'exception fournit un moyen commode de traiter tout ce qui peut être considéré comme « anormal » ou « exceptionnel » dans le déroulement d'un programme. Une exception se comporte comme une sorte de déroutement déclenché par programme depuis une séquence d'exécution « normale » vers une séquence chargée de traiter les cas exceptionnels.
Une exception peut être déclarée par l'utilisateur ; certaines exceptions sont prédéfinies, comme Storage_Error en cas de mémoire insuffisante, ou Constraint_Error en cas de valeur incorrecte affectée à une variable.
Une exception est déclenchée soit implicitement (en cas de non-respect d'une règle du langage à l'exécution), soit explicitement au moyen de l'instruction raise. Un bloc de programme peut déclarer un traite-exception auquel le contrôle sera donné si l'exception citée se produit dans le bloc considéré. Voici un bloc dans lequel un traite-exception permet de renvoyer une valeur par défaut dans le cas où il se produit une division par 0 :
Si une exception n'est pas traitée localement, elle est propagée aux unités appelantes, jusqu'à ce que l'on trouve un traite-exception adapté, ou que l'exception sorte du programme principal, ce qui arrête l'exécution. Une clause spéciale, when others, permet de traiter toutes les exceptions. Voici un exemple de ce que pourrait être un programme principal qui garantirait un arrêt propre, quoi qu'il arrive dans le programme, sans connaissance a priori de la fonctionnalité des éléments appelés :
Il est important de noter que des événements « catastrophiques », comme la saturation de l'espace mémoire, provoquent simplement en Ada la levée d'exceptions prédéfinies, et sont donc traitables par le programme utilisateur.

## Le modèle du typage fort
### Le typage fort, même pour les types élémentaires

Toutes les variables doivent être déclarées et munies d'un type. Le typage est extrêmement strict (beaucoup plus qu'en Pascal – ne parlons pas de C ou de C++), et ceci constitue un atout majeur du langage. Nous y reviendrons souvent.
Un type définit un ensemble de valeurs muni d'un ensemble d'opérations portant sur ces valeurs. En fait, un type Ada représente des entités de plus haut niveau que les types d'autres langages : il représente en effet une entité du domaine de problème, et non une entité machine. Le programmeur exprime les exigences de son problème ; si par exemple il doit représenter des temps avec une précision absolue de 1 ms et des longueurs avec une précision relative de 10$^{-5}$, il déclarera :
C'est le compilateur qui choisira, parmi les types machine disponibles, le plus performant satisfaisant au moins les exigences ainsi exprimées.
Le langage offre une vaste palette de types numériques : types entiers normaux ou modulaires (ces derniers – nouveaux en Ada 95 – sont non signés et munis d'une arithmétique modulaire), types flottants, fixes et décimaux (ces derniers également apportés par Ada 95). Par exemple, la première déclaration ci-dessus correspond à une déclaration d'un nombre point fixe, qui approxime les nombres réels avec une erreur absolue constante, alors que la seconde correspond à une déclaration de nombre point flottant qui approxime les nombres réels avec une erreur relative constante. Ces derniers correspondent aux nombres «réels» des autres langages. Noter la possibilité de limiter l'intervalle de valeurs en plus du type et de l'étendue de la précision. Puisqu'il s'agit d'entités de nature différente, deux variables de types différents sont absolument incompatibles entre elles (on ne peut additionner une longueur et un temps !), même lorsqu'il s'agit de types numériques. Étant donné :
on peut écrire :
mais l'affectation suivante est interdite par le compilateur :
Autrement dit, on ne peut pas « mélanger des choux et des carottes ». Ada est le seul langage à offrir cette propriété, qui paraît tout à fait naturelle aux débutants en informatique... et pose parfois des problèmes aux programmeurs expérimentés. Il est toujours possible de convertir des types numériques entre eux (le nom du type sert d'opérateur de conversion). Si l'on veut demander à une personne d'aller à l'étage correspondant à son âge, on peut écrire :
Ceci montre un point fondamental de la « philosophie » Ada : on n'empêche jamais le programmeur d'écrire ce dont il a besoin, mais si ce qu'il demande est potentiellement dangereux, on exige de lui qu'il décrive explicitement le comportement demandé afin d'avertir le futur programmeur de maintenance.
Ada dispose de types énumératifs et de formes habituelles pour les tableaux et enregistrements simples (nous verrons des formes plus perfectionnées par la suite) :
Des agrégats permettent de fournir directement des valeurs d'un type structuré :
* ↑ 10$^{-16}$ m représente le diamètre du quark, 10$^{25}$ m est le diamètre de l'Univers (cf. [Mor82]).

### Types et sous-types

Ada fait une distinction très nette entre la notion de type et la notion de sous-type. Alors que le type décrit les propriétés générales d'une entité du monde réel, le sous-type exprime des restrictions applicables seulement à certains objets du type. Ces restrictions sont appelées contraintes. Une contrainte peut être par exemple la limitation des valeurs à un certain intervalle numérique, ou la précision des bornes d'un type tableau non contraint (dont on a laissé les bornes indéfinies au moyen du symbole «boîte» (<>), comme ci-dessous).
L'affectation, le passage de paramètre sont autorisés (à la compilation) si et seulement si les types correspondent ; une exception se produira à l'exécution si les sous-types sont incompatibles. Un sous-type peut toujours être dynamique : ceci permet notamment de choisir les tailles des tableaux lors de l'exécution, sans pour autant recourir à l'utilisation de pointeurs (qui existent par ailleurs dans le langage, où on les appelle des types accès). Ceci permet de définir des sous-programmes travaillant sur des tableaux de bornes quelconques sans perte de contrôle des débordements. Des attributs permettent de connaître les valeurs effectives des bornes. Voici un exemple de sous-programme effectuant le produit de deux matrices :
Ce sous-programme effectue le produit de deux objets de type Matrice dès lors que les bornes de la deuxième dimension de la première matrice correspondent aux bornes de la première dimension de la deuxième matrice (sinon, il y a levée de l'exception Matrice_Error). Grâce aux attributs, le compilateur peut déterminer qu'aucun débordement d'indice n'est possible ; l'optimiseur supprimera tous les contrôles inutiles, et le gain de sécurité apporté par Ada ne se traduira par aucune perte de performances. La possibilité de définir des opérateurs arithmétiques entre types quelconques (y compris tableaux), ainsi que de définir des fonctions renvoyant des types structurés, permet de simplifier considérablement l'utilisation. On pourra écrire :

### Types paramétrables

Ada offre un mécanisme original et extrêmement puissant pour paramétrer un type article de la même façon que l'on peut fournir des paramètres pour diriger le comportement des sous-programmes. Ces paramètres s'appellent discriminants et doivent être d'un type discret (entier ou énumératif), ainsi que d'un type accès (pointeur) en Ada 95. Les discriminants peuvent servir à contrôler la structure de l'article ; ils peuvent être lus, mais ne peuvent être modifiés. Voici par exemple comment représenter des matrices carrées :
Une contrainte de discriminant peut être dynamique ; on peut ainsi lire la taille de la matrice souhaitée :
Les discriminants peuvent aussi servir à faire des structures dont les composants présents dépendent de leur valeur :

### Types dérivés

Plutôt que de redéfinir un type avec toutes ses propriétés, on peut créer un nouveau type à partir d'un type existant dont il héritera les propriétés (domaine de définition et opérations). Un tel type est appelé type dérivé. Bien sûr, il s'agit d'un type différent, et donc incompatible avec le type d'origine.
Des types dérivés l'un de l'autre, ou dérivés d'un même ancêtre, sont convertibles entre eux. Ce mécanisme a été enrichi en Ada 95 pour fournir le mécanisme de base de la programmation orientée objet.

### Résumé de l'arborescence des types

Les différentes formes de types et leurs relations sont schématisées à la figure 2 (nous avons inclus quelques types apportés par Ada 95 qui seront présentés dans la prochaine partie).
Ada peut certainement se prévaloir d'être le langage le plus riche en matière de typage !

## Paquetages

Une notion centrale en Ada est celle de paquetage (package). Le paquetage permet de regrouper dans une seule entité des types de données, des objets (variables ou constantes) et des sous-programmes (procédures et fonctions) manipulant ces objets. Un paquetage est constitué de deux parties, compilables séparément : la spécification et le corps. La spécification comporte une partie visible comprenant les informations utilisables à l'extérieur du paquetage, et une partie privée qui regroupe les détails d'implémentation auxquels les utilisateurs du paquetage n'ont pas accès (mais qui sont nécessaires au compilateur) ; ces deux parties sont séparées par le mot clé private.
L'exemple ci-dessous montre la spécification d'un paquetage de gestion de nombres complexes.
Le type Complex est appelé type privé : dans la partie visible, on ne fait qu'annoncer sa présence, la définition complète étant fournie dans la partie privée (après le mot private) ; l'utilisateur ne peut faire usage des propriétés de l'implémentation. Si on décide par la suite de représenter les nombres complexes sous forme polaire plutôt que cartésienne, les règles du langage garantissent qu'aucune application utilisant ce paquetage n'aura à être modifiée. Un type privé dispose de l'affectation et de la comparaison d'égalité (et d'inégalité). Il est possible de supprimer ces propriétés en déclarant le type comme "limited private" ; le type est alors un type limité.
Une spécification ne définit que l'interface externe du paquetage ; on doit fournir dans le corps du paquetage l'implémentation des fonctionnalités offertes. Par exemple pour la fonction "+", on aurait la forme suivante :
Les paquetages peuvent être compilés séparément, et seule leur spécification est nécessaire pour les utiliser. On réalise ainsi une séparation complète entre les spécifications d'une unité fonctionnelle et son implémentation. De plus, les règles très rigoureuses de recompilation garantissent, d'une part, qu'il n'est pas possible d'utiliser une information cachée d'un paquetage, et d'autre part que la recompilation des unités qui utilisaient une spécification est obligatoire lorsque celle-ci est modifiée : le langage garantit donc la cohérence globale des différentes unités constituant un programme.

## Unités génériques

Les unités génériques sont des unités paramétrables permettant de définir un algorithme indépendamment des types d'objet manipulés. Voici par exemple une procédure générique qui intervertirait ses deux arguments :
La déclaration du type Elem comme private signifie que n'importe quel type pour lequel l'affectation (et la comparaison d'égalité) est définie peut faire l'affaire. L'unité générique n'est pas utilisable par elle-même : ce n'est qu'un modèle (on dit parfois un moule) dont on doit faire une instanciation pour obtenir l'unité réelle.
L'instanciation précise les valeurs des paramètres génériques. Ainsi, pour obtenir une procédure permettant d'échanger deux variables de type Age, nous écrirons :
L'exemple suivant donne la spécification d'une unité générique de tri de tableau :
Les paramètres génériques sont le type d'indice du tableau, le type de composant, le type du tableau lui-même, et une fonction de comparaison. Cette fonction de comparaison est munie d'une valeur par défaut (clause is <>), signifiant que si l'utilisateur ne fournit pas de valeur, la comparaison prédéfinie (si elle existe) doit être utilisée. Les avantages de cette démarche sont évidents : à partir d'un algorithme écrit une fois pour toutes, et que l'on pourra optimiser soigneusement, il est possible d'obtenir instantanément les fonctions de tri sur n'importe quel type de données, avec n'importe quel critère de comparaison. On obtient par exemple ainsi une procédure triant un tableau de nombres flottants dans l'ordre croissant :
Si l'on souhaite trier dans le sens décroissant, il suffit d'inverser le critère de comparaison :
Pour cette instanciation, nous avons utilisé une association nommée de paramètres : la correspondance entre paramètres formels et réels se fait au moyen du nom du paramètre formel, et non pas par la position. Ceci améliore considérablement la lisibilité et évite bien des erreurs. L'association nommée est également possible (et même recommandée) pour les appels de sous-programmes.

## Parallélisme

Le langage Ada permet de définir des tâches, qui sont des unités de programme s'exécutant en parallèle. Les tâches sont des objets appartenant à des types tâche ; elles peuvent donc être membres de structures de données : on peut ainsi définir des tableaux de tâches, des pointeurs sur des tâches, etc.
Les tâches se synchronisent et échangent des informations au moyen d'un mécanisme unique appelé rendez-vous. Une tâche dite serveuse déclare des points d'entrée, dont la spécification ressemble à une spécification de procédure. Une instruction spéciale, accept, lui permet d'accepter un appel du point d'entrée. Une autre tâche peut appeler le point d'entrée à tout moment. Une tâche acceptant une entrée, ou une tâche appelant un point d'entrée, est suspendue jusqu'à ce que son partenaire ait effectué l'instruction complémentaire. Le rendez-vous a alors lieu, la communication s'établissant par échange de paramètres comme pour un appel de procédure. Une fois le rendez-vous terminé, les deux tâches repartent en parallèle. D'autres formes syntaxiques permettent de raffiner ce mécanisme, en permettant l'attente multiple d'un serveur sur plusieurs points d'entrée à la fois, l'attente avec temporisation (time out), l'acceptation conditionnelle, ou la possibilité de terminer automatiquement la tâche si le serveur n'est plus nécessaire.
Ada étant un langage « temps réel », il est possible de suspendre une tâche pendant un certain intervalle de temps et d'accéder à l'heure absolue. Le paquetage Calendar fournit la définition d'un type « temps » (Time) et d'une fonction renvoyant l'heure courante.
Il est possible de mettre une tâche en attente au moyen de l'instruction delay :
Ada 95 a rajouté une instruction delay until permettant d'attendre jusqu'à une heure absolue.
Voici un exemple de tâche simple permettant de tamponner la transmission d'un caractère entre une tâche productrice et une tâche consommatrice. La spécification annonce que la tâche fournit deux services (Lire et Ecrire) :
Le corps de la tâche décrit l'algorithme utilisé :
La tâche boucle sur une attente multiple (instruction select) de ses entrées Lire et Ecrire. Ces entrées sont munies de gardes (clauses when) qui feront que l'on n'acceptera de servir l'entrée Lire que s'il y a effectivement un caractère dans le tampon, et inversement que l'on n'acceptera Ecrire que si le tampon est vide. La clause terminate assure la terminaison automatique de la tâche lorsque plus aucun client potentiel n'existe.

## Utilisation de bas niveau

Ada étant un langage également destiné à la programmation des systèmes, il permet facilement d'accéder au bas niveau : on peut forcer la représentation machine des structures abstraites, spécifier des traitements d'interruptions, inhiber les vérifications de type, et même inclure du code machine. Toutefois, des précautions ont été prises pour conserver la sécurité même avec ce genre de manipulations.

### Clauses de représentation

Il est possible de spécifier au bit près la représentation des types de données. Normalement, la représentation interne est choisie par le compilateur, et le langage garantit la totale indépendance de l'utilisation des types vis-à-vis de leur représentation. Cependant, il faut parfois imposer une représentation, notamment lors d'interfaçages avec le matériel ou avec des sous-programmes écrits dans d'autres langages. On donne alors au programmeur une vision de haut niveau, tout en imposant la représentation de bas niveau. On peut ainsi décrire une cellule de mémoire écran d'IBM-PC :
Si l'on veut changer la valeur du bit Clignotement d'un attribut, on écrira simplement :
ce qui aura bien pour effet de changer la valeur du bit considéré. On voit qu'ainsi, il n'est plus jamais besoin de calculer des masques, décalages, etc., pour intervenir à bas niveau. En cas de modification de la représentation interne des données, aucune modification du code n'est nécessaire.
Les règles du langage ne sont nullement changées par la présence de clauses de représentation, ce qui constitue un outil très puissant. Supposons que nous ayons besoin, par exemple lors de l'écriture d'une interface, de deux représentations physiques différentes d'un même type de données. Il suffit de faire un type dérivé muni de clauses différentes de celles d'origine. Les types dérivés étant convertibles entre eux, le changement de représentation sera pris en charge par le compilateur, comme dans l'exemple suivant :

### Détypage

Les conversions précédentes étaient des conversions de haut niveau, respectant la sémantique des types. Il est parfois nécessaire d'effectuer des conversions plus directes (type cast), notamment lorsqu'une même donnée doit être vue selon deux niveaux d'abstraction différents. Par exemple, des caractères sont des entités propres, qui ne sont pas considérées comme des valeurs numériques. On ne peut donc additionner des caractères, ce qui n'aurait aucun sens... sauf si l'on veut calculer un CRC. On peut alors forcer une vue d'un caractère sous forme numérique grâce à l'instanciation de la fonction générique Unchecked_Conversion :
La sécurité des types n'est pas mise en cause, car les règles du langage font que l'utilisation de cette fonction n'est possible qu'à la condition de mettre en tête de module la clause with Unchecked_Conversion. Ceci a deux conséquences importantes :
* Celui qui relit le programme est immédiatement prévenu de la présence dans le module de fonctionnalités potentiellement dangereuses ou non portables. Réciproquement, l'absence de cette clause signifie qu'il ne peut y avoir de forçage de type.
* Si les règles de codage du projet imposent une justification spéciale pour l'utilisation d'Unchecked_Conversion, le chef de projet peut s'assurer qu'aucune utilisation non autorisée n'en a été faite simplement en interrogeant son gestionnaire de bibliothèque, sans avoir à inspecter manuellement tout le code : la règle peut donc être commodément vérifiée. Ce point est fondamental : une règle qui serait invérifiable en pratique est lettre morte dans un projet quelque peu important.
* ↑ Cyclic Redundancy Code, code utilisé pour vérifier la validité de données, et fondé sur une valeur clé obtenue par des manipulations arithmétiques sur les codes des caractères.

## Compilation séparée

Favoriser la modularité des programmes était une contrainte forte dans la définition du langage. Il fallait faire en sorte que l'on n'ait jamais intérêt à faire des modules trop gros, donc difficiles à maintenir.
Ceci a été obtenu au moyen de la notion de bibliothèque de programme. Une bibliothèque Ada est différente de ce que l'on appelle « bibliothèque » dans d'autres langages de programmation ; c'est une sorte de base de données conservant les produits des compilations : codes objets bien entendu, mais également informations sémantiques sur les modules (résultats de la compilation des spécifications), et aussi souvent informations annexes : dates de première et dernière recompilations, copie des sources, noms des fichiers d'origine...
La compilation d'une unité Ada consiste à la rajouter à cette bibliothèque, tout en mémorisant ses liens de dépendance (clauses with), ainsi que toutes les caractéristiques des entités exportées par les spécifications.
Ceci permet de garantir des contrôles aussi stricts, notamment en matière de typage, pour des unités compilées séparément que pour des unités compilées en même temps. Le compilateur refusera de construire un programme incohérent. On obtient ainsi le bénéfice des systèmes de contrôle externes utilisés dans d'autres langages (makefile), mais gérés automatiquement par le compilateur. On dit que le langage Ada fournit la possibilité de compilations séparées, mais non indépendantes.
Le processus de construction d'un programme en Ada est donc très différent de ce qui se passe dans les autres langages. On construit tout d'abord (avec un utilitaire fourni avec le compilateur) une bibliothèque vide. Puis l'on ajoute, module par module, des unités à cette bibliothèque. On bâtit ainsi progressivement le programme, en assemblant les différents morceaux qui le constituent comme les pièces d'un puzzle. On commencera par mettre dans la bibliothèque les spécifications communes à l'ensemble du projet, puis chaque programmeur codera les implémentations de ces spécifications ; le langage garantira la cohérence des implémentations par rapport aux spécifications aussi bien que celle des appels par l'utilisateur. De plus, tous les compilateurs possèdent des raffinements personnels améliorant ce modèle : bibliothèques liées (bibliothèques physiquement différentes, mais logiquement reliées), sous-bibliothèques fournissant des règles de visibilité des modules (un module est recherché dans la sous-bibliothèque avant d'être recherché dans la bibliothèque principale), familles de bibliothèques...
Ada 95 a quelque peu assoupli le modèle de bibliothèque, notamment pour diminuer les recompilations et permettre d'autres modèles de gestion de modules. Mais le principal a été conservé : il est impossible de construire un programme incohérent.

## Exercices

* Prendre n'importe quel programme d'exercice en C ou en Pascal, et le traduire directement en Ada. Reprendre le programme précédent et le recoder en utilisant au mieux les fonctionnalités d'Ada non présentes dans les autres langages. Comparer la puissance d'expression des deux versions.
* Comparer les contrôles de type offerts par Ada avec ceux d'autres langages. Pourquoi aucun autre langage n'a-t-il été aussi loin dans les contrôles ?
* Comparer les mécanismes de compilation séparée de divers langages avec ceux d'Ada.

# Les nouveautés d'Ada 95

Les nouveautés d'Ada 95 sont organisées autour de quelques grands thèmes : extensions pour la programmation orientée objet, nouvelles fonctionnalités pour la programmation système et le temps réel, meilleur support de très gros projets... et correction des problèmes connus d'Ada 83. D'autre part, la nouvelle norme est en deux parties, séparant le langage proprement dit de la bibliothèque prédéfinie. Les besoins spécifiques de domaines particuliers sont pris en compte dans des annexes facultatives.

## Perfectionnements généraux

Le jeu de caractères de base est maintenant Latin 1 (8 bits) et non plus ASCII (7 bits). Le compilateur peut offrir des options permettant de gérer d'autres jeux de caractères, tels que Latin 2 ou IBM/PC. Les lettres accentuées sont autorisées même dans les identificateurs.
Ada 95 fournit un support direct de l'arithmétique décimale, exigence indispensable à son succès en informatique de gestion. Les types décimaux sont des types point-fixe, mais à base décimale au lieu de binaire. On notera également l'introduction de types entiers modulaires (les opérations « bouclent » au lieu de déborder) et l'ajustement du modèle de l'arithmétique aux évolutions du matériel (arithmétique IEEE notamment).
De nouvelles formes de paramètres génériques (passage de paquetage générique en paramètre formel générique) facilitent la combinaison de fonctionnalités, en supprimant une cause majeure de duplication de code. Le plus simple est de l'illustrer par un exemple :
Lors de l'instanciation du paquetage Matrices_Complexes, on passera n'importe quel paquetage obtenu par instanciation du paquetage Nombres_Complexes :
Enfin de nouvelles formes de pointeurs sont autorisées : pointeurs sur objets globaux et pointeurs sur sous-programmes facilitent les interfaces avec des systèmes extérieurs, systèmes de fenêtrage notamment. Des contrôles à la compilation comme à l'exécution garantissent l'impossibilité de référencer un objet qui n'existerait plus, comme un objet local à un sous-programme après la sortie de celui-ci par exemple.

## Extensions pour la programmation orientée objet

De nombreux utilisateurs critiquaient le manque de support d'Ada pour le paradigme d'héritage. Ce point était cependant délicat, car l'héritage s'oppose à certains des principes d'Ada, entraînant notamment un affaiblissement du typage (certaines cohérences de type ne peuvent être vérifiées qu'à l'exécution, alors qu'Ada les assure traditionnellement lors de la compilation). Enfin, la programmation orientée objet conduit souvent à une multiplication de pointeurs et d'allocations dynamiques cachées. Or le monde du temps réel, particulièrement pour les applications devant fonctionner 24 heures sur 24, est très hostile à l'allocation dynamique à cause des risques de fragmentation mémoire et de perte d'espace qu'elle induit. Il fallait donc introduire un mécanisme permettant d'ouvrir Ada à de nouvelles applications, sans lui faire perdre ses principes fondamentaux : primauté de la facilité de maintenance sur la facilité de conception, contrôle précis de l'allocation et vérifications strictes lors de la compilation.
La solution retenue est fondée sur une extension naturelle du mécanisme des types dérivés : les types étiquetés (tagged). Nous ne présenterons ici que quelques lignes directrices, car nous en détaillerons l'utilisation dans la deuxième partie de ce livre. Il s'agit de types munis d'un attribut supplémentaire caché permettant d'identifier le type d'un objet à l'intérieur de sa classe (ensemble des types dérivés directement ou indirectement d'un même ancêtre).
Ces types offrent trois nouvelles possibilités :
* L'extension de types. Il est possible, lorsque le type parent d'un type dérivé est un type étiqueté, d'ajouter des champs supplémentaires lors de la dérivation. Les opérations du parent restent disponibles, mais n'opèrent que sur la partie commune.
* Le polymorphisme. Des sous-programmes peuvent posséder des paramètres se référant à la classe d'un type étiqueté : ils sont applicables alors non seulement au type d'origine, mais également à tous les types qui en sont dérivés. Il est possible de définir des pointeurs sur classe, pouvant désigner n'importe quel objet appartenant à la classe. Cette dernière forme de polymorphisme permet l'implémentation des mécanismes habituels de la programmation orientée objet, tout en conservant le contrôle explicite des pointeurs.
* Les liaisons dynamiques. Si on appelle un sous-programme en lui passant un paramètre de type classe, le sous-programme appelé n'est pas connu à la compilation. C'est l'étiquette (tag) de l'objet réel qui détermine à l'exécution le sous-programme effectivement appelé. Nous reviendrons sur ce mécanisme lors de l'étude des méthodes par classification, mais remarquons qu'il n'exige pas de pointeurs : contrairement à nombre de langages orientés objet, on peut effectuer des liaisons dynamiques sans imposer de pointeurs, cachés ou non.
Généralement, les langages orientés objet fournissent toutes ces possibilités « en bloc ». En Ada 95, l'utilisateur ne demandera que les fonctionnalités dont il a besoin, et ne paiera que le prix de ce qu'il utilise. Là où l'extensibilité et la dynamicité sont plus importantes que la sécurité, Ada offrira toutes les fonctionnalités nécessaires. En revanche, un concepteur qui ne souhaiterait pas utiliser ces nouvelles fonctionnalités conservera le même niveau de contrôle de type et la même efficacité du code généré qu'avec Ada 83.
On notera que l'héritage multiple est obtenu au moyen des autres blocs de base du langage plutôt que par une construction syntaxique spéciale. Ceci n'est pas une omission, mais un choix délibéré : le risque de complexité lié à ce mécanisme aurait été trop important, et les autres perfectionnements, notamment concernant les génériques, ont permis de construire tous les cas d'héritage multiple que l'on peut rencontrer dans la littérature.
* ↑ Dont Eiffel, Java, C# et C++. Noter qu'en Java, tout est pointeur, ceux-ci n'apparaîssent donc plus explicitement. Cela ne signifie pas qu'ils ne sont pas là !

## Nouvelles facilités pour la gestion de très grosses applications

Une grande force du langage Ada est son contrôle automatique de configuration : le compilateur vérifie toutes les dépendances entre modules compilés séparément, et aucun module ne peut référencer un module périmé. Dans de grosses applications, ceci peut conduire à des recompilations massives, d'autant plus regrettables que la modification n'est bien souvent due qu'à l'ajout de fonctionnalités pour un besoin nouveau, ce qui ne perturbait pas en fait les anciens utilisateurs. D'autre part, il est souvent nécessaire de regrouper logiquement des paquetages concourant à un but commun ; un tel ensemble est appelé « sous-système » dans la terminologie de Booch. Des environnements de programmation sont actuellement capables de gérer de tels sous-systèmes, mais il n'y avait pas de support direct au niveau du langage.
Ada 95 introduit la notion de paquetages hiérarchiques. Il est possible de créer des paquetages « enfants » qui rajoutent des fonctionnalités à des paquetages existants sans avoir à toucher à leurs « parents », donc sans recompilation des utilisateurs de ces parents. Ceci permet une meilleure séparation en fonctionnalités « principales » et fonctionnalités « annexes », facilitant l'utilisation aussi bien que la maintenance. Par exemple, lorsque l'on fait un type de donnée abstrait, on ne souhaite généralement pas mélanger les propriétés de base avec les entrées-sorties. Ceci peut s'obtenir de la façon suivante :
Le corps du paquetage enfant a accès à la partie privée du parent, autorisant donc des implémentations efficaces fondées directement sur la représentation interne du type abstrait. De plus, il est possible de définir des enfants « privés », non accessibles depuis les unités extérieures à la famille. On obtient ainsi des contrôles supplémentaires, puisque seul le parent est visible de l'extérieur et constitue le point d'entrée obligé du sous-système dont les autres éléments sont cachés.

## Nouvelles facilités pour le temps réel et la programmation système
### Parallélisme et synchronisation

Un certain nombre de perfectionnements ont été apportés à la demande des utilisateurs. Si, comme en Ada 83, chaque tâche peut être munie d'une priorité d'exécution, celle-ci n'a plus à être statique. La priorité initiale est définie par une déclaration dans la tâche, et un paquetage prédéfini fournit les services permettant de changer dynamiquement cette priorité.
Les types tâches peuvent être munis de discriminants, qui servent à paramétrer leur comportement. On peut transmettre ainsi à la tâche des valeurs simples (notamment l'espace mémoire requis ou la priorité d'exécution), mais également des pointeurs sur des blocs de paramètres, sur d'autres tâches...
Ada 95 a rajouté une nouvelle forme d'appel d'entrée, le select asynchrone, qui permet à un client de continuer à travailler pendant qu'une demande de rendez-vous est active, ou après avoir armé un délai. Si la demande est servie, ou si le délai expire, avant la fin du traitement associé, celui-ci est avorté ; sinon, c'est la demande qui est annulée. Les deux formes de select asynchrone sont montrées ci-dessous :
La synchronisation et la communication entre tâches étaient fondées en Ada 83 sur un mécanisme unique : le rendez-vous. Pour protéger une variable contre des accès simultanés, il fallait l'enfermer dans une tâche « gardienne ». Ce mécanisme fonctionnait bien ; il conduisait cependant à une multiplication des petites tâches, et la communauté temps réel souhaitait un moyen simple, plus léger et très performant de protéger des variables.
La solution proposée par Ada 95 s'appelle les articles protégés. Il s'agit d'objets (ou de types d'objets) auxquels sont associés des sous-programmes spéciaux, dont on garantit l'exclusivité d'accès. Ils s'apparentent donc aux moniteurs de Hoare, mais en plus perfectionné (possibilité de mettre des barrières notamment). Voici un tel objet protégé :
Une tâche appelant l'entrée Passer est bloquée jusqu'à ce qu'une autre tâche appelle la procédure Ouvrir ; toutes les tâches en attente sont alors libérées. La fonction En_Attente permet de connaître le nombre de tâches en attente. Pour bien comprendre cet exemple, il faut savoir que :
* Plusieurs tâches peuvent appeler simultanément une fonction de l'objet protégé, ou (exclusivement) une seule tâche peut exécuter une procédure ou une entrée (algorithme des lecteurs/écrivains).
* Des tâches susceptibles de s'exécuter parce que la condition associée à une entrée est devenue vraie s'exécutent toujours avant toute autre tâche appelant une opération de l'objet protégé.
Par conséquent, les tâches libérées lorsqu'un appel à Ouvrir a remis la variable Ouverte à True reprendront la main avant toute tâche non bloquée sur l'entrée. Une tâche qui appellerait l'entrée Passer juste après qu'une autre a appelé Ouvrir se bloquera donc bien (après que toutes les tâches en attente auront été libérées) ; aucune condition de course n'est possible.
Il est possible (avec les implémentations supportant l'annexe « temps réel ») d'associer une priorité à un objet protégé, qui est une priorité « plafond » : toute opération protégée s'effectuera à ce niveau de priorité, et il est interdit à une tâche de niveau de priorité supérieure d'appeler ces opérations protégées. Ce mécanisme permet de garantir l'absence de phénomènes d'inversion de priorité; de plus, sur un système monoprocesseur, il est suffisant pour garantir l'exclusivité d'accès aux objets protégés, sans nécessiter de sémaphore supplémentaire, ce qui rend les objets protégés particulièrement efficaces.
Retenons sur le plan philosophique l'introduction dans Ada d'un mécanisme de synchronisation par les données, ce qui représente une nouveauté importante par rapport à la version précédente du langage.
Une autre fonctionnalité importante est l'instruction requeue qui permet, pendant le traitement d'un accept de tâche ou d'un appel d'une entrée d'objet protégé, de renvoyer le client en attente sur une autre queue. Par exemple :
Ici une tâche serveuse de messages dispose d'un point d'entrée unique pour des messages urgents (les télégrammes) ou moins urgents (les lettres). Elle traite les télégrammes immédiatement, mais renvoie les lettres sur son entrée Voir_Plus_Tard, qu'elle ira traiter lorsqu'il n'y aura plus de messages en attente sur Envoyer.
* ↑ Cas qui se produit lorsqu'une tâche de haute priorité est bloquée par une de plus basse priorité à cause d'un accès à une ressource commune.

### Gestion du temps

S'il existe plusieurs bases de temps dans le système, l'implémentation a le droit de fournir d'autres types « temps » en plus du type prédéfini Calendar.Time. L'annexe « temps réel » impose la présence d'un temps « informatique », garanti monotone croissant (dans le paquetage Ada.Real_Time). Il est possible de mettre une tâche en attente jusqu'à une heure absolue par l'instruction delay until :
L'attente absolue est exprimée dans un quelconque « type temps » (au moins Calendar.Time). Si l'implémentation en fournit plusieurs, c'est le type de l'argument qui déterminera l'horloge utilisée. Ce mécanisme permet de disposer d'instructions liées au temps portables, tout en bénéficiant de l'accès à des horloges spécifiques si la précision est plus importante que la portabilité.
* ↑ Le temps par défaut peut correspondre à l'heure légale, et donc subir des sauts vers l'avant ou vers l'arrière lors des passages heure d'été / heure d'hiver.

### Contrôle de l'ordonnancement et gestion des queues

Normalement, l'ordonnancement des tâches est géré par l'exécutif fourni avec le compilateur. Ada 95 autorise plusieurs politiques d'ordonnancement, sélectionnables au moyen d'un pragma. Une seule est standardisée : FIFO_Within_Priorities, qui exprime que les tâches sont ordonnancées dans l'ordre où elles deviennent candidates, les tâches moins prioritaires ne s'exécutant que si aucune tâche plus prioritaire n'est susceptible de prendre le contrôle d'un processeur. Si cette politique est choisie, alors il est obligatoire de spécifier également le plafond de priorité pour les types protégés. Une implémentation est autorisée à fournir d'autres politiques d'ordonnancement.
Cependant, de nombreuses applications temps réel souhaitent un contrôle plus précis de l'ordonnancement. Dans ce but, l'annexe « programmation système » fournit des attributs et un paquetage permettant de récupérer le « Task_ID » interne d'une tâche (en particulier, de l'appelant dans un rendez-vous) et d'effectuer certaines opérations dessus. Un paquetage permet de créer des attributs de tâches, sortes de variables associées à chaque tâche (concrètement, cela signifie que l'on rajoute des variables utilisateur dans le bloc de contrôle de tâche).
À ces fonctionnalités, l'annexe « temps réel » rajoute un paquetage de contrôle synchrone fournissant une sorte de sémaphore sur lequel les tâches peuvent venir se bloquer, et un paquetage de contrôle asynchrone permettant de suspendre et de relancer n'importe quelle tâche dont on connaît le « Task_ID ». L'ensemble de ces fonctionnalités permet à l'utilisateur de contrôler entièrement l'ordonnancement des tâches ; en particulier, les attributs permettent de stocker dans chaque tâche les données nécessaires à la détermination de la prochaine tâche à ordonnancer.
En ce qui concerne la gestion des queues, les requêtes de rendez-vous et les files d'attente des entrées protégées sont traitées par défaut dans l'ordre premier arrivé - premier servi. L'annexe « temps réel » fournit un pragma (Queing_Policy) demandant de traiter les requêtes par ordre de priorité (les tâches plus prioritaires « doublent » les tâches moins prioritaires dans la queue) ; toute implémentation est autorisée à fournir des pragmas définissant d'autres algorithmes de mise en queue.

### Gestion des interruptions

Ada 83 faisait gérer les interruptions par des tâches, qui associaient des entrées à des interruptions physiques. À l'usage, ce mécanisme s'est révélé peu conforme à l'attente des utilisateurs temps réel. Il existe toujours en Ada 95 (ne serait-ce que par compatibilité ascendante), mais est classé « obsolète », ce qui signifie que l'on est censé lui préférer le nouveau mécanisme, l'attachement d'interruptions à des procédures protégées. Ce mécanisme fait partie de l'annexe « programmation système ».
Il existe deux moyens d'attacher une procédure protégée à une interruption : statiquement, au moyen de pragmas, ou dynamiquement au moyen de sous-programmes fournis dans le paquetage Ada.Interrupts. Ce paquetage fournit également des fonctionnalités permettant de savoir si une procédure est actuellement attachée à une interruption, d'attacher une interruption à une procédure protégée en récupérant un pointeur sur le traitement d'interruption précédemment attaché (pour le rétablir plus tard), etc.
Le choix de représenter les traitements d'interruption par des procédures protégées plutôt que par des sous-programmes normaux permet d'exprimer de façon commode la nature non réentrante des traitements d'interruptions. Il permet également d'interdire l'utilisation d'instructions bloquantes, comme delay ou les appels d'entrées, depuis les gestionnaires d'interruption.

### Flots de données

Un flot de données est un type descendant du type (étiqueté) Root_Stream_Type défini dans le paquetage Ada.Streams. Il représente une suite d'octets et est muni de procédures Read et Write permettant de lire et d'écrire des tableaux d'octets dans le flot. En plus de cette utilisation de premier niveau, l'intérêt des flots de données vient de ce que le langage fournit (sous forme d'attributs) des procédures permettant de lire et d'écrire tout type de donnée (non limité) depuis ou vers un flot de données. L'utilisateur peut redéfinir lui-même ces procédures s'il souhaite une représentation externe différente.
Il peut aussi définir ces procédures pour des types limités. On peut ainsi passer de façon sûre et portable d'une vue abstraite d'un type de donnée vers un simple ensemble d'octets. Comme les fonctionnalités des flots permettent de convertir ces octets depuis/vers n'importe quel type, on peut créer des fichiers de données hétérogènes (contenant des valeurs provenant de types différents) sans perdre les avantages du typage fort. C'est en particulier indispensable pour transmettre des types de haut niveau sur un réseau, ou si l'on veut écrire des méthodes d'accès pour des fichiers indexés ou autre.
Concrètement, un flot peut stocker ses données en mémoire, dans un fichier, à travers un réseau ou par tout autre moyen. Le langage prédéfinit le paquetage Ada.Streams.Stream_IO qui fournit une implémentation des flots de données dans un fichier binaire, et Ada.Text_IO.Text_Stream qui utilise un fichier texte.

### Contrôle de l'allocation mémoire

Le paquetage System.Storage_Elements fournit la notion de tableau d'octets (Storage_Array), ainsi que le type Integer_Address, qui représente une adresse sous forme de nombre entier, et est muni de fonctions de conversion sûres depuis/vers le type System.Address. Le paquetage System.Address_To_Access_Conversion procure des fonctionnalités de conversion (propres !) entre types accès et adresses. Il est donc possible d'accéder directement à la mémoire sans recourir à des utilisations douteuses (et souvent non portables) de Unchecked_Conversion.
Enfin le paquetage System.Storage_Pools fournit un type de données permettant à l'utilisateur de définir des « objets pools » et de les associer à des types accès. L'allocation et la désallocation dynamiques (opérations new et Unchecked_Deallocation) utilisent alors obligatoirement ces objets. L'utilisateur contrôle donc entièrement les algorithmes d'allocation/désallocation de mémoire dynamique, et peut garantir l'absence de fragmentation, le temps maximum d'une allocation dynamique, etc., indépendamment de la bibliothèque d'exécution (run-time) du compilateur.

## Nouveaux paquetages prédéfinis

Ada 83 n'offrait que peu de paquetages prédéfinis, principalement des fonctionnalités d'entrées-sorties. Le langage était suffisamment puissant pour permettre à l'utilisateur d'écrire lui-même tout ce dont il avait besoin. À l'usage, il est apparu cependant que l'utilisateur aurait préféré trouver un environnement plus complet, plutôt que de devoir tout refaire lui-même. Ada 95 offre donc un plus grand nombre de paquetages, fournissant les fonctionnalités faisant partie généralement de la définition des autres langages.
Nous donnons ici un bref survol de cette nouvelle bibliothèque standard. Tous ces paquetages doivent être obligatoirement fournis par tout compilateur validé. Nous n'avons pas inclus les paquetages définis au niveau des annexes (cf. prochaine section). Remarquons que la plupart de ces paquetages n'ont nullement besoin de faire appel aux nouvelles fonctionnalités « 95 ». Les personnes programmant actuellement encore en Ada 83 peuvent donc les utiliser (il existe une implémentation dans le domaine public).

### Organisation générale

Le concept d'unités hiérarchiques a permis d'organiser tous les paquetages de façon logique. Toutes les unités de compilation sont des enfants de Standard. Les éléments prédéfinis sont regroupés comme enfants de trois paquetages principaux : Ada, pour les unités indépendantes de l'implémentation, Interfaces, pour ce qui concerne la liaison avec d'autres langages, et System pour ce qui concerne les éléments dépendant de l'implémentation. Text_IO est ainsi devenu Ada.Text_IO. Des surnommages permettent d'assurer la compatibilité avec les programmes utilisant les anciens noms. La hiérarchie complète de l'environnement est résumée à la figure 3 (nous avons inclus les paquetages définis dans les annexes, décrits dans la prochaine section).

### Le paquetage Standard

La seule modification apportée au paquetage Standard (qui définit les éléments directement utilisables par tout programme) est l'addition du type Wide_Character (caractères 16 bits) et d'un type Wide_String associé.

### Chaînes et caractères

Le paquetage Ada.Characters définit des fonctions de test (Is_control, Is_Upper, Is_Decimal_Digit...) et de conversion (To_Lower, To_Upper...) sur les caractères, ainsi qu'entre Character et Wide_Character. Son enfant Ada.Characters.Latin_1 contient des définitions de constantes nommant tous les caractères du jeu Latin 1.
Plusieurs paquetages offrent des fonctionnalités assez évoluées de gestion de chaînes de caractères et d'ensembles de caractères. Ces fonctionnalités utilisent le type String (et Wide_String) ou des types définis dans les paquetages pour des chaînes de longueur variable soit bornée (Bounded_String), soit non bornée (Unbounded_String).

### Traitement numérique

Le paquetage de fonctions élémentaires (Sin, Sqrt, etc.) qui était déjà une norme secondaire d'Ada 83, fait désormais partie de l'environnement standard. De plus, il existe des paquetages de nombres aléatoires.
* ↑ Notons pour la petite histoire que la formulation du générateur aléatoire a été l'un des points les plus débattus de l'histoire d'Ada 95, et pour lequel le consensus a été le plus difficile à obtenir...

### Entrées-sorties

Le paquetage Ada.Text_IO s'est vu rajouter les nouveaux sous-paquetages génériques Modular_IO et Decimal_IO pour les types modulaires et décimaux, respectivement. Modular_IO est semblable à Integer_IO, et Decimal_IO à Fixed_IO. Text_IO s'est également enrichi d'un nouveau fichier prédéfini, Standard_Error, correspondant à la sortie d'erreur que l'on trouve sur de nombreux systèmes d'exploitation, d'une procédure Get_Immediate permettant de lire un caractère « au vol » sans attendre de retour chariot et d'une procédure Look_Ahead permettant d'anticiper sur le prochain caractère à lire.
Un nouveau paquetage Ada.Wide_Text_IO est semblable à Text_IO, mais travaille avec des Wide_Character et des Wide_String.

### Interfaces

Le paquetage Interfaces regroupe des enfants pour les différents langages dont l'interfaçage est supporté. Les spécifications de Interfaces.C, Interfaces.FORTRAN et Interfaces.COBOL sont données dans la norme. On y trouve les déclarations Ada des types de ces langages, ainsi que des fonctionnalités nécessaires à une bonne liaison. C'est ainsi que, pour C, le paquetage enfant Interfaces.C.Strings gère des chaînes de caractères « à la C » (pointeur sur un flot d'octets terminé par un caractère NUL), et que Interfaces.C.Pointers gère une arithmétique sur pointeurs.
Nous ne pouvons qu'insister sur la facilité d'interfaçage apportée par ces paquetages ; dans le cadre du projet GNAT (cf. paragraphe le compilateur GNAT), nous avons implémenté une interface complète avec le système d'exploitation OS/2 (en utilisant la bibliothèque C) ; grâce à ces paquetages, tout a fonctionné sans problème dès le premier essai !

## Annexes

Ada est un langage à spectre très large ; ses plus beaux succès se trouvent aussi bien dans le contrôle de simulateurs ou l'aéronautique qu'en gestion financière ou en CAO. Or il est évident qu'un utilisateur « temps réel » n'aura pas les mêmes exigences qu'un utilisateur « gestion ». C'est pourquoi la norme définit certains éléments comme « dépendants de l'implémentation », afin de ne pas privilégier un type d'application aux dépens d'un autre. D'autre part, Ada est un langage extensible : son mécanisme de paquetages permet de rajouter toutes les fonctionnalités voulues sans avoir à toucher au langage lui-même.
Ada 95 est muni d'un certain nombre d'annexes correspondant à divers domaines d'utilisation. Elles précisent des éléments du langage autrement laissés libres, et définissent des paquetages spécifiques aux différents domaines. Les annexes ne contiennent ni nouvelle syntaxe, ni modification des règles du langage telles que définies dans la norme principale. Les fournisseurs de compilateurs seront libres de passer une, plusieurs, ou même aucune des suites de tests correspondant aux différentes annexes. Bien entendu, le certificat de validation mentionnera quelles annexes ont été passées.
Si la validation n'est accordée pour une annexe que si elle est entièrement implémentée, il n'est pas interdit de n'en fournir qu'une partie, à condition que les fonctionnalités offertes soient conformes à la définition de la norme. Il sera donc possible d'offrir, par exemple, le paquetage de gestion des nombres complexes même si des contraintes matérielles empêchent de valider totalement l'annexe numérique (précision de la machine insuffisante par exemple).
Il est important de comprendre que ce mécanisme n'est pas une rupture avec le dogme d'unicité du langage ; au contraire, il augmente la portabilité des applications en poursuivant la standardisation d'éléments spécifiques d'un domaine au-delà de ce qu'il était possible de faire dans le cadre d'un langage général. On trouve ainsi six annexes :
* L'annexe sur la programmation système décrit des paquetages et des pragmas permettant une gestion plus directe des interruptions, la pré-élaboration de paquetages (mécanisme facilitant la mise en mémoire morte de parties de systèmes) et des manipulations supplémentaires de bas niveau sur les tâches. La documentation devra comporter obligatoirement des précisions sur l'efficacité en temps d'exécution de certains mécanismes.
* L'annexe sur les systèmes temps réel rajoute des fonctionnalités de gestion des priorités (priorités dynamiques notamment), d'ordonnancement des tâches et de gestion des files d'attente (files prioritaires). Elle comporte des exigences sur les délais avant avortement et la précision de l'instruction delay, un modèle de tâches simplifié permettant une implémentation extrêmement efficace, et de nouvelles fonctionnalités de gestion du temps. Noter qu'un compilateur enregistré comme supportant cette annexe devra obligatoirement supporter également l'annexe sur la programmation système.
* L'annexe sur les systèmes distribués permet de définir un programme comme un ensemble de partitions s'exécutant sur des processeurs différents. Le programmeur peut contrôler le format des messages échangés entre les différentes partitions, et il est possible de remplacer dynamiquement une partition sans relancer tout le système. Cette annexe définit également un modèle d'appel de sous-programme distant (remote procedure call). Vu son importance, nous la détaillerons quelque peu ci-dessous.
* L'annexe sur les systèmes d'information fournit de nouvelles fonctionnalités de présentation des données (utilisant des clauses picture analogues à celles de COBOL) et des paquetages d'arithmétique complémentaire sur les nombres décimaux. Cette annexe s'est réduite au fil du temps, car une grande partie de ce qu'elle comprenait a été jugée tellement importante qu'elle a été remise dans le corps du langage !
* L'annexe sûreté et sécurité impose des contraintes supplémentaires au compilateur sur le traitement des cas appelés « erronés » dans le langage et sur un certain nombre de points laissés à la discrétion de l'implémentation. De plus, le compilateur doit fournir tous les éléments (listings) permettant de contrôler manuellement le code généré et offrir des pragmas permettant la vérification par des outils extérieurs.
* L'annexe sur les numériques fournit un modèle encore plus précis des calculs réels, notamment pour les machines utilisant le modèle IEEE des nombres flottants, et inclut la définition des paquetages de nombres complexes, ainsi que les fonctions élémentaires et les entrées-sorties sur ces nombres complexes.

## Systèmes répartis

L'annexe systèmes distribués définit des éléments supplémentaires permettant d'écrire des systèmes répartis. Ada 95 est le premier langage à inclure un tel modèle au niveau de la définition du langage, donc de façon portable et indépendante des systèmes d'exploitation ou des exécutifs sous-jacents.

### Partitions

Un programme Ada est constitué d'un ensemble de « partitions » faiblement couplées, susceptibles de se trouver physiquement sur des machines différentes. Les partitions peuvent être actives ou passives ; dans ce dernier cas, elles ne peuvent posséder de tâche en propre ni de programme principal. La façon de construire des partitions à partir du programme est laissée à la discrétion de l'implémentation, mais les contrôles à la compilation sont faits au niveau « programme » : on garantit donc la cohérence de toutes les partitions figurant dans une même bibliothèque.
La définition de la notion de partition est volontairement vague, afin de lui permettre de correspondre à différentes utilisations, que nous avons résumées dans le tableau de la figure 4.
Figure 4: Utilisations des partitions :
| | Partition active | Partition passive |
| Cartes autonomes | Carte processeur | Carte mémoire |
| Réseau local | Ordinateur | - |
| Système d'exploitation | Processus | DLL, mémoire partagée |

### Classification des paquetages

L'écriture d'un système distribué nécessite certaines précautions. Par exemple, si plusieurs partitions utilisent un paquetage « normal », celui-ci sera dupliqué dans chacune d'elles : s'il possède des états cachés, ils évolueront indépendamment dans chaque partition. Si l'on souhaite un autre comportement, on peut fournir des pragmas de catégorisation pour définir des sémantiques différentes qui apportent les restrictions (vérifiées par le compilateur) nécessaires au bon fonctionnement du système. On trouve ainsi :
* Les paquetages « purs » (pure). Ils sont dupliqués dans chaque partition, mais les restrictions imposées garantissent que leur comportement est indépendant de leur duplication (pas d'état local).
* Les paquetages « partagés passifs » (shared_passive). Ils ne peuvent contenir aucune entité active (ni servir à accéder à une entité active de façon indirecte) et sont présents dans une seule partition passive du système. Ils peuvent contenir des sous-programmes et des états rémanents, qui sont partagés par tous les utilisateurs du système.
* Les paquetages « types distants » (remote_types). Ils servent à définir les types de données susceptibles d'être échangés entre partitions. Les restrictions associées garantissent que ces types ont les propriétés nécessaires pour permettre leur transmission sur un réseau (pas de pointeurs locaux par exemple, sauf si l'utilisateur a défini une procédure permettant la transmission).
* Les paquetages « interface d'appel distant » (remote_call_interface, en abrégé RCI). La spécification de ces paquetages est dupliquée dans chaque partition, mais le corps n'est physiquement présent que dans une seule ; dans les autres, un corps de remplacement route les appels vers la partition qui possède le corps vrai.

### Appels à distance

Lorsqu'un sous-programme est défini dans un paquetage « interface d'appel distant », tous les appels sont routés vers la partition sur laquelle il se trouve physiquement. De même, un pointeur sur un sous-programme défini dans un paquetage « interface d'appel distant » ou « types distants » est un type accès à distance (remote access type). Ceci signifie que le pointeur contient l'information nécessaire pour localiser la partition où se trouve physiquement le sous-programme. Tout appel à travers un tel pointeur sera également un appel distant. Les restrictions sur ces paquetages garantissent que les paramètres de tels sous-programmes sont toujours transmissibles sur le réseau.
De plus, lorsqu'un appel est transmis à travers le réseau, le compilateur doit utiliser pour le routage les services d'un paquetage prédéfini (System.RPC), dont le corps peut être fourni par l'utilisateur, le fournisseur du réseau, etc. Le compilateur est ainsi indépendant de la couche « transport », et il est possible d'implémenter la distribution par-dessus n'importe quelle couche réseau sans avoir à toucher au compilateur.

### Cohérence d'un système réparti

Le problème de la cohérence d'un système réparti est délicat, car il doit répondre à deux exigences contradictoires :
* d'une part, il faut garantir que toutes les partitions du système « voient » les données communes de la même façon ;
* d'autre part, il faut pouvoir arrêter et relancer une partition, éventuellement pour corriger une erreur, sans être obligé d'arrêter tout le système.
Un programme Ada est dit « cohérent » si toutes les partitions qui le constituent utilisent la même version de toutes les unités de compilation. On autorise un programme Ada à être incohérent, c'est-à-dire qu'il est possible de corriger une erreur qui n'affecte qu'une seule partition, d'arrêter la partition et de la relancer avec la nouvelle version. Toutefois, si la correction implique un paquetage « partagé passif » ou « interface d'appel distant » (les seuls qui ne soient pas dupliqués), alors toute communication est coupée entre la nouvelle partition et le reste du système. On autorise donc un système localement incohérent, tant que cela ne sort pas de la partition en cause ; la cohérence est exigée pour les seuls éléments qui sont physiquement répartis sur le réseau.
Enfin, chaque unité de compilation possède un attribut 'Version (pour la spécification) et 'Body_Version (pour le corps), dont la valeur change à chaque modification. Il est donc possible de programmer des contrôles de cohérence plus fins si cela est nécessaire.

## Le compilateur GNAT

Un des obstacles importants à la diffusion d'Ada a longtemps été le prix des compilateurs, notamment pour les institutions universitaires. Il est vrai que les fournisseurs ont consenti un important effort financier pour les organismes d'enseignement, mais le meilleur moyen de convaincre les gens d'utiliser Ada est de leur faire essayer le langage. Or peu d'enseignants (aux crédits limités !) sont prêts à acheter un compilateur, même à prix réduit, juste pour voir...
Conscient de cet état de fait, le DoD a financé le développement d'un compilateur Ada 95 dont la diffusion est entièrement libre et gratuite. Il s'agit en fait d'un frontal du célèbre compilateur multi-langages GCC, faisant partie de l'environnement GNU de la Free Software Foundation, connu sous le nom de GNAT (GNU Ada Translator). La réalisation en a été confiée à la fameuse équipe de New York University, qui s'était déjà illustrée en réalisant le premier compilateur Ada (83) validé.
Comme tous les logiciels diffusés par la Free Software Foundation, GNAT est un logiciel libre, disponible non seulement sous forme exécutable, mais aussi sous forme de sources. Tous les enseignants des cours de compilation peuvent ainsi offrir à leurs élèves d'intervenir dans un compilateur Ada, c'est-à-dire dans ce qui se fait de mieux en matière de compilation.
GNAT est disponible par les canaux de diffusion habituels du GNU, en particulier on peut le charger par FTP anonyme depuis tous les bons serveurs de logiciels libres.
* ↑ Nous avons souvent constaté que les plus farouches opposants à Ada ne l'avaient jamais essayé...
* ↑ Pour Gnu Compiler Collection; le langage C n'est qu'un parmi les nombreux autres langages acceptés.

## Exercices

Ces exercices s'adressent plus particulièrement aux anciens utilisateurs d'Ada 83.
* Etudier comment la notion de bibliothèque hiérarchique peut être utilisée pour réaliser la hiérarchisation des unités de la méthode HOOD (cf. La méthode HOOD)
* Reprendre les exemples classiques de boîte aux lettres ou de variable protégée qui utilisaient des tâches en Ada 83, et les récrire avec des types protégés.
* Le manuel de référence Ada 83 donne un exemple d'utilisation de l'instruction delay pour attendre jusqu'à une heure absolue. Expliquer pourquoi cet exemple est faux, ce qui a rendu nécessaire l'introduction de delay until.
* Comparer les fonctionnalités offertes par les nouveaux paquetages prédéfinis avec ceux de la bibliothèque standard d'autres langages, C notamment.

# Première 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 ]
Méthodes et langages ont toujours eu des rapports conflictuels. Voici un rapprochement de quelques citations et/ou lieux communs pour comprendre l'étendue du problème.
    Le langage n'a aucune importance ; seule l'application de bonnes méthodes peut résoudre la crise du logiciel.
    Le langage Ada a été spécifiquement conçu pour soutenir les méthodes de conception.
    La méthode doit être indépendante du langage d'implémentation utilisé.
    La méthode HOOD a été conçue pour répondre à un appel d'offres de l'Agence spatiale européenne qui voulait une méthode spécifique pour Ada.
    Langage sans méthode n'est que ruine... de la société de service.
Nous voyons, à travers ces quelques exemples, une contradiction dans la perception du rôle du langage par rapport à celui de la méthode : simple moyen de traduire une conception en une forme compréhensible par l'ordinateur pour les uns, expression directe et vérifiée de la conception pour les autres. En fait, les deux ont raison... selon le langage considéré. Dans cette partie, nous allons étudier les rapports des méthodes aux langages, et nous verrons que si les autres langages ne peuvent que partiellement apporter leur soutien aux méthodes, Ada a été conçu précisément pour leur servir de prolongement naturel.

# Rôle et principes des méthodes de conception

Si la nécessité d'une méthode est bien établie dans le développement de logiciels industriels, elle est trop souvent ressentie comme une gêne par le programmeur, qui souhaiterait toujours coder immédiatement. D'ailleurs, celui-ci a souvent écrit des programmes sans utiliser de méthode, et ils ont très bien marché. L'intérêt d'une méthode ne se fait sentir qu'à long terme, souvent au niveau des phases d'intégration. Ceux qui pensent pouvoir s'en dispenser nous rappellent cette citation d'un industriel américain du début du siècle :
Aussi est-il important de bien comprendre le rôle joué par les méthodes pour la conception et leur impact sur le cycle de vie. La décision d'écrire un programme d'informatique est toujours prise dans le but de résoudre un problème du monde réel. L'expression de la solution informatique souhaitée est le cahier des charges, qui définit ce que doit faire l'application informatique. Nous allons parler ici des méthodes permettant, à partir de là, d'obtenir un programme répondant à ses exigences ; il s'agit donc de ce que l'on appelle les méthodes de conception détaillée, par opposition aux méthodes de spécification qui interviennent dans l'établissement du cahier des charges.
* ↑ Sauf pour ceux qui écrivent des virus, qui cherchent alors plutôt à créer des problèmes…

## Complexité et limitations de l'esprit humain

Lorsque le problème à résoudre est suffisamment simple, il est relativement facile de concevoir directement le programme à partir du cahier des charges. Dans l'enseignement traditionnel de l'informatique, qui se réduit trop souvent à l'enseignement de la programmation, on fait exprès de donner des problèmes dont la solution se déduit relativement facilement de l'énoncé. Même si l'on impose une méthode à l'étudiant, celui-ci a généralement l'impression qu'elle est inutile, car il se sent capable de produire directement la solution. Bien souvent, il écrira tout de suite le programme et ne produira les documents de conception que par la suite, pour faire plaisir au professeur.
Ne jetons pas la pierre aux seuls étudiants : deux ans plus tard, ceux-ci sont ingénieurs et continuent à appliquer les mêmes méthodes de travail. Le problème est que cela fonctionne parfaitement ! Bien sûr on connaît parfois quelques difficultés de maintenance, la survie du programme est mise en péril lors du départ du concepteur initial, mais grosso modo on y arrive, d'autant mieux que l'entreprise dispose de personnels « doués ». Mais insensiblement, la taille des programmes tend à augmenter. On continue à développer de la même façon, avec des difficultés croissantes, mais puisqu'on y arrivait jusqu'à présent, il n'y a pas de raison que cela ne continue pas à fonctionner… Le problème est qu'il existe une taille critique, que l'on peut évaluer aux alentours de 10 000 lignes de code par programmeur, pour laquelle les problèmes de gestion vont soudain déborder les capacités des programmeurs. Un proverbe résume bien ce phénomène :
On ne peut définir précisément à partir de quelle largeur de rivière il faudra changer de technologie ; mais il est certain que personne ne songerait à faire le pont de Normandie au moyen d'un tronc jeté en travers ! En logiciel, le problème est qu'en quelque sorte, la rivière s'élargit progressivement jusqu'au point de rupture : que survienne une demande nouvelle, le besoin d'intégrer des interfaces graphiques, le portage sur une nouvelle machine, et la taille critique sera dépassée. Tout à coup, le programmeur sera incapable de répondre à la demande, il sera dépassé par son programme. Que s'est-il passé ?
On a découvert [Mil56] que le cerveau humain ne peut s'occuper en moyenne que de 7 ± 2 éléments en même temps ; tant que le programmeur reste au dessous de cette limite, tout va bien. La complexité du logiciel augmentant, elle va atteindre insensiblement la limite des capacités du concepteur. Celui-ci ne parviendra plus à maîtriser simultanément tous les éléments nécessaires. L'effet de ce phénomène est particulièrement frustrant, car le programmeur continue à maîtriser les vues partielles de son problème ; simplement, lorsqu'il commence à saisir une partie des éléments nécessaires à la compréhension, cela se fait au détriment d'une autre partie qu'il avait comprise précédemment. Face à cette situation, il va chercher désespérément à continuer à percevoir tous les éléments. S'il est très doué, il y parviendra temporairement. Mais une fois le problème résolu et l'échauffement intellectuel passé, ni lui, ni aucun autre programmeur chargé d'effectuer la maintenance ne retrouvera cet «état de grâce» temporaire : le programme sera devenu totalement incompréhensible.
Il faut se faire une raison : la seule chose que la technologie ne peut améliorer, ce sont nos facultés mentales. Comme le disait Dijkstra : «I only have a very small head and I must live with it». Méthodes et langages sont les outils qui nous permettent de réaliser des logiciels toujours plus complexes, en tenant compte de nos limitations intellectuelles.
* ↑ Cette limite ne provient pas de mesures précises, mais est plutôt déterminée intuitivement par nos expériences personnelles. Nous pensons cependant que la plupart des chefs de projet seront d'accord avec ce chiffre.
* ↑ G. A. Miller. «The Magical Number Seven, Plus or Minus Two», The Psychological Review, vol.63, n° 2, mars 1956.
* ↑ Je n'ai qu'une toute petite tête, et il faut bien que je vive avec.

## Notion de saut sémantique

Le cahier des charges d'un problème informatique représente une description de haut niveau de la solution souhaitée. Un programme est une réalisation particulière du cahier des charges, c'est-à-dire une description de bas niveau d'une solution. La conception, c'est-à-dire le processus permettant de passer d'une description à l'autre, est donc fondamentalement un saut sémantique.
Dans un problème simple, un tel saut peut s'accomplir directement : des méthodes intermédiaires sont donc inutiles. Au fur et à mesure que le problème se complique, la taille du saut sémantique augmente. Rapidement, les limitations intrinsèques de nos capacités intellectuelles ne nous permettent plus de maîtriser ce saut, et il ne devient plus possible de passer directement de l'énoncé du problème à sa solution. Or que fait-on dans la vie courante lorsque l'on est en présence d'un saut trop important pour pouvoir être effectué directement ? On le remplace par une succession de petits sauts (communément appelée escalier), individuellement faisables, et dont la somme permet d'accomplir la descente souhaitée. De même en génie logiciel, il faudra définir des étapes de conception intermédiaires destinées à abaisser graduellement le niveau sémantique depuis le niveau du cahier des charges jusqu'à celui du langage de programmation.

## Surmonter la complexité

Comment faire pour réaliser ces différents «sauts» qui constituent l'essentiel d'une méthode de conception ? Le problème, qui n'est pas propre à l'informatique, a été résolu par des moyens connus depuis Descartes :
Informatiquement parlant, ceci signifie que l'on divisera un programme en modules, qui seront des unités de programme pouvant être considérées individuellement par le programmeur. Dans le reste de ce chapitre, nous utiliserons ce terme de «module» pour désigner l'unité de structuration conceptuelle, même si cela ne correspond pas forcément à la notion informatique de «quantité compilable individuellement».
Ce que Descartes ne dit pas, c'est le nombre requis de telles divisions (autant qu'il serait requis… voilà qui ne nous avance guère !). On pourrait croire qu'il suffit de diviser le problème en un grand nombre de modules pour surmonter la complexité. Or il n'en est rien ; le fait de diviser un programme introduit un niveau de complexité supplémentaire : celui dû aux relations nécessaires entre les parties. Supposons une échelle arbitraire, où 7 représenterait la limite de ce qui peut être raisonnablement fait par une seule personne ; si un programme est de complexité 14, il faudra non pas deux, mais trois personnes pour le résoudre, la troisième étant chargée de gérer la communication entre les deux autres. Ceci va en fait limiter le gain pouvant être obtenu de la décomposition. Considérons en effet deux cas extrêmes :
* Nombre de modules = 1. Aucune complexité de relation, mais complexité interne au module maximale.
* Nombre de modules = autant que d'instructions (chaque module ne contient qu'une seule instruction). Aucune complexité interne, mais énorme complexité de relation. En fait, la complexité globale dans ce cas est équivalente au cas précédent ; on n'a fait que transformer une complexité interne en complexité de relation.
Les deux cas extrêmes étant équivalents, cela signifie qu'il existe une taille optimale de décomposition, pour laquelle la complexité globale (interne + relation) est minimale, comme illustré sur la figure 5. En revanche, rien n'indique que cette complexité minimale soit inférieure à 7 ; autrement dit, dans un projet d'une taille suffisante, si l'on se contente de le diviser simplement en modules, on atteindra une complexité de relation trop importante avant d'avoir obtenu une complexité interne acceptable. En quelque sorte, décomposer un problème en sous-problèmes ne nous fournit qu'une seule «marche» dans notre descente des niveaux sémantiques. Il faut raffiner le processus afin d'obtenir autant d'étapes que nécessaire.
Pour cela, on décomposera le problème en un nombre de modules restreint ; la complexité de relation sera acceptable, mais chaque module possédera une complexité interne trop importante. On considérera alors chaque module comme un problème autonome, et on lui appliquera de nouveau la méthode ci-dessus, définissant ainsi des sous-modules, puis des sous-sous-modules jusqu'à obtenir des unités de taille acceptable. Ceci implique, pour garder une complexité de relation gérable, qu'un module d'un certain niveau ne doit avoir de relation conceptuelle qu'avec les modules de même niveau. Le programme sera donc organisé en couches logicielles hiérarchisées, dont l'organisation générale correspondra à la figure 6. Chaque module dépendra logiquement d'un nombre restreint d'autres modules : le graphe de ces dépendances s'appelle la topologie de programme.
Ces considérations ne sont que la justification en termes de maîtrise de la complexité d'une technique connue depuis longtemps : la méthode d'analyse descendante (ou de décomposition hiérarchique). Il reste pourtant une question en suspens dans ce mode de décomposition : en fonction de quels critères va-t-on décomposer les difficultés en parcelles (ou, disons, les programmes en modules) ? Un problème, quel qu'il soit, mais en particulier en informatique, ne se découpe pas en tranches comme un vulgaire saucisson : il faut établir des critères logiques pour répartir les différentes parties du logiciel entre les modules. L'analyse précédente montre qu'il existe deux dimensions dans l'analyse : la décomposition horizontale qui détermine les sous-modules d'un module donné, et la décomposition verticale qui détermine à quel sous-niveau un module doit appartenir. Il sera donc nécessaire de posséder deux ensembles de critères : l'un, pour la décomposition horizontale, qui détermine la répartition des éléments entre modules d'une même étape de décomposition, et l'autre, pour la décomposition verticale, qui détermine à quel niveau de la décomposition un élément donné doit apparaître.
* ↑ Mathématiquement parlant, cela pourrait aussi bien être un maximum... fort heureusement, il n'en est rien.

## Caractérisation des méthodes

Il existe de nombreuses méthodes de conception, regroupées dans un certain nombre de familles (méthodes structurées, en flots de données, entités-relations, orientées objet...). Chaque méthode a ses partisans, et selon le domaine du problème à résoudre, l'une ou l'autre peut s'avérer plus performante. Comment donc définir ce que contient une méthode ?
L'élément essentiel qui caractérise une méthode est son principe de base, c'est-à-dire les critères utilisés pour les décompositions horizontale et verticale. On dit que des méthodes appartiennent à la même famille lorsque leurs critères de décomposition sont identiques, ou au moins très voisins. Noter que comme la décomposition résulte de l'application de ces critères, la topologie de programme obtenue sera, en général, caractéristique de la méthode.
Ensuite, il convient de mettre en œuvre ces principes : les méthodes définissent donc une démarche précise à suivre pour guider pas à pas le processus de conception. En particulier, elles définissent des étapes de conception et des critères d'acceptabilité permettant de passer d'une étape à la suivante.
Enfin, une méthode définira les représentations (textuelle, graphique) et les documents qui doivent accompagner la conception, dans un but de suivi du développement aussi bien que d'aide à la maintenance.
En général, des outils informatiques ont été développés pour soutenir la bonne application des méthodes, en particulier en automatisant le suivi de la conception et la production des documents associés.

## Méthodes de conception et langages

Comme nous l'avons vu précédemment, il est nécessaire d'abaisser graduellement le niveau sémantique de la description de la solution jusqu'au point où l'on peut la décrire au moyen du langage de programmation. La comparaison avec l'escalier montre bien comment définir de façon optimale le nombre et le niveau de ces étapes :
* Il y aura d'autant plus d'étapes intermédiaires que la distance entre le niveau du problème et celui du langage sera plus grande.
* Les étapes seront optimales si elles correspondent à des sauts sémantiques d'égale hauteur.
Ces constatations, qui semblent évidentes, ont pourtant des conséquences qui ne sont pas toujours comprises. En particulier, on a longtemps dit que les méthodes de conception devaient être indépendantes du langage de codage utilisé. Si l'on utilise un langage de plus haut niveau, la répartition optimale des «marches» sera différente, certaines pouvant même être appelées à disparaître (Figure 7). C'est ainsi que s'il est fondamental de décrire des algorithmes au moyen de pseudo-code si l'on programme en FORTRAN, c'est totalement inutile lorsque l'on utilise Ada, car le langage de codage est alors de même niveau sémantique (si ce n'est supérieur !) que le pseudo-code.
Retenons donc que l'utilisation d'un langage de haut niveau comme Ada va permettre une remontée du niveau sémantique de toutes les étapes intermédiaires. C'est donc tout le processus de développement qui est remis en cause.
Ne peut-on aller plus loin, inventer un langage de niveau si haut qu'il nous dispenserait totalement de l'utilisation d'une méthode ? En un sens oui : à partir d'une méthode comme HOOD, il existe des outils qui produisent (presque) automatiquement le code Ada correspondant. Dans l'idéal, il n'existerait plus d'intervention manuelle entre la méthode et l'exécution par l'ordinateur ; c'est la méthode qui serait devenue langage de très haut niveau.
En pratique, il existe une limite à la remontée de niveau sémantique des langages de programmation conventionnels. Ceux-ci se veulent en effet «universels», donc permettant de réaliser des applications de domaines, et de contraintes, très variés. Or contrôler, c'est nécessairement restreindre. Certaines possibilités offertes par un langage sont impératives pour un domaine d'application, et dangereuses pour d'autres : on n'utilisera pas l'héritage dans des applications temps réel, ni la manipulation directe d'interruptions dans une application de gestion. Notons au passage qu'il est par conséquent normal qu'aucune application n'utilise l'intégralité des possibilités d'un langage.
Tout langage est donc un compromis entre puissance d'expression (pouvoir tout faire) et sécurité (tout contrôler). Un langage à vocation universelle sera limité dans ses possibilités de contrôle par la nécessité d'offrir les services requis par diverses classes d'applications. La méthode en revanche est généralement spécifique d'un domaine particulier : personne ne songerait à utiliser Merise en temps réel, ni Buhr en gestion ! Étant plus spécifique, elle peut donc exprimer des contraintes supplémentaires.

# Rôle et principes d'un langage de programmation

Après avoir étudié le rôle des méthodes, nous allons nous intéresser dans ce chapitre à leur prolongement, le langage, et à son influence sur la conception et sur la maintenabilité des applications informatiques. Dans cette réflexion sur le rôle des langages de programmation, nous utiliserons et nous contrasterons principalement Ada d'une part, et le couple C/C++ d'autre part. Ce choix n'est pas seulement dû au fait que ces langages sont les plus utilisés actuellement: nous verrons qu'ils correspondent à deux approches, deux philosophies pourrait-on dire, de la programmation fondamentalement différentes, qui les ont conduit à faire des choix radicalement opposés. Pourquoi ne pas faire la différence entre C et C++? C++ est un perfectionnement technologique du langage C ; on l'appelle souvent the C after. Mais malgré l'introduction d'outils de plus haut niveau (notamment l'héritage), les principes, les rapports du programmeur avec son langage, sont restés les mêmes.
* ↑ Le C d'après.

## Le double niveau de lecture

On considère généralement que le rôle d'un langage est de porter une information, un message, d'une personne qui parle à une personne qui écoute. Dans le cas des langages de programmation, celui qui «parle» est bien entendu le programmeur. Mais qui est celui qui «écoute»? L'ordinateur, pense-t-on généralement. Certes, mais aussi celui qui relira le logiciel plus tard, c'est-à-dire le programmeur de maintenance.
Hofstadter [Hof85] soutient que le message ne porte pas l'information, mais ne fait que déclencher l'information contenue dans l'interlocuteur en «appuyant» sur des «boutons» préexistants. Considérons par exemple la phrase suivante (tirée d'un livre de lecture, classe de CM1):
    Dans la boulangerie, Madame Durand dit à Florence: «Tu n'as pas vu Minet?»
En lisant cette phrase, vous avez parfaitement compris que
    La boulangère s'inquiète auprès de la petite fille d'avoir perdu son chat.
Or aucun des éléments de la seconde formulation ne figure dans la première! Comment savez-vous que Madame Durand est la boulangère? Parce que vous savez que si ce n'était pas la boulangère, la formulation «dans la boulangerie» n'aurait aucune utilité. Qui vous a dit que Florence était une petite fille? Parce que vous savez que la formulation «Madame X» d'un côté, un prénom de l'autre traduit une relation d'adulte à enfant. Qui vous a dit que le chat était perdu? Parce que vous savez que la construction «Tu n'as pas vu...» traduit une inquiétude. Et bien entendu, vous savez que «Minet» est un nom de chat... Cet exemple montre bien que la compréhension par l'homme d'un texte fait usage de toute une culture extérieure au seul message. Or la lecture d'un texte de programme par un ordinateur ne mettra en jeu que le texte lui-même et les règles du langage. C'est de cette différence de lecture que provient un grand nombre d'erreurs, la plus célèbre étant celle survenue à la NASA dans les années 60. Une sonde destinée à observer Vénus est passée à dix fois la distance prévue, suite à une erreur de calcul dans la trajectoire. Une commission d'enquête a révélé qu'un programmeur avait écrit (en FORTRAN) :
Dans l'esprit de celui qui avait écrit cette instruction, ainsi que dans l'esprit de tous ceux qui l'avaient relue par la suite, il s'agissait d'une boucle DO. Cette conviction était suffisamment forte pour que l'on ne remarque pas la présence d'un point à la place de la virgule normalement requise dans une boucle DO... Le compilateur qui ne comprend en revanche que ce qui est écrit a relu cette instruction sous la forme (sachant que les espaces ne sont pas significatifs en FORTRAN) :
c'est-à-dire comme l'affectation de la valeur réelle 1.5 à la variable de nom DO10I, non déclarée (mais ce n'est pas un problème en FORTRAN où la déclaration de variable n'est pas obligatoire)...
Plus prosaïquement, si vous lisez dans un programme:
vous serez persuadé que la boucle sera exécutée 1000 fois, car vous savez que la chaîne de caractères 'M', 'I', 'L', 'L', 'E' a la même signification que le nombre 1000. Le compilateur, lui, ira regarder dans sa table des symboles, où il trouvera que l'identificateur MILLE résulte de la déclaration :
il exécutera donc la boucle dix mille fois...
La compréhension par le programmeur de son programme s'appuie donc sur des éléments non écrits faisant partie de sa culture propre. Pourquoi est-ce nécessaire? Après tout, le compilateur est bien capable de s'y retrouver sans ces éléments «culturels». Mais l'ordinateur dispose d'un atout complémentaire : une mémoire illimitée ; toute information qu'on lui a communiquée une fois reste disponible. Au contraire, le programmeur ne dispose que d'une mémoire à court terme limitée (dont, comme nous l'avons vu, on évalue la capacité à sept «cases»). L'homme va compenser ce manque de mémoire en déduisant les éléments dont il a besoin, recréant ainsi l'information pour éviter de la stocker. Plus prosaïquement, ceci justifie la nécessité (toujours répétée, jamais justifiée, et pas toujours observée) d'avoir des identificateurs parlants : si l'on veut stocker le nombre de lignes imprimées, cela ne fait aucune différence pour l'ordinateur d'appeler la variable XYZ123, NBLN ou NOMBRE_DE_LIGNES. En tant qu'être humain, je dois entièrement mémoriser le rôle de la variable XYZ123. Dans le deuxième cas, je dois mémoriser que NB est une abréviation pour «nombre» et LN pour «lignes». Ce stockage est cependant plus économique, car de nombreuses variables peuvent être construites en combinant ainsi un petit nombre d'abréviations. Avec la troisième forme, je n'ai rien à mémoriser : je peux déduire entièrement la signification de la variable de son nom.
Cependant, cette information portée par le nom de la variable échappe totalement au compilateur : rien n'empêche la variable NOMBRE_DE_LIGNES de désigner la distance de la Terre à la Lune ! Tout l'art du programmeur va donc être d'écrire son programme de façon qu'il porte deux messages à la fois : un pour le compilateur, gouverné par les seules règles du langage, et un pour le lecteur, gouverné par son fonds culturel, et de faire en sorte que les deux correspondent ! La difficulté réside bien entendu en ce que le « fonds culturel » varie d'un programmeur à l'autre. Un exemple typique se trouve dans l'utilisation d'abréviations : par exemple, LN est une abréviation « évidente » de « ligne » pour certains, alors que pour d'autres elle signifiera « longueur » ou « logarithme népérien ». Il convient donc d'éviter de façon générale l'usage de tout « fonds culturel » non explicite, et plus particulièrement des abréviations.
Un exemple caractéristique de la non-compréhension de ce double rôle du langage de programmation peut être trouvé dans les langages C et C++ : dans ceux-ci, deux identificateurs qui ne diffèrent que par l'usage des minuscules ou des majuscules sont différents. Ainsi, Nombre_de_Lignes ne désigne pas la même variable que NOMBRE_DE_LIGNES. Du point de vue du compilateur, il s'agit de deux chaînes de caractères différentes, donc de deux identificateurs différents. En revanche, la signification « culturelle » pour l'être humain ne dépend pas de la façon d'écrire les mots, donc la compréhension de la signification est la même. On oblige le programmeur C à raisonner avec la vue de l'ordinateur, donc à mémoriser un nombre d'informations plus grand pour comprendre la signification d'un programme.
* ↑ Dans le cadre du génie logiciel, on doit toujours considérer que celui qui relit un logiciel n'est pas celui qui l'a écrit.
* ↑ C souffre du même problème : il existe de nombreux cas où une faute de frappe d'un seul caractère conduit à un programme légal, mais faisant quelque chose de totalement différent de ce qui était prévu.
* ↑ Cet exemple n'est pas irréaliste ; nous l'avons effectivement trouvé dans un projet réel...
* ↑ Tout au moins comparée à la mémoire (à court terme) humaine.
* ↑ Ceci n'est pas limité au logiciel... Le français qui arrive aux États-Unis reste en général perplexe devant les panneaux annonçant «PED. XING». Il ignore que la lettre X, qui représente une croix, est souvent utilisée comme abréviation pour le mot «cross» (croix). La signification du panneau est donc «Pedestrian Crossing» (passage piéton).

## Niveau sémantique des langages

On entend souvent l'expression «langage de haut niveau». Nous avons dit précédemment que les méthodes de conception s'arrêtaient lorsque l'on avait atteint le «niveau» du langage de programmation. Mais comment définit-on le niveau d'un langage? La meilleure définition se rapporte à la position du langage dans le processus de développement : plus la formulation sera proche de la définition du problème, plus nous dirons que le langage est de haut niveau. Inversement, si le langage exprime les contraintes de la réalisation sur machine, nous parlerons d'un langage de bas niveau.
Nous allons illustrer cette notion par un exemple simple, et voir comment différents langages permettent de prendre le relais des méthodes plus ou moins tôt (cet exemple est critiquable du point de vue des algorithmes utilisés ; son but n'est que de montrer la démarche d'abaissement du niveau sémantique dans un cas simple).
Supposons que nous voulions trouver tous les diviseurs d'un nombre donné N. La spécification du problème (point de départ) peut s'exprimer comme :
    Calculer la liste de tous les nombres P tels que P divise N
La première étape consiste à exprimer la description de la solution sans faire référence à un quelconque algorithme informatique. Un langage comme SETL nous permet de nous situer directement à ce niveau. On écrirait :
qui se lit «Mettre dans la variable LISTE la suite des nombres P de l'intervalle de 1 à N tels que N modulo P soit égal à 0». Noter qu'à ce niveau, on ne spécifie pas d'algorithme : on exprime le résultat souhaité, et c'est le travail du compilateur de déterminer comment obtenir ce résultat. Nous ne nous préoccupons pas non plus de problèmes annexes, comme de savoir quelle quantité d'espace mémoire réserver : là encore, le système n'a qu'à se débrouiller.
La deuxième étape va consister à trouver un algorithme décrivant comment obtenir la solution désirée, ce qui nous obligera également à introduire les structures de données nécessaires à sa réalisation. L'algorithme s'exprimera au moyen de constructions de haut niveau (boucles, tests, aiguillages...) et les structures de données pourront être relativement abstraites (piles, listes...). Si nous ne disposons pas d'un langage comme SETL, nous devrons exprimer notre conception à ce niveau. En Ada, ceci s'exprimerait comme :
Nous avons choisi de représenter notre liste au moyen d'un tableau, ce qui nous pose des contraintes d'implémentation. Pour pouvoir ajuster nos dimensionnements, nous supposerons que la variable N est déclarée du type Integer. Nous ne savons pas a priori combien il y aura de diviseurs, mais il ne peut y en avoir plus de N. Nous allons déclarer un type pour repérer les éléments dans le tableau; bien entendu, la nature de ce type n'a aucun lien avec le type de N: nous choisissons donc un type différent, et non un sous-type. Pourquoi avoir choisi de le dériver de Positive plutôt que de Integer? Tout type servant à compter ne peut intrinsèquement avoir que des valeurs positives ; en revanche, le fait que ce type soit limité à N provient de notre problème particulier. Il est donc logique d'exprimer cela en faisant dériver le type Index de Positive, puis en le contraignant à l'intervalle 1..N.
    Natural et Positive sont deux sous-types prédéfinis de Integer, comportant respectivement les nombres positifs ou nuls et les nombres strictement positifs.
En revanche, les diviseurs que nous allons stocker sont de même nature que N : nous préférons donc déclarer Diviseurs comme un sous-type de Natural, et nous exprimons qu'en plus tous les diviseurs sont inférieurs ou égaux à N. Pourquoi avoir autorisé la valeur 0? Nous avons fait ici le choix de conception d'initialiser à 0 les éléments non significatifs du tableau (encore une fois ce n'est ni le seul, ni même certainement le meilleur choix possible). Nous devons donc autoriser cette valeur «de garde». Cependant, pour exprimer ce rôle spécial de la valeur 0, nous déclarons la constante Non_Alloué, qui nous servira plutôt que la valeur 0 elle-même là où ce sera nécessaire : nous exprimons ainsi que nous ne considérons pas 0 comme un diviseur.
Il ne nous reste plus qu'à exprimer qu'une liste est un tableau repéré par un Index de Diviseurs garni au départ uniquement de valeurs Non_Alloué. Nous avons choisi d'appeler Prochain la variable servant à repérer l'endroit où mettre le diviseur, pour bien marquer qu'elle désigne le prochain élément à remplir, et non le dernier rempli. Remarquez qu'à ce niveau, notre préoccupation principale a été d'exprimer autant que possible au moyen du langage Ada la connaissance que nous avions du domaine de problème et des propriétés de la solution, notamment au niveau du typage.
Les notions utilisées à ce stade (tableau dynamique, boucle à compteur, types abstraits) sont encore assez éloignées de ce que peut connaître la machine. Celle-ci ne manipule que des types élémentaires (adresses, octets, mots mémoire) et des instructions très simples (tests et branchements). L'étape suivante va donc consister à représenter les éléments abstraits au moyen des entités machine. Si nous devions maintenant écrire ce programme en langage C, il deviendrait :
Nous avons dû faire ici un choix d'implémentation supplémentaire : notre besoin est celui d'un tableau de taille choisie dynamiquement. C ne fournit pas cette possibilité ; nous devons donc la réaliser au moyen d'un tableau de taille fixe. En général, l'espace supplémentaire sera perdu. Si au contraire l'espace est insuffisant, nous courons le risque de déborder du tableau et d'écraser les zones mémoire qui le suivent. Une bonne programmation exigerait donc de vérifier que le tableau ne déborde pas ; mais que ferait-on alors? C ne nous fournit pas non plus de mécanisme d'exception. Nous devrions donc adopter une politique particulière de traitement d'exception (comme d'avoir systématiquement une variable de retour pour signaler si la fonction s'est bien terminée), politique qui devrait être traitée par l'appelant... On voit que le traitement complet des cas exceptionnels nous entraînerait vite très loin, et c'est pourquoi beaucoup de programmeurs C préfèrent prévoir largement les tableaux et prier pour qu'ils ne débordent pas...
Le fait que nous voulions une initialisation particulière du tableau ne peut plus être donné simplement : nous devons écrire une boucle pour expliquer à l'ordinateur comment initialiser le tableau. Il nous faut même expliquer comment réaliser cette boucle : initialisation, test de fin, incrément... Enfin, les différentes propriétés que nous connaissons sur les types de données sont perdues : nous ne manipulons plus que des adresses (pointeurs) et des int, c'est-à-dire des entiers machine.
Cette description utilise donc un niveau adapté au fonctionnement de l'ordinateur, mais encore suffisamment abstrait pour être indépendant d'une architecture machine particulière. La dernière étape consiste à traduire le programme en instructions machine correspondant à l'ordinateur cible, ce que l'on fait lorsque l'on travaille en assembleur. À ce niveau, toute notion de structure disparaît : des notions aussi simples que des boucles doivent être réalisées au moyen de tests et de branchements. La description appropriée est donc l'ordinogramme : un graphe représentant le parcours exact de la machine. De même, toute notion de typage disparaît, puisque l'on ne fait même plus de différence entre des nombres entiers, flottants ou des pointeurs, ni même entre structures de programmes et structures de données : les seules notions restantes sont des instructions, des adresses et des mots mémoire. La notion de tableau, par exemple, devra être réalisée au moyen de l'indexation pour accéder à des mots mémoire consécutifs. Le langage n'est plus à même d'effectuer aucun contrôle, puisque l'on n'est même pas sûr que ce qu'exécutera l'ordinateur correspond au texte du programme (un programme peut se modifier lui-même).
Nous voyons donc que la descente de niveaux sémantiques est essentiellement un passage du « quoi » (expression de besoin) au « comment » (solution au besoin). Bien entendu, il n'est nécessaire de spécifier le « comment » que si le langage ne fournit pas directement l'outil adéquat. Par exemple, nous avons eu l'enchaînement : besoin : liste (OK en SETL), réalisée par des tableaux de taille variable (OK en Ada), réalisés par des tableaux de taille fixe (OK en C), réalisée par une zone mémoire indexée. Remarquer que nous nous sommes arrêtés là parce que nous avons supposé que la machine fournissait directement l'abstraction nécessaire : sur une machine plus élémentaire, nous aurions pu devoir réaliser la notion d'indexation par un calcul d'adresse explicite et une indirection. Remarquons également que la descente dans les niveaux d'abstraction s'accompagne d'une perte d'information : la version SETL exprime quasiment directement le besoin ; en Ada, nous ne voyons plus explicitement la notion de liste. En C, toute l'information sur les relations logiques entre les différentes données est perdue. Enfin en assembleur, les variables deviennent totalement indifférenciées. Les physiciens (et les théoriciens de l'information) diraient que l'entropie du système augmente.
Si cette information n'est plus visible, il est possible de la reconstituer, de même qu'il est possible de faire diminuer l'entropie : mais comme en physique, cela coûte beaucoup d'énergie : il faut reconstruire le comportement dynamique du système (ce qui se passe à l'exécution), puis identifier quelle structure abstraite de plus haut niveau pourrait avoir ce comportement pour implémentation. Inutile de dire que ce processus est nettement plus difficile que la démarche inverse qui consiste à passer de la structure abstraite à son implémentation. Ce qui nous amène à une autre règle d'or :
    Tout comportement doit être décrit au moyen de la structure de plus haut niveau disponible.
C'est dans ce cadre que l'on peut également régler la fameuse « polémique du GOTO », qui fit rage dans les années 70, mais qui n'est pas encore totalement évacuée pour certains. En gros, le GOTO est généralement considéré comme un ordre contraire aux bons principes de programmation, mais d'aucuns argumentent que finalement, les structures de contrôle ne sont que des GOTO déguisés. C'est exact : en fait le rôle d'un langage de haut niveau est précisément de cacher les structures de bas niveau, en leur attribuant un plus haut niveau sémantique. Donc si vous travaillez en un langage de bas niveau (Le défunt FORTRAN IV, le Basic « standard »), vous devrez utiliser des GOTO, et il n'y a pas de honte à cela, à condition que dans une étape de conception précédente vous ayez exprimé votre algorithme au moyen de concepts de plus haut niveau.
Insistons sur un point : quel que soit le langage de programmation utilisé, toutes les étapes que nous avons décrites devront être accomplies... mais pas nécessairement par le programmeur. Le compilateur travaille en procédant de la même façon : une phase d'analyse de haut niveau, suivie d'une phase d'expansion destinée à abaisser le niveau sémantique de la description, suivie enfin d'une phase de génération de code. Le niveau d'un langage correspond donc au niveau sémantique à partir duquel un outil automatique (le compilateur) est capable de prendre le relais du programmeur. Un langage est d'autant plus haut niveau qu'il prend le relais du programmeur plus tôt, c'est-à-dire qu'un plus grand nombre de ces étapes seront accomplies automatiquement.
Automatiquement, certes, mais accomplies tout de même ! Ceci explique pourquoi il est normal que, toutes choses égales d'ailleurs, il faille plus de temps pour compiler un programme écrit dans un langage de plus haut niveau. [Ber89] rapportent qu'il faut quatre fois plus de temps pour compiler un programme Ada qu'un programme Pascal faisant la même chose ! D'ailleurs, les compilateurs simples ne se contentent-ils pas d'une ou deux passes alors que les compilateurs Ada en ont souvent près de huit ? C'est simplement que partant de plus bas, les premiers ont moins de travail à faire. Ceci ne signifie pas que le travail correspondant à l'abaissement du niveau sémantique ne doive pas être fait : simplement il doit être accompli par le programmeur au lieu d'être pris en charge par le compilateur. Pour être honnête, il faut donc ajouter au seul temps de compilation des langages de bas niveau l'effort de conception supplémentaire (et le coût des erreurs introduites) dû à ce plus bas niveau. À ce moment, les avantages du haut niveau redeviennent évidents : [Ber89] rapportent que depuis que les équipes sur lesquelles ont été effectuées leurs mesures ont abandonné Pascal pour Ada, les factures mensuelles passées en temps de compilation ont diminué... simplement grâce au fait que de nombreuses erreurs qui n'étaient diagnostiquées qu'à l'exécution (et qui nécessitaient plusieurs cycles de re-compilation pour être identifiées) sont maintenant «piégées» dès la première compilation.
Notons enfin que le gros risque avec les langages de bas niveau est de court-circuiter des étapes. Nous avons vu comment abaisser progressivement le niveau sémantique d'un projet. Mais l'outil final (le langage) étant en général accessible au concepteur, celui-ci tendra à passer directement de l'expression de haut niveau au codage. Cela peut marcher pour des petits projets, mais lorsque la taille du saut (sémantique) augmente... on finit par se casser la figure.
* ↑ Ce principe de description du résultat à obtenir et non du moyen d'y parvenir est également à la base du langage SQL dans le domaine des bases de données.
* ↑ Ou si nous ne sommes pas prêts à payer en temps d'exécution le prix de la facilité offerte par ce langage...
* ↑ Plus vraisemblablement, dans un programme correctement écrit, elle serait déclarée d'un type dérivé d'Integer.
* ↑ Noter qu'il existe un cas particulier où le programme tel qu'il est écrit lèvera l'exception Constraint_Error. Nous laissons à la perspicacité du lecteur le soin de le découvrir...
* ↑ Tout au moins pour des variables locales classiques, comme le permet Ada. Il faudrait faire appel à l'allocateur et gérer un niveau de pointeur supplémentaire.

## Apport des langages de haut niveau

Les premiers langages de programmation étaient orientés machine : on forçait en quelque sorte le programmeur à penser avec le même vide culturel que la machine. Au moins était-on sûr qu'il n'y avait pas de risque d'interprétations divergentes. En fait, c'était bien entendu extrêmement pénible, ce qui a conduit à l'apparition de langages de plus haut niveau, c'est-à-dire plus proches de la façon de penser du programmeur. Ce faisant, on a également éloigné le niveau de lecture du programmeur du niveau de lecture de l'ordinateur : on a donc accru le risque de divergence entre les deux. Quel est donc l'intérêt des langages de haut niveau ?
Il provient essentiellement de ce que ces langages permettent de communiquer à l'ordinateur une partie du « savoir » supplémentaire que possède le programmeur. Autrement dit, au lieu de forcer le programmeur à comprendre le programme comme le fait le compilateur, on a la possibilité d'indiquer à celui-ci un certain nombre d'éléments « culturels » qui rapprocheront son analyse du texte de celle effectuée par l'être humain. Ceci permettra au compilateur d'effectuer des tests beaucoup plus nombreux, et surtout situés à un niveau sémantique bien supérieur. Le programmeur devra donc prendre soin de tenter de décrire, non plus les éléments de la machine dont il a besoin, mais les éléments de son domaine de problème. C'est pourquoi les types prédéfinis, qui appartiennent au domaine de la machine, devront (en général) être évités. Prenons un exemple.
Dans beaucoup de programmes, et quel que soit le langage de programmation, il est fréquent de trouver des déclarations comme :
Nous prétendons que cette déclaration est trompeuse, non maintenable, dangereuse, non portable, inefficace, et pour tout dire, opposée aux principes même du génie logiciel ! Pourquoi ?
* Elle est trompeuse et non maintenable, car elle autorise des valeurs négatives pour Compteur, alors qu'un compteur ne peut contenir que des valeurs positives ou nulles. Bien sûr, le lecteur humain sait qu'un compteur ne peut être négatif, mais cette information n'est pas fournie au compilateur. Il n'y a donc aucune garantie que la variable sera effectivement utilisée comme compteur. Le programmeur de maintenance avisé devra alors résoudre une question difficile : est-ce que le programmeur d'application a utilisé le type Integer par simple paresse, ou n'y a-t-il pas quelque endroit où il a mis une valeur négative dans Compteur pour réaliser une «grosse astuce»? Le seul moyen de résoudre la question est d'aller inspecter toutes les utilisations de la variable.
* Elle est dangereuse et non portable, parce qu'elle ne se préoccupe pas de la question de la borne supérieure. Tout type de donnée possède nécessairement une limite aux valeurs qu'il peut stocker. Il est vraisemblable que le programmeur n'a voulu qu'une variable «entière», et n'a même pas songé à cette limite, dont la valeur dépend ici de l'implémentation. Trop souvent, les programmeurs déclarent des variables du type Integer simplement pour éviter d'avoir à penser au problème de la borne supérieure, comme s'il s'agissait réellement d'entiers mathématiques. Un tel programme, qui fonctionne parfaitement sur une machine 32 bits, peut avoir un comportement imprévisible sur une machine 16 bits.
* Elle est inefficace, parce qu'une définition plus précise aurait permis au compilateur d'utiliser la mémoire de façon plus efficace, et même de générer un meilleur code. Supposons que cette variable serve à indexer un tableau de 10 éléments; si elle avait été déclarée avec une contrainte correspondant à sa vraie utilisation, le compilateur aurait été capable de n'utiliser qu'un octet de mémoire (au lieu de 2 ou 4), et surtout aurait pu éliminer toutes les vérifications de débordement lorsqu'elle était utilisée pour indexer le tableau.
* Plus que pour toutes ces excellentes raisons, elle est contraire aux principes du génie logiciel parce que tout le savoir qu'avait le concepteur des propriétés de sa variable n'a pas été écrit, et ne sera donc pas transmis aux personnes chargées de la maintenance.
Qu'aurait-il dû faire? Étudier le domaine de problème et refléter ses connaissances dans l'expression du langage, ce qui aurait donné :
Noter que le compilateur aurait utilisé la précision de cette déclaration pour réserver juste l'espace nécessaire : 16 bits si la machine disposait d'entiers non signés, 24 ou peut-être 32 sinon, mais de toute façon le type le plus économique permettant de répondre au besoin exprimé.
Ceci n'est pas possible avec un langage qui travaille au niveau machine ; quel type aurions-nous choisi en C, s'il nous fallait être portable? Certainement pas int (et encore moins short), car même si l'on peut supposer qu'il soit au moins sur 16 bits sur la plupart des machines, on ne peut supposer plus, et c'est insuffisant ici. unsigned ? Le langage n'offre aucune garantie qu'il dispose d'une étendue plus grande que int. Nous aurions dû nous rabattre sur long (ou mieux unsigned long), avec le risque de réserver 64 bits par variable sur certaines machines, là où 16 auraient suffi.

### Les types de données abstraits

Puisque nous voulons désormais travailler sur les éléments du domaine de problème, il nous faut utiliser non des types machine, mais des abstractions d'éléments du monde réel. Les types de données correspondants sont appelés types de données abstraits. Un tel type est caractérisé par un ensemble de valeurs et un ensemble d'opérations qui lui sont applicables.
Si cette notion de type de donnée abstrait est commode pour l'utilisateur, elle est fort éloignée de ce que peut traiter un ordinateur : des nombres. Par conséquent, chaque attribut et chaque opération doivent être implémentés au moyen d'éléments de plus bas niveau. Il existe donc deux vues différentes d'un type de donnée abstrait : la vue abstraite, ou externe, qui exprime les propriétés utilisables de l'abstraction (ce que l'on appelle dans la terminologie Ada la spécification) et les mécanismes utilisés pour concrétiser cette abstraction (pour Ada, l'implémentation). Comme pour tout élément logiciel, la solution au problème de l'implémentation d'une abstraction donnée n'est jamais unique ; cependant, toutes les solutions possibles doivent être considérées comme équivalentes du point de vue de l'utilisateur, tant qu'elles fournissent des abstractions sémantiquement équivalentes.
En principe, il devrait donc être possible de remplacer n'importe quelle implémentation d'une abstraction par une implémentation différente sans perturber les utilisateurs. Si une spécification dépend malencontreusement d'un mécanisme d'implémentation particulier, cela s'appelle une sur-spécification : la spécification est trop précise, car elle mentionne des éléments qui n'appartiennent plus à la vue abstraite. Attention : une spécification peut imposer des contraintes (en temps d'exécution, en mode de gestion de la mémoire) qui interdisent certaines implémentations : il n'y a pas sur-spécification tant que ces contraintes résultent du besoin extérieur, et non d'une vue a priori d'une méthode d'implémentation particulière.
Inversement, si un utilisateur fait appel à des propriétés d'un type de donnée abstrait qui ne font pas logiquement partie de l'abstraction, on dira qu'il viole l'abstraction. Dans un cas comme dans l'autre, il devient impossible de remplacer une implémentation par une autre, et l'indépendance entre spécification et implémentation est perdue.
* ↑ Sauf éventuellement du point de vue des performances.

### Le langage comme outil de vérification

Éviter toute sur-spécification est une tâche difficile qui requiert un grand talent d'abstraction, mais prévenir les violations d'abstraction peut être obtenu par les vérifications effectuées par le langage. Est-ce si important que le langage vérifie cela ? Et à quoi cela sert-il après tout que le langage effectue des vérifications ?
Reprenons le problème à la base. À un moment donné, l'ensemble des valeurs de toutes les variables d'un programme constitue ce que l'on appelle le vecteur d'état. Le plus vaste vecteur d'état comporte toutes les combinaisons de bits possibles pour toutes les variables du programme. Nous appellerons cet ensemble les états matériels. Mais seul un sous-ensemble de celui-ci est autorisé par les règles d'un langage de programmation de haut niveau, puisque le compilateur effectue (au moment de la compilation ou de l'exécution) un certain nombre de vérifications, qui empêchent certaines combinaisons de se produire. Nous appellerons ce sous-ensemble les états autorisés. À l'intérieur de ce sous-ensemble, un ensemble encore plus réduit est constitué des états accessibles par une exécution correcte du programme. Nous appellerons ce dernier ensemble les états corrects. Ces différents états sont résumés dans la figure 8.
Les états matériels doivent nécessairement englober les états autorisés, autrement le langage ne serait pas compilable (sur cette machine). De même, les états autorisés doivent inclure les états corrects, autrement le programme désiré ne pourrait être écrit dans le langage. Notez cependant que les états autorisés ne peuvent coïncider exactement avec les états corrects : cela reviendrait à prouver formellement le programme. Il y a donc nécessairement des états incorrects, correspondant à la zone des états autorisés n'appartenant pas aux états corrects, qui peuvent être atteints en fonction des seules règles du langage; c'est la responsabilité du programmeur de faire en sorte que ceci ne se produise jamais durant l'exécution du programme. D'une certaine façon, un «bug» est une brèche par laquelle on atteint un état incorrect.
C'est sur ce schéma que l'on comprendra le mieux la différence fondamentale de philosophie entre les langages C/C++ et Ada. C a été conçu comme une alternative à l'assembleur ; ses concepteurs l'ont même qualifié d'«assembleur portable». Comme en assembleur, l'idée était de permettre de faire faire «ce que l'on voulait» à la machine ; la crainte était que le langage empêche le programmeur de faire ce qu'il souhaitait. Le langage a donc cherché à étendre au maximum les états autorisés pour les rapprocher au maximum de l'ensemble des états matériels. C'est en ce sens que beaucoup considèrent que C est un langage très puissant : il permet de tout faire.
Tout, y compris des catastrophes... L'approche suivie par Ada est radicalement différente. Le but était d'avoir un langage fiable et de haut niveau pour des applications critiques. Il fallait pour cela non seulement fournir des outils permettant la définition de vues abstraites, mais également interdire l'accès aux détails de l'implémentation afin d'éliminer la plus grande partie des états incorrects. Le but recherché était l'inverse de celui de C : il s'agissait de resserrer l'ensemble des états autorisés autour des états corrects, éliminant ainsi autant d'états incorrects que possible. Vu ainsi, on peut (et même on doit) se poser la question : à quoi cela pourrait-il servir d'autoriser des états incorrects? La vraie question n'est donc pas pourquoi faudrait-il contraindre, mais quel pourrait être l'intérêt de ne pas contraindre.
Cette approche présente tout de même un risque : il peut arriver qu'à force de restreindre les états autorisés, on ampute une petite partie des états corrects. Autrement dit, le langage ne laisse pas, au nom de règles de sécurité, le programmeur faire ce qui lui est nécessaire. Certaines échappatoires (Unchecked_Conversion par exemple, nous y reviendrons) ont été prévues au niveau du langage, mais malgré tout il peut arriver que le langage interdise certaines pratiques qui bien qu'utiles ou même nécessaires dans certains cas, seraient trop dangereuses en général. Le programmeur devra donc changer sa conception pour se plier aux règles générales. Cette nécessité (qui paraît révoltante au programmeur C!) ne paraîtra choquante que dans le monde du logiciel. Toutes les autres branches de l'industrie sont coutumières du fait, et savent qu'elles doivent ajuster leurs conceptions à des règles de sécurité dont l'applicabilité est parfois discutable compte tenu des particularités du contexte, mais qui ont force de loi. Songez par exemple que le défunt Concorde était muni de doubles commandes mécaniques, dont il était de notoriété publique qu'elles étaient quasiment inutilisables dès que l'avion avait pris une certaine vitesse... et qui ont coûté très cher dans le bilan de poids de l'appareil. Mais personne n'aurait osé prendre la responsabilité de les supprimer, et c'est normal, car il s'agissait d'un dispositif de sécurité fondamental.
Quel est l'avantage d'une telle approche? Si on recherche une erreur dans un programme qui compile, on peut être assuré que les règles du langage sont respectées. Autrement dit, on peut garantir que le vecteur d'état du programme appartient toujours à l'ensemble des états autorisés. En réduisant le nombre d'états autorisés mais incorrects, un langage rigoureux diminue la probabilité d'erreurs (moins d'états incorrects sont accessibles) et facilite la maintenance (moins d'états incorrects sont à envisager lors de la recherche d'erreurs). De plus, le compilateur pourra faire usage de cette connaissance supplémentaire pour optimiser le programme. Pour prendre un exemple concret, si l'on doit manipuler des couleurs, il est possible depuis Pascal (le langage !) de définir un type énumératif :
En faisant ainsi, on garantit qu'il n'est pas possible de multiplier deux couleurs, ni d'affecter à une couleur une valeur numérique incorrecte. Il paraît équivalent de déclarer :
Après tout, n'est-ce pas ce que fait le compilateur? Et cela marchera aussi bien que le type énumératif, tout au moins tant que personne ne tentera de violer l'abstraction. Ce que l'on perd, c'est la vérification par le compilateur que seules les propriétés abstraites sont disponibles. Représenter des couleurs par des nombres est une façon d'implémenter la notion abstraite de couleur, ce qui est le travail du compilateur. Si vous le faites vous-même, vous autorisez l'utilisateur de l'abstraction à travailler au niveau de l'implémentation plutôt qu'au niveau abstrait, et vous ouvrez ainsi une voie vers des états incorrects.
Enfin, l'avantage peut-être le plus important de l'approche de haut niveau est le contrôle qu'elle peut exercer sur la conception. Puisque maintenant le compilateur a de l'information sur les éléments de plus haut niveau du problème, une faute de conception se traduira souvent par une incohérence au niveau du typage qui sera interceptée par le compilateur. Combien de fois avons-nous vu des programmeurs nous appeler à la rescousse pour un problème de langage, alors qu'il s'agissait en fait d'un problème de conception! En voici un exemple typique :
Un étudiant débarqua un jour dans mon bureau en grognant : «Ada est vraiment un langage stupide. Regardez ça.» Il travaillait sur le simulateur d'un ordinateur un peu particulier. Il avait utilisé un tableau d'Integer pour représenter la mémoire. La mémoire était divisée en pages de 512 mots. Il avait donc les déclarations suivantes :
Le problème était que le compilateur refusait l'affectation suivante :
alors que la boucle suivante, qui était sémantiquement strictement équivalente, était acceptée :
Évidemment, du point de vue du langage, il y avait de bonnes raisons. La première affectation s'effectuait entre une variable de type Page et une expression de type Mémoire ; comme il s'agissait de deux types différents, l'affectation était interdite. Dans le second cas, on n'affectait que des composants, tous du type Integer, et chacune des affectations était autorisée.
Je ne tentai même pas de m'aventurer dans une telle explication, car l'étudiant aurait pensé (comme le lecteur le pense actuellement) que le typage fort ne fait qu'apporter des complications au malheureux utilisateur en l'obligeant à écrire une boucle explicite. La conversation continua comme ceci :
    Moi : Pourquoi avoir déclaré le type Page?
    Étudiant : Parce que j'en avais besoin!
    Moi : Pensez-vous qu'une page soit une entité de nature différente d'une mémoire?
    Étudiant : Hmm... Non, c'est une sorte de mémoire.
    Moi : Quelle est donc la différence?
    Étudiant : Une page fait toujours 512 mots, alors que la mémoire peut avoir n'importe quelle taille a priori.
    Moi : Ah! Comment peut-on exprimer ceci en Ada?
    Étudiant : J'y suis : une Mémoire est un type tableau non contraint, et Page est un sous-type contraint de Mémoire!
Il modifia son programme de la façon suivante :
et dès lors, son affectation fonctionna sans problème, puisque P et M appartenaient au même type. Que pouvons-nous conclure de cet exemple? Le programme de l'étudiant comportait une faute de conception, puisque l'utilisation qui était faite des types ne correspondait pas aux abstractions souhaitées, ce qui avait provoqué l'erreur de compilation. Le mouvement naturel de l'étudiant avait été de s'en prendre au langage et de chercher un moyen de tourner le contrôle. Il y était arrivé en ne travaillant pas globalement sur les objets, mais au niveau des composants. Remarquez que cela était possible parce que le type Mémoire était visible : s'il s'était agi d'un type privé, il n'aurait pas pu s'en sortir aussi aisément. En fait, il était descendu d'un niveau d'abstraction, puisqu'il ne travaillait plus globalement sur la Mémoire, mais sur la façon dont cette mémoire était représentée. À ce plus bas niveau, la sémantique était plus pauvre, et il avait pu violer l'abstraction.
Cet exemple montre bien comment un langage très strict peut piéger les fautes de conception ; si une telle situation était apparue en C++ (en C, aucun contrôle de ce type n'est possible), il est vraisemblable que l'étudiant se serait contenté d'un vigoureux forçage de type (type cast) pour obliger la compilation à passer... et cacher le problème de conception. Dans le contexte du génie logiciel, où fiabilité et facilité de maintenance sont primordiaux, le langage de programmation doit fournir des contrôles rigoureux pour éliminer un maximum d'états incorrects dès l'étape de compilation. On peut résumer ceci ainsi :
    Ce qui fait la valeur d'un langage de programmation, ce n'est pas ce qu'il autorise, c'est ce qu'il interdit.
* ↑ Sauf en cas de bug du compilateur. Ceux-ci sont suffisamment rares pour que l'on puisse faire cette hypothèse, mais c'est ce qui rend les erreurs dues aux compilateurs si difficiles à trouver.

### Autres formes de protection

La programmation est un domaine plein de pièges subtils, dont certains requièrent une grande attention pour être évités... à moins que le langage ne prenne en charge les problèmes. Si les types abstraits sont l'outil de base de protection du programmeur, il existe d'autres cas où une formulation de plus haut niveau permet d'obtenir une meilleure sécurité.
Un exemple caractéristique est celui des boucles en C. Prenez un programmeur C moyen (ou même expérimenté), et demandez-lui comment il écrirait l'équivalent de la boucle Ada :
Il vous répondra immédiatement :
Demandez-lui alors ce qui se passe si, par exemple, I est un octet (ou plus exactement un char, puisqu'en C les caractères sont des nombres!), que A vaut 0 et B vaut 255. Ce cas de figure n'a rien d'exceptionnel : il s'agit d'une simple boucle sur l'ensemble des caractères. Horreur! Lorsque I atteint 255, il repasse à 0, car il n'y a pas de notion de débordement en C, et la boucle ne s'arrête jamais! Ce qui est surprenant (et inquiétant), c'est que nous avons tenté cette expérience avec de nombreux programmeurs C, et qu'aucun n'a jamais pensé au problème! On est donc en droit de penser que tous les programmes C marchent par hasard, parce qu'on ne s'aventure jamais trop près des cas limites... Bien entendu, des tests de la suite de validation Ada assurent que la boucle produite par le compilateur marche toujours correctement dans ce cas de figure. Le programmeur Ada est donc protégé par le langage d'une erreur grave, sans même avoir à s'en préoccuper.
Cet exemple montre bien que le langage peut effectivement fournir un degré de sécurité supplémentaire qui n'a rien à voir avec un quelconque problème de conception. Remarquons qu'encore une fois, ce résultat est obtenu par le fait que la description Ada se situe un niveau d'abstraction au-dessus de la description C : le programmeur décrit ce qu'il veut obtenir (une boucle de 0 à 255) et non comment l'obtenir (par une affectation, un test et un incrément – algorithme dont nous venons de montrer qu'il était faux).
* ↑ En toute rigueur, la norme C n'interdit pas de «planter» le programme dans ce cas, mais la plupart des compilateurs ne le font pas. Inutile de penser à un traitement d'exception (même en C++, qui a pourtant les exceptions, les débordements ne sont pas traités).
* ↑ Signalons au lecteur qui chercherait la solution correcte en C qu'il n'est pas possible de coder une boucle for (au sens d'Ada) au moyen d'une simple boucle while (dont le for C n'est qu'une variante). Il faut nécessairement un exit (break en C).

## Et l'efficacité?

Si l'utilisation de fonctionnalités de haut niveau améliore indiscutablement la lisibilité, la sécurité et la maintenabilité, l'on est en droit de se demander si cela n'a pas un effet adverse sur l'efficacité. En fait, l'efficacité est souvent invoquée comme raison d'utiliser des langages de plus bas niveau. Voyons donc plus précisément ce problème.
Tout d'abord, il faut savoir de quelle efficacité l'on parle. Il existe souvent des contraintes temporelles au cahier des charges : celles-là doivent être impérativement respectées, car un programme qui ne répond pas à son cahier des charges est un programme faux. La question de l'efficacité se pose seulement après : si le programme vérifie largement ses contraintes, comment va-t-on utiliser la marge supplémentaire? Les machines utilisées au début de l'informatique étaient incroyablement lentes par rapport aux machines (mêmes individuelles) actuelles : on disait alors que le bon programmeur était celui dont les programmes allaient le plus vite possible. À l'époque, les programmes étaient de taille plus modeste qu'aujourd'hui, et l'on se souciait peu de la maintenance... Cette mentalité n'a plus lieu d'être aujourd'hui : un bon programme, c'est d'abord un programme évolutif, fiable et facile à maintenir. De plus, la puissance des machines rend acceptables des solutions autrefois impossibles. Ceci ne signifie pas que l'efficacité ne doive pas être recherchée, mais seulement que, encore une fois, il faut trouver le bon compromis, car ce que l'on gagne en efficacité, on le perd sur d'autres aspects, tels que lisibilité et portabilité. Par exemple, imaginons un programme interactif (comme une interrogation de base de données). S'il donne la réponse au bout de dix secondes, c'est inacceptable (et vraisemblablement trop pour les contraintes du cahier des charges) : il faut faire quelque chose. Si on améliore les performances pour que les réponses parviennent en moins d'une seconde, c'est bien ; l'attente est sensible pour l'utilisateur, mais acceptable. Si l'on fait passer ce temps à un dixième de seconde, c'est parfait : la réponse semble instantanée. Si l'on poursuit dans cette voie pour obtenir un temps de réponse d'un centième de seconde, on perd son temps : l'utilisateur ne sera pas sensible à la différence, et on risque de le payer cher sur d'autres aspects.
D'autre part, il n'est pas du tout évident qu'une description de plus haut niveau se traduise nécessairement par une perte d'efficacité; au contraire, le compilateur est capable d'utiliser le supplément d'information pour mieux optimiser le code. Considérons cet exemple :
Il n'y a pas de vérification au moment de l'affectation à I en (1), mais comme l'on ne sait pas a priori quelle est la valeur retournée, il faut vérifier à chaque utilisation (deux fois en (2) ) que la valeur est correcte. Si nous informons le compilateur de notre intention d'utiliser cette variable pour indexer des chaînes comme ceci :
alors le compilateur effectue une vérification lors de l'affectation en (1), mais il n'est plus nécessaire d'en effectuer lors des utilisations en (2). Comme on utilise les variables plus souvent qu'on ne les modifie, cette approche est généralement préférable. C'est tellement vrai que beaucoup de programmeurs sont déçus lorsqu'ils tentent d'accélérer leurs programmes en les compilant « sans vérification » : le gain excède rarement quelques pour-cents. Ceci signifie simplement que l'optimiseur a été capable d'éliminer de lui-même tous les tests redondants... et donc que le (petit) gain d'efficacité se fait certainement au détriment des tests réellement importants, donc avec un impact maximal sur la fiabilité! Conséquence : en général, en Ada, on laisse toutes les vérifications même dans la version finale, commercialisée, du programme.
Il faut savoir que les techniques modernes d'optimisation permettent d'obtenir des résultats surprenants. Nous avons ainsi connu un programmeur qui avait écrit un programme de façon « ignoble », persuadé que cela irait plus vite. Il reçut du compilateur le message suivant :
    WARNING: frame of control too complicated; optimizer gives up.
Du coup, le programme allait moins vite que s'il avait écrit son code proprement et laissé l'optimiseur faire son travail... À travers cet exemple, on notera la différence d'état d'esprit : le rôle du compilateur C est de fournir une traduction directe vers les instructions machine, l'optimisation étant du ressort du programmeur. Du coup, un compilateur C, même relativement simple, produira un code acceptable – d'où la réputation d'efficacité traditionnellement attachée au C. Inversement, le rôle du compilateur Ada est de libérer (autant que possible) le programmeur des contraintes de bas niveau ; un compilateur sans optimiseur performant serait catastrophique, mais le langage a été conçu pour permettre des optimisations très perfectionnées. Il est donc possible d'optimiser beaucoup plus du code Ada que du code C (cf. [Tar93] pour un exemple de programme initialement développé en C qui n'a pu tenir ses performances qu'après un recodage en Ada).
Enfin, l'on ne saurait trop rappeler que la recherche d'efficacité doit se faire d'abord par la recherche d'algorithmes performants. Les bénéfices que l'on peut en retirer sont de plusieurs ordres de grandeur supérieurs à ce qui peut être obtenu par des «astuces» de codage. Le programmeur doit toujours garder à l'esprit la règle des 90/10 : un programme passe 90% de son temps dans 10% de son code ; or ces fameux 10% sont en général très difficiles à identifier. La seule constante que nous ayons rencontrée dans des projets où se posait un problème d'efficacité est que le point critique n'était jamais là où le supposait le programmeur. Il est indispensable de disposer d'outils de mesure lors de toute recherche d'amélioration des performances.
En conclusion, nous dirons que l'efficacité est importante, mais que ce n'est plus le seul critère à prendre en compte pour juger de la qualité d'un programme, et que les principes du génie logiciel continuent à s'appliquer même pour l'écriture de programmes critiques sur le plan des performances. Quand l'on voit la fréquence avec laquelle l'efficacité sert de justification abusive à des pratiques peu recommandables, on ne peut que conclure par une paraphrase :
    Efficacité, que de bugs on commet en ton nom!
* ↑ AVERTISSEMENT : Structure de contrôle trop compliquée ; l'optimiseur abandonne.
* ↑ Nous avons connu ainsi un compilateur Pascal qui passait l'essentiel de son temps... dans la boucle de lecture de caractères. La réécriture en assembleur de la seule procédure de lecture d'un caractère a permis de doubler la vitesse du compilateur.

## Conclusion

Le typage fort et la vérification par le langage des abstractions sont des dispositifs de sécurité indispensables : ils ne peuvent être pris à la légère. Cette démarche est universellement adoptée dans d'autres branches de l'industrie. Par exemple, une machine dangereuse comme un massicot ne peut fonctionner que si l'opérateur appuie simultanément sur deux boutons, disposés de telle façon qu'il soit obligé d'utiliser ses deux mains : on est ainsi assuré qu'il n'y a pas une main qui traîne sous la lame. On pourrait penser que l'opérateur d'un massicot a une perception beaucoup plus directe du risque qu'il y aurait à laisser une main dans la machine que le programmeur d'utiliser un forçage de type ; il est cependant connu qu'en l'absence de dispositif de sécurité, des accidents se produisent. Il n'y a pas de raison de supposer que les choses se passeraient différemment en programmation. Remarquons au passage que l'utilisation de dispositifs de sécurité dans l'industrie ennuie les opérateurs et diminue bien souvent la productivité ; mais l'on considérerait comme inacceptable de sacrifier la sécurité à de tels impératifs.
Donner au développement de logiciel un caractère industriel demande que l'on reconnaisse la nécessité d'un contrôle plus pointu, et que la sécurité des systèmes complexes ne peut pas ne dépendre que du talent des individus qui les conçoivent. Nous conclurons en illustrant cette différence fondamentale dans la perception du rôle du programmeur par le rapprochement de deux citations tirées respectivement d'introductions aux langages C et Ada :
    C a été conçu dans l'idée que le programmeur est quelqu'un de raisonnable et qui sait ce qu'il fait.
    Ada a été conçu en tenant compte du fait que le programmeur est un être humain.

## Exercices

* En C++, il est possible de définir des classes fournissant la notion de tableau avec contrôle de débordement. Quels en sont les inconvénients par rapport à des tableaux vérifiés par le compilateur ? Discuter sur le plan des principes du génie logiciel et sur le plan des optimisations possibles du code généré.
* Rechercher dans les langages autres qu'Ada les restrictions qui sont d'ordre méthodologique et celles dues à la technique du compilateur. Que peut-on en conclure ?
* Nous avons mis en garde le lecteur contre l'utilisation du type Integer. Ces arguments sont-ils applicables au type String ? Au type Boolean ?
* Écrire au moyen d'une boucle loop simple l'équivalent exact de la boucle for d'Ada. Attention, cet exercice est plus difficile qu'il n'y paraît !

# Liaison entre méthode et langage

Lorsque l'on doit intervenir sur un programme existant pour corriger des erreurs, lui apporter des améliorations ou lui faire subir une révision majeure, on est amené à intervenir sur le code, bien sûr, mais à l'intérieur d'un cadre qui provient de la méthode. Comme toujours, nous considérons que celui qui effectue la maintenance n'est pas le concepteur initial. Il faut donc commencer par comprendre la philosophie générale de la conception, puis déterminer comment l'évolution pourra être effectuée sans perturber la structure générale, ou, si ce n'est pas possible, faire évoluer la conception elle-même. L'énorme différence par rapport à ce qui se passe en conception initiale est qu'à ce moment, le code existe déjà. Il importe donc d'être capable de déterminer l'impact de toute modification de la conception sur le code. Ce problème porte le nom général de traçabilité : être capable de «suivre à la trace» (dans les deux sens) les liens qui existent entre la conception et le code.

## Le problème de la documentation

L'idée la plus naturelle est de considérer que la documentation est là justement pour assurer cette traçabilité. Depuis des années, la question de la documentation des conceptions a été centrale à la démarche de rationalisation de la production de logiciel... et toujours aussi difficile à obtenir :
    La documentation est l'huile de ricin de la programmation : cela doit bien servir à quelque chose, puisque les chefs de projets insistent tant pour en avoir....
Nous avons vu que la bonne compréhension par un être humain d'un programme s'appuyait sur une part «culturelle», c'est-à-dire sur des éléments ne figurant pas dans le texte du programme. Une bonne partie de la difficulté de reprendre le programme de quelqu'un d'autre tient à ce phénomène : la «culture» d'un programmeur est différente de celle d'un autre. Même si l'on reprend son propre programme quelques mois après l'avoir écrit, il faut un certain temps avant de «reconstituer le contexte» nécessaire à sa compréhension. On comprend mieux alors le rôle de la documentation : elle permet de transmettre cette information nécessaire à la compréhension du programme qui ne se trouve pas dans le texte du programme. Cela explique aussi pourquoi les programmeurs sont généralement réticents à l'écrire, et pourquoi elle est généralement si mal faite : elle doit mettre noir sur blanc ce qui paraît absolument évident à son auteur... mais pas forcément à quelqu'un d'autre ; le programmeur aura donc du mal à identifier ce qui posera problème à un futur mainteneur doté d'une autre «culture», et risquera d'insister lourdement sur des détails inutiles, tout en laissant de côté des points fondamentaux. Nous avons été ainsi amené à relire des spécifications pour un système de menus. La documentation fournissait abondance de détails sur toutes les interactions possibles, parlant par exemple de saisie d'éléments de menu par l'utilisateur, chose qui nous a paru totalement contradictoire avec la notion même de menu... jusqu'au moment où nous avons compris que ce que le rédacteur avait appelé menu était ce que nous appelions masque de saisie. Le programmeur avait tout simplement oublié de décrire la nature même de ce dont il parlait.
La documentation souffre d'un autre problème : même si elle existe, il convient de la mettre à jour au fur et à mesure de l'évolution du projet ou des modifications dues à la maintenance. Un programmeur de maintenance consciencieux devrait rajouter à la documentation tous les points qu'il aurait souhaité y trouver et qu'il a dû reconstituer à partir du programme à grand-peine... c'est rarement le cas.
Ce problème est rendu encore plus aigu par le fait que la plupart des méthodes confondent démarche de conception et documentation. Lors de la conception, un certain nombre de documents sont produits qui expriment l'état d'avancement du projet et de la réflexion des concepteurs. Concrètement, une méthode se traduit de façon visible par la production de ces documents. Pour les concepteurs, cette documentation devient en quelque sorte obsolète une fois le projet entré en phase de codage, et les programmeurs éprouvent rarement le besoin de la remettre à jour en cas de modification apparaissant tard dans le processus de développement. La documentation nécessaire par la suite doit se donner pour but de permettre à une personne nouvelle dans un projet de comprendre sa structure et les décisions de conception qui ont été prises ; il n'y a aucune raison a priori que celle-ci coïncide avec les documents de la méthode, qui reflètent plutôt l'historique de la conception. Mais comme cette seconde documentation existe rarement, la première en tient lieu.
La documentation est donc rarement complète, appropriée et à jour. C'est un point faible du développement logiciel, mais elle est indispensable pour porter l'information qui ne peut s'exprimer directement dans le texte du programme. Si l'on pouvait exprimer tout le contexte culturel du programmeur dans le programme lui-même, et si le langage était d'assez haut niveau pour correspondre directement aux éléments de la conception, alors la documentation de programmation deviendrait inutile.

## Vers l'auto-documentation

Encore une fois, ce sont les éléments tellement «évidents» (pour le concepteur initial) qu'ils ne figurent nulle part qui sont la cause d'une grande partie des difficultés de maintenance. Par conséquent,
    Tout le savoir du programmeur doit être exprimé dans le texte du programme.
De quelle façon peut-on formuler ce savoir? La première idée qui vient à l'esprit est d'utiliser dans ce but les commentaires. Nous parlons ici bien entendu des commentaires algorithmiques ; les en-têtes de modules (auteur, historique, etc.) sont indispensables et jouent un rôle différent. Hélas, ils souffrent des mêmes défauts que la documentation séparée, hormis le fait qu'ils figurent textuellement dans le corps de programme, ce qui facilite leur mise à jour simultanée lors de modifications ; en particulier, il est possible de modifier le programme sans mettre à jour les commentaires correspondants. En cas de désaccord, le programmeur croira toujours le code contre le commentaire. [Lan91] conseille même, lorsque l'on a à intervenir dans un code écrit par quelqu'un d'autre, d'ignorer systématiquement tous les commentaires : en effet, si l'auteur a fait une erreur, le commentaire risque d'induire le lecteur dans la même erreur! On peut même dire qu'un commentaire est un aveu de faiblesse de la part du programmeur : s'il éprouve le besoin de clarifier les choses, c'est qu'il n'a pas écrit son programme de façon qu'il soit directement compréhensible.
    Le commentaire est la plus mauvaise forme d'expression du savoir du programmeur.
Les possibilités de typage très rigoureux d'Ada ouvrent une nouvelle voie : exprimer une partie du savoir sous une forme compilable, donc vérifiable par le compilateur. Supposons par exemple que nous voulions un type destiné à compter quelque chose, et considérons les déclarations suivantes :
Selon toute vraisemblance, le code généré par ces quatre déclarations sera exactement le même ; il n'est donc pas question ici de considérer des différences d'efficacité. La déclaration de Compte_1 ne nous apporte aucune information supplémentaire. Compte_2 nous montre que le programmeur voit effectivement son type comme un compteur, puisqu'il le dérive d'un type qui ne peut intrinsèquement pas être négatif. Compte_3 nous apporte une information supplémentaire : c'est un type dérivé de Comptable ; il porte donc une dépendance conceptuelle à ce type (en particulier, ceci nous informe – et le compilateur vérifiera – que Compte_3 ne peut pas s'étendre au-delà de Comptable). Il s'agit cependant d'un type de nature différente, puisque c'est un type dérivé ; alors qu'avec Compte_4, il ne s'agit que d'un sous-type, donc d'entités de même nature que Comptable.
On voit bien sur cet exemple que le fait d'avoir plusieurs façons d'exprimer la même chose devient une aide, car cela permet de transmettre un renseignement supplémentaire au lecteur : s'il y a un choix, le fait d'adopter une solution plutôt qu'une autre est porteur d'information. Plus les possibilités de typage du langage seront riches, plus il y aura de choix d'implémentations possibles, donc d'information transmise à la fois au compilateur (qui pourra faire des vérifications plus précises) et au lecteur. Ceci fonctionne remarquablement bien... à condition que le programmeur ait effectivement utilisé toutes les possibilités du typage.
Si l'on adopte cette façon de faire, il ne doit plus rester dans un programme que deux formes de commentaires : les en-têtes de module (de format standardisé, selon les normes du projet), et l'expression d'invariants, éléments supposés toujours vrais à un endroit du programme. Et encore, ces derniers peuvent faire l'objet d'une expression dans le langage (et donc d'un contrôle automatique). Il suffit de disposer de la procédure suivante (on peut en faire des versions plus perfectionnées) :
On peut alors exprimer les invariants sous la forme :
Si l'invariant n'est pas satisfait, il y aura levée de l'exception Program_Error : encore une fois, les hypothèses du programmeur peuvent être vérifiées automatiquement par le langage.
En résumé, les possibilités de typage d'Ada permettent de transmettre beaucoup plus d'information au lecteur que d'autres langages. Ce n'est bien entendu pas une garantie ; mais en pratique, on s'aperçoit rapidement, lorsque l'on reprend un programme écrit par quelqu'un d'autre, à quel point l'on peut faire confiance aux déclarations. Et de toutes façons, la situation ne peut être pire qu'avec les autres langages où ce type de documentation n'est pas possible. Cela demande plus de travail au programmeur? Certes, mais pas plus que de mettre des commentaires, pour un niveau de sécurité bien meilleur ; et comme le remarquait [Lan91] :
    Un programmeur à l'esprit ordonné écrit des instructions limpides. un programmeur à l'esprit embrouillé fait d'obscurs commentaires.

## Tu ne coderas point avant d'avoir conçu

Une fois que nous avons admis que notre connaissance des objets que nous manipulons doit s'exprimer dans le langage, nous pouvons nous poser la question : ne peut-on aller plus loin? Ne peut-on également utiliser Ada pour exprimer nos décisions de conception, avant même d'atteindre la phase de codage? Nous touchons là à un principe sacro-saint : on ne doit pas commencer à écrire du code tant que les phases de conception ne sont pas totalement terminées. Mais quelle est l'origine de ce principe? Revenons d'abord sur la démarche de conception elle-même.
Au départ, on trouve un problème particulier à résoudre, dans le cadre du projet. Mais ce problème n'est souvent qu'un cas particulier d'un besoin plus général, qu'il nous faut identifier. Enfin, nous devons trouver une implémentation répondant au besoin.
À l'aube de l'humanité (c'est-à-dire avant l'apparition d'Ada), le langage de programmation n'était considéré que comme une suite d'instructions données à une machine. Autrement dit, il ne permettait d'exprimer que la troisième étape. Le programmeur en revanche n'était préoccupé que de la première : trouver une solution à son problème. En codant directement ce qui lui paraissait une solution, il court-circuitait totalement la deuxième partie : la vraie réflexion sur la nature du problème.
Prenons par exemple un dispositif destiné à faciliter l'accordage d'un piano. Après avoir mesuré la fréquence d'une note jouée, il faut rechercher la note théorique la plus proche, et indiquer l'écart entre la fréquence mesurée et la fréquence théorique de la note (besoin particulier). Ce besoin n'est en fait qu'un cas particulier du problème général de situer une valeur par rapport à une fonction tabulée. La solution à un problème d'informatique n'est jamais unique, par conséquent il existera de nombreuses façons différentes d'implémenter cette notion abstraite. Dans notre exemple, il pourra exister différentes formes d'organisations de la table des valeurs et plusieurs algorithmes de recherche possibles (les implémentations). Ici, le programmeur (qui travaillait en Pascal, mais venait de FORTRAN) n'avait songé à utiliser que l'outil normal à tout faire de FORTRAN : la boucle DO (ou for en Pascal). Il avait donc codé un algorithme de parcours séquentiel de la table, et trouvait son code insuffisamment performant (il fallait reconnaître les notes en temps réel). La table des fréquences des notes étant naturellement triée, il fallait bien sûr utiliser une recherche dichotomique. L'algorithme trouvait ainsi la bonne note (parmi 128) en 7 comparaisons (au pire) au lieu de 64 (en moyenne) avec la recherche séquentielle. Soit une accélération de presque un facteur 10...
Le passage trop rapide du problème particulier au codage risque donc de faire passer à côté d'autres solutions plus performantes, ou bien de faire adopter une solution à court terme impossible à faire évoluer. La règle habituelle, qui interdit au programmeur de coder avant d'avoir formalisé son problème, sert à l'obliger à passer par cette deuxième étape de reformulation. Le point fondamental n'est donc pas de concevoir avant de coder : c'est de formaliser le problème avant d'adopter une implémentation particulière.
Faute de passer par cette étape, il n'est plus possible de faire la différence entre un concept et son implémentation. Et comme chaque couche de logiciel s'appuie sur d'autres couches plus profondes, des dépendances transitives aux implémentations vont s'établir à travers tous les niveaux, conduisant à de véritables fuites d'abstraction. Un exemple typique de ceci est un problème auquel se trouvent confrontés les développeurs utilisant DBase III sur PC. La séquence de code pour faire passer une imprimante en mode double largeur se termine par l'envoi du caractère NUL. Or, le mode d'emploi de DBase III spécifie bien «qu'il n'est pas possible d'envoyer un caractère NUL sur l'imprimante». La raison en est évidente à quiconque a pratiqué les langages de programmation : DBase est écrit en C, et c'est l'habitude en C d'utiliser le caractère NUL comme terminateur de chaîne, ce qui interdit de le faire figurer dans une chaîne. Il n'empêche qu'il n'est pas possible d'écrire en double largeur dans une application DBase à cause d'un choix de représentation d'une structure de donnée particulière dans le langage qui a servi à écrire le compilateur DBase! L'origine du problème se trouve trois niveaux d'abstraction plus bas, mais C est typiquement un langage qui «fuit» : le besoin abstrait est celui d'une chaîne de caractères, mais le comportement des abstractions est entièrement gouverné par le choix de représentation sous-jacent : impossible d'ignorer qu'une chaîne de caractères est en fait un pointeur sur un octet, ni qu'un tableau n'est en fait que l'adresse de son premier élément.
* ↑ Pour que l'histoire soit complète, notons que ce programmeur était venu nous voir à l'origine pour savoir si un case était plus performant qu'un if. Bien sûr, on ne peut espérer mieux à ce niveau qu'un gain de quelques pour-cents de performance dans le meilleur des cas. Bel exemple de ce que nous avons dit à propos de la recherche d'efficacité...
* ↑ Signalons au passage à ceux qui ne voient pas le problème ici qu'en Ada, la valeur interne d'une variable tableau n'est généralement pas l'adresse de son premier élément.

## Le parcours horizontal du V de développement

Les différentes méthodes de conception recouvrent différentes façons de formaliser les problèmes. Or Ada nous offre, au moyen des spécifications indépendantes des implémentations, la possibilité d'exprimer la formulation abstraite sous une forme compilable, c'est-à-dire vérifiable (dans une certaine mesure) au moyen d'un outil automatique, le compilateur. Cette possibilité a un effet considérable sur tout le processus de développement.
On représente habituellement les étapes de la conception au moyen du «V» de développement, qui représente le modèle dit de la « chute d'eau » (Figure 9).
On part du cahier des charges, dont on tire l'analyse générale, puis la conception détaillée et le codage. On ne passe à l'étape suivante qu'après avoir validé l'étape précédente. Une fois le codage effectué, on va vérifier en remontant en sens inverse : au codage correspond la mise au point, à la conception détaillée correspondent les test unitaires, à la conception générale correspond l'intégration, et enfin au cahier des charges correspond la recette.
Le principal problème de ce modèle est qu'il est extrêmement sensible aux erreurs. Si l'on a commis une faute au niveau de la conception générale, elle ne sera trouvée qu'au moment de l'intégration, et sa correction nécessitera une nouvelle itération à travers tout le cycle de développement (d'où la bombe!). On a tenté de pallier cette difficulté en multipliant les niveaux de contrôle, mais il semble utopique d'espérer éliminer totalement la possibilité d'erreurs.
En revanche, si nous considérons les différentes étapes de conception comme des vues de plus en plus concrètes de l'expression d'un problème, nous pouvons à chaque étape formaliser la conception au moyen de spécifications Ada. Ces spécifications sont compilées, et donc vérifiées. On n'aborde les étapes ultérieures qu'après avoir vérifié que les différents éléments de plus haut niveau peuvent bien s'assembler de la façon souhaitée. On va donc en quelque sorte effectuer l'intégration avant même d'aborder la conception détaillée! Ceci aboutit à ce que nous appelons le parcours horizontal du «V» de développement, représenté sur la figure 10. Nous verrons, dans la troisième partie de cet ouvrage, comment cette façon de faire peut être systématisée pour développer des applications par maquettage progressif.
Ces considérations peuvent paraître théoriques : il n'en est rien, et l'une des remarques qui revient le plus souvent lorsqu'on interroge des responsables de projets qui sont récemment passés à Ada est la mention d'un véritable effondrement des temps d'intégration. Ce n'est pas étonnant : le langage est un gardien tellement vigilant qu'il empêche la construction de pièces qui ne pourraient s'assembler par la suite.

## La nouvelle documentation de maintenance

À partir du moment où nous disposons d'un langage suffisamment puissant pour permettre l'expression directe et vérifiée de nos conceptions, la documentation traditionnelle est-elle appelée à disparaître? Certainement pas, mais son rôle va être modifié.
Tout d'abord, la documentation de conception restera, car il importe de garder l'historique des choix principaux qui ont conduit à la structure actuelle du projet. La documentation réellement mise en cause est la documentation de codage. Elle ne doit plus être une description des algorithmes effectivement choisis, puisque ceux-ci sont mieux décrits par le code, ou alors se situer à un niveau nettement supérieur (description au moyen de langages formels) : elle doit essentiellement servir à tracer les raisons des choix qui ont abouti à la sélection de telle ou telle politique d'implémentation.
On ne le répétera jamais assez : la solution à un problème d'informatique n'est jamais unique. Une excellent habitude consiste d'ailleurs, lorsqu'un choix d'implémentation paraît évident, à systématiquement chercher une deuxième possibilité, quitte à la réfuter immédiatement : on est ainsi assuré d'avoir fait un choix délibéré, et non un choix implicite, c'est-à-dire provenant simplement d'un manque de réflexion sur le problème.
Il arrive également fréquemment que l'on s'aperçoive après coup qu'un choix d'implémentation n'était pas le bon, et que l'on doive revenir sur une décision antérieure, parce que l'on n'avait pas envisagé certaines difficultés. Le rôle de la documentation de bas niveau va être essentiellement de garder la trace de ces choix, des raisons qui les ont motivés, et en cas de retour arrière, des raisons du choix initial, des difficultés rencontrées, et des motivations de la remise en cause. Ceci apportera au lecteur ultérieur une meilleure compréhension du problème, et surtout lui évitera de refaire lors d'une opération de maintenance les erreurs commises au moment du développement. Combien de fois n'avons-nous pas eu l'impression que le concepteur initial était passé à côté d'une solution «évidente», alors qu'en fait celle-ci ne pouvait fonctionner pour des raisons qui n'apparaissaient que bien plus tard!
En résumé, on peut dire que le rôle de la documentation de maintenance n'est plus de décrire la solution adoptée, mais les raisons et les différents compromis qui ont conduit à préférer cette solution à d'autres. En particulier, le développeur consciencieux mentionnera les évolutions possibles qui peuvent amener à reconsidérer les choix : par exemple, une solution a pu être préférée à une autre pour des raisons d'efficacité, mais l'arrivée d'un nouveau matériel plus performant peut remettre en cause ce choix.

## Exercices

* Reprendre la documentation de maintenance d'un logiciel réel et la critiquer en fonction des critères de ce chapitre. Décrit-elle les principes ou les détails de la solution? Un nouveau venu y trouverait-il rapidement les éléments lui permettant de comprendre la structure du projet? Les informations n'auraient-elles pas pu s'exprimer dans le langage? Etc.
* Expliquer pourquoi il serait bon d'écrire le mode d'emploi «utilisateur» d'un programme avant d'écrire le programme.

# Deuxième partie

* Avant propos, préface, introduction
* Une présentation rapide du langage Ada
* Langages et méthodes
* Méthodes avec Ada
* Composants logiciels
* Organisation de projet et choix fondamentaux
* Une méthode orientée objet pour les projets
* Conclusion
* Annexes
* Bibliographie
* Glossaire
* Avant propos, préface, introduction
* Une présentation rapide du langage Ada
* Langages et méthodes
* Méthodes avec Ada
* Composants logiciels
* Organisation de projet et choix fondamentaux
* Une méthode orientée objet pour les projets
* Conclusion
* Annexes
* Bibliographie
* Glossaire
[ Modifier le sommaire ]
Nous avons vu dans la première partie le rôle important joué par les langages du point de vue méthodologique. Dans cette deuxième partie, nous passons en revue les différentes grandes classes de méthodes de conception, en montrant comment Ada leur fournit un soutien actif, et ceci quelle que soit la méthode.
Car là est bien l’enjeu et l’ambition d’Ada : être le langage qui soutient toutes les méthodes.

# Les méthodes structurées
## Principes de la méthode

La programmation structurée fut historiquement la première formalisation de l’analyse par décomposition descendante. Le principe de cette méthode est simple : on considère qu’un programme est constitué d’une suite d’actions élémentaires. On décompose donc au plus haut niveau le problème à résoudre en actions individuelles que l’on suppose résolues; on analyse ensuite chacune de ces actions en la décomposant sous forme d’actions de plus bas niveau, et ainsi de suite jusqu’à obtenir des actions considérées comme triviales et susceptibles de s’écrire directement dans le langage de programmation. La programmation structurée peut donc être définie comme une méthode dont le critère de décomposition horizontale est l’enchaînement des actions, et le critère de décomposition verticale la généralité des actions.
Le langage dont la philosophie se rapproche le plus de la programmation structurée est Pascal. On peut voir l’arbre de décomposition de la programmation structurée comme l’arbre d’imbrication des procédures d’un programme Pascal : un programme principal, qui se décompose en procédures de premier niveau, elles-mêmes décomposées en procédures de deuxième niveau, etc.
Ada, possédant un sous-ensemble équivalent à Pascal, permet bien entendu cette approche des problèmes. Il lui apporte cependant un perfectionnement important : il est possible d'imbriquer logiquement des modules, tout en conservant la possibilité de les compiler séparément. Le Pascal standard ne possède pas de mécanisme de compilation séparée. Les Pascal «étendus» (tels que Turbo Pascal par exemple) possèdent de tels mécanismes, mais à condition de ne pas conserver la notion d'imbrication. Le mécanisme des corps séparés permet, comme nous allons le voir dans l'exemple suivant, de compiler séparément des modules tout en conservant l'imbrication logique.

## Exemple en programmation structurée

Supposons que nous voulions imprimer un triangle de Pascal (le mathématicien, pas le langage !). Il s'agit d'un triangle de nombres, se présentant comme à la figure 11.
Chaque élément d'une ligne s'obtient en faisant la somme de l'élément situé au-dessus de lui et de l'élément situé au-dessus et à gauche, le dernier élément à droite (qui n'a pas d'élément supérieur) étant toujours un 1. Pour résoudre le problème global (imprimer le triangle), il suffit de savoir résoudre deux sous-problèmes : calculer une ligne, et l'imprimer. Nous avons également besoin d'une structure de données pour communiquer entre ces deux étapes : la Ligne. Le programme principal peut s'écrire :
Nous pouvons compiler ce programme pour vérifier qu'il ne contient pas d'incohérence à ce niveau de décomposition, alors que nous n'avons encore pris aucune décision de conception en ce qui concerne les niveaux inférieurs.
    La clause is separate permet de compiler séparément un corps de sous-programme, tout en gardant toutes les propriétés (notamment la visibilité et le contrôle des types) qu'il aurait s'il se trouvait effectivement à la place de la clause.
Ensuite, nous devons descendre d'un niveau en nous préoccupant de savoir comment nous allons réaliser les procédures Calculer_ligne et Imprimer_ligne. Comme les lignes sont calculées dans l'ordre, lorsque Calculer_ligne est appelé la variable Ligne contient la ligne précédente (d'ordre Numéro-1). Nous pouvons utiliser cette information pour calculer la ligne d'ordre N, à condition de calculer de droite à gauche, afin de ne pas «écraser» des valeurs dont nous avons encore besoin. Le premier élément valant toujours 1, il est inutile de le recalculer, et nous savons que le dernier vaut également toujours 1. Nous pouvons donc écrire :
Au niveau d'abstraction supérieur, nous n'avions pas prévu de traitement spécial pour les premières lignes; nous devons donc vérifier ce qui se passe, pour éventuellement faire un cas spécial. Si Numéro vaut 1, la procédure met la valeur 1 dans Ligne(1), et la boucle n'est pas effectuée (Numéro-1 vaut 0, qui est plus petit que 2). Le traitement est donc correct. Si Numéro vaut 2, le premier élément est intouché, on met 1 dans le deuxième élément, et la boucle n'est pas effectuée : le traitement est encore correct. Enfin, à partir de 3, l'algorithme normal s'applique. Il n'y a donc pas à faire de cas particulier, ce qui n'était pas évident au départ. Pour imprimer, nous pouvons représenter le comportement souhaité de la façon suivante : écrire la ligne d'ordre I revient à écrire les I premiers éléments, suivis d'un retour à la ligne. Noter qu'à ce niveau, nous ne savons pas comment écrire un élément : nous reportons ce problème à l'étape suivante. Nous pouvons donc écrire :
Il ne nous reste plus qu'à analyser comment imprimer un élément simple. Ada fournit des entrées-sorties prédéfinies sur le type Integer dans le paquetage Ada.Integer_Text_IO.
    Ce paquetage n'est en fait qu'une pré-instanciation du paquetage générique Text_IO.Integer_IO, qui permet d'avoir des entrées-sorties sur n'importe quel type entier. Il n'était pas obligatoire en Ada 83.
Nous allons donc utiliser ce composant :
Bien sûr, cet exemple est un peu exagérément détaillé, car son but est de montrer la démarche : pour imprimer un triangle de Pascal, on suppose que l'on sait calculer et imprimer une ligne. Pour imprimer une ligne, on suppose que l'on sait imprimer un élément. Chaque étape s'appuie sur des problèmes plus spécifiques, que l'on suppose résolus, puis que l'on implémente. Inversement, chaque étape de la résolution s'appuie sur une vue de plus en plus précise, mais de plus en plus étroite du problème : Imprimer_ligne ne traite que de l'impression, sans se préoccuper de la façon dont la ligne a été calculée. Imprimer s'occupe uniquement de l'impression d'un élément, sans savoir où ni pourquoi on le lui demande. Enfin, chaque niveau s'exécute séquentiellement dans le temps, et sait dans quel ordre il est appelé : c'est ce qui permet à Calculer_ligne d'utiliser le résultat du calcul précédent.

## Critique de la méthode

Lors de son émergence, la programmation structurée représentait un grand progrès par rapport aux méthodes, ou plutôt à l'absence de méthodes, existantes. Cependant, avec les progrès de l'informatique et l'extension du domaine des problèmes susceptibles d'être résolus de façon informatique, elle bute sur un certain nombre de difficultés que nous allons résumer maintenant.
Cette méthode conduit à une topologie de programme purement arborescente, comme illustrée par la figure 12. Chaque module est fait spécifiquement en fonction des exigences du niveau supérieur : ceci conduit à une architecture de programme très monolithique, semblable à un puzzle où chaque morceau est destiné à entrer à un endroit précis pour lequel il a été conçu. Il y a donc un couplage spatial entre les modules : comme un module «voit» tous les éléments qui lui sont supérieurs, il n'est pas possible de recopier un module dans une autre branche du programme. De plus, le développeur sait quand, dans le déroulement du programme, son module sera appelé : il peut donc faire des suppositions sur l'état des variables, ce que fera le prochain module appelé, etc. Ceci entraîne également un couplage temporel des modules : le même sous-programme, appelé depuis un endroit qui n'était pas celui initialement prévu, risque de ne plus fonctionner normalement.
Une autre conséquence de l'aspect monolithique des programmes ainsi développés est qu'il est très difficile d'autoriser les compilations séparées. Pascal en standard ne les permet pas. Si de nombreuses extensions de Pascal permettent de le faire, c'est toujours en répétant dans chaque module les déclarations globales qu'il est censé voir; une modification d'un de ces objets globaux doit être répercutée dans tous les modules qui l'utilisent, et le langage ne garantit absolument pas la cohérence des différentes déclarations. Tout ceci entraîne que les modules développés pour une application peuvent difficilement être réutilisés dans un contexte différent. Même à l'intérieur d'une même application, on trouve souvent des fonctionnalités très voisines, mais légèrement différentes, ce qui conduit à une duplication de codes quasi identiques. Ceci est d'ailleurs une conséquence inévitable de la hiérarchisation. C'est ce que nous allons montrer maintenant.
Il arrive souvent qu'une fonctionnalité identique se retrouve à différents endroits d'un même programme; on peut imaginer par exemple que les modules marqués X et Y sur la figure 12 aient tous deux besoin d'effectuer des lectures sur le terminal, et que l'on souhaite passer par une procédure permettant d'éditer la ligne par exemple. Dans la décomposition strictement arborescente de la programmation structurée, ceci s'exprimerait par le fait que deux modules, a priori totalement indépendants, expriment tous deux dans le cours de leurs actions la sous-action «lire une ligne». En suivant strictement la méthode, il faudrait écrire le même sous-programme deux fois dans deux modules distincts. C'est évidemment peu souhaitable; ce n'est pas tant la duplication de l'écriture qui est gênante (un bon éditeur de texte le fait en quelques touches) ni même l'espace mémoire gaspillé par la duplication du code (les machines ont des espaces mémoire importants de nos jours) : le problème réel vient de la maintenance. Si une modification doit être faite dans un de ces modules dupliqués, il faut la répercuter dans toutes les copies, et le risque est grand d'en oublier. On peut ainsi voir des erreurs corrigées à un endroit réapparaître soudainement à un autre; on risque également des problèmes d'incohérence : imaginez un programme où le caractère d'effacement serait Del par endroits et Backspace à d'autres!
La solution généralement adoptée consiste à remonter la fonctionnalité dans l'arbre de décomposition jusqu'à un endroit où elle sera visible de tous les modules qui l'utilisent, c'est-à-dire presque toujours jusqu'à la racine. Le module se situe alors à un niveau global qui ne correspond plus à la décomposition logique. Le même phénomène se produit pour les structures de données, lorsque deux parties du programme doivent à travailler sur une même variable. Les règles de visibilité exigent alors que la variable soit «remontée» dans l'arbre au moins jusqu'au nœud commun aux deux branches concernées.
On s'aperçoit donc qu'en pratique, la belle arborescence théorique décrite par la méthode dégénère pour aboutir à une topologie illustrée par la figure 13. On a alors des arborescences partielles, avec un regroupement important de fonctions et de structures de données au niveau de la racine. Remarquons qu'il s'agit d'une première entorse aux principes de la programmation structurée : des entités sont déclarées à un niveau où elles ne correspondent pas à une action élémentaire du niveau immédiatement supérieur.
Ce regroupement a une conséquence extrêmement néfaste : les éléments ainsi propulsés au niveau global sont alors visibles non seulement depuis les modules concernés, mais également depuis l'ensemble du programme. Si par exemple une variable ne doit être (logiquement) modifiée qu'en passant par l'intermédiaire d'une procédure de contrôle, on n'a plus aucune garantie qu'un programmeur «astucieux» n'a pas trouvé plus commode d'accéder à la variable directement. Il devient donc extrêmement difficile de contrôler quelle partie du programme utilise quelle autre partie, puisque toutes sont visibles entre elles. On a bien décomposé le programme en unités de tailles suffisamment petites pour être gérables, mais on a établi des liens de dépendance qui croissent rapidement, jusqu'à dépasser ce que l'on peut gérer; on ne sait plus alors très bien qui modifie quoi ou qui appelle qui dans le programme.
Notons pour terminer que la définition même de la programmation structurée suppose l'ordonnancement séquentiel des actions. Il n'est donc pas possible de l'utiliser pour définir des systèmes parallèles. On peut pallier cet inconvénient en définissant un système parallèle comme un ensemble de processus, individuellement séquentiels, mais s'exécutant en parallèle. La programmation structurée peut alors s'appliquer à l'analyse de chacun des processus, mais bien sûr l'analyse du comportement global requiert une méthode appropriée au parallélisme.
Ce problème de l'analyse des systèmes parallèles à été longtemps ignoré, ou tout au moins confiné à quelques domaines particuliers, car les langages courants n'offraient pas de possibilité de programmation parallèle, ce qui n'encourageait pas les développeurs de systèmes d'exploitation à développer le parallélisme, ce qui n'encourageait pas le développement de langages parallèles, etc. La situation change aujourd'hui, car la demande de toujours plus de puissance de calcul ne pourra être satisfaite qu'avec des machines parallèles, ce qui a amené l'introduction de processus légers (threads) au niveau des systèmes d'exploitation... ainsi bien entendu que l'apparition d'un langage de programmation industriel complet offrant, au même titre que ses autres fonctionnalités, un modèle simple de programmation parallèle.
* ↑ Qui n'a jamais entendu des remarques du genre : «Pas la peine de remettre à jour la variable X, elle est écrasée par le prochain module»?
* ↑ Le même reproche peut d'ailleurs être adressé à C, à FORTRAN (avec les COMMON), etc.
* ↑ Ada, pour ceux qui ne l'auraient pas reconnu !

## Ada et la programmation structurée

Pascal avait été conçu spécifiquement pour mettre en œuvre les principes de la programmation structurée. Ada a poursuivi dans cette voie, tout en ajoutant les éléments nécessaires à un langage d'envergure industrielle.

### Utilisation des sous-programmes

Ainsi que nous l'avons vu, Ada permet comme Pascal de traduire la hiérarchie des actions résultant de la programmation structurée sous forme d'imbrication de procédures, mais offre de plus la possibilité de compiler séparément les corps. On peut certes s'affranchir de la hiérarchie stricte en regroupant certains sous-programmes en paquetages; mais la hiérarchisation est toujours possible. On a prétendu [Cla80] que cette possibilité d'imbrication était inutile, voir même nocive. Les langages comme C/C++ n'autorisent d'ailleurs pas l'imbrication de sous-programmes. Il est en effet toujours possible de mettre deux sous-programmes « à côté » l'un de l'autre, au lieu de les imbriquer, avec sensiblement les mêmes fonctionnalités :
La différence vient encore de la maintenance : si un sous-programme semble appelé à tort et qu'il est déclaré au niveau le plus externe, on doit jeter la suspicion sur l'ensemble du programme ; si au contraire il est déclaré à l'intérieur d'un autre sous-programme, l'appel incorrect a forcément lieu depuis l'intérieur de cet autre sous-programme, réduisant considérablement le champ d'investigation nécessaire pour retrouver l'erreur.
Ada permet donc de traduire la structure de la conception directement dans la structure du programme, tout en conservant la possibilité de compilation séparée. Une difficulté est que si l'on modifie une couche haute, il faudra recompiler tous les éléments (séparés) de plus bas niveau; mais ceci est un problème inhérent à la méthode, où une modification des couches hautes a des conséquences sur toutes les couches inférieures; le langage ne fait que répercuter les propriétés de la méthode.

### Utilisation des paquetages

En programmation structurée, les paquetages servent essentiellement à regrouper soit des structures de données, soit des structures de programmes. On trouvera donc, selon la classification établie par Booch, essentiellement des « collections de données » et des « collections de sous-programmes ». Les principes de la programmation structurée impliquant la visibilité des données, on trouvera rarement des parties privées dans les paquetages utilisés avec cette méthode.
* Collection de données
* Collection de sous-programmes

### Utilisation des exceptions

Les canons de la programmation structurée ne connaissent pas la notion de déroutement; l'utilisation des exceptions sera donc généralement limitée si l'on applique strictement la méthode. D'ailleurs, les règles de programmation associées aux méthodologies en programmation structurée (surtout pour les systèmes à haute sécurité) déconseillent ou interdisent l'utilisation des exceptions. L'avantage de la programmation structurée est qu'elle permet de suivre directement l'exécution du programme, propriété qui n'est plus vérifiée en présence d'exceptions. Malgré tout, il faut bien définir une politique d'erreur (nous en discuterons dans la quatrième partie); mais on utilisera plutôt une technique par codes de retour.
On ne peut cependant ignorer totalement les exceptions : d'une part, les composants logiciels n'ont pas d'autre solution que de les utiliser en cas d'impossibilité de rendre le service demandé; d'autre part, une anomalie du programme peut conduire à la levée d'une exception prédéfinie. On évitera donc en général de lever des exceptions, mais on devra prévoir quand même des traite-exceptions de sécurité. En cas de levée d'exception imprévue, on appellera la procédure de gestion normale des situations erronées.

### Utilisation des génériques

Nous avons vu que la réutilisation de code en programmation structurée impliquait soit de le dupliquer, ce qui posait des problèmes de maintenance, soit de le remonter à un niveau global, ce qui violait la décomposition logique. Les génériques d'Ada offrent une solution élégante à ce problème. Supposons par exemple que nous ayons besoin de la procédure Lire_Ligne à plusieurs endroits du programme; cette procédure a pour mission de lire une ligne au terminal (en la complétant avec des espaces, éventuellement dans une fenêtre, avec des fonctions d'édition...) et de la mettre dans une certaine variable de la procédure englobante. Initialement, le programmeur avait écrit :
Lorsqu'apparaît le besoin de réutiliser cette procédure dans un autre contexte, le programmeur ne duplique pas le code, mais le transforme en générique :
Il peut ensuite, dans l'ancienne procédure ainsi que dans toutes celles qui en ont besoin, écrire :
Ainsi, chaque procédure utilisatrice dispose de sa propre procédure locale, respectant la structure logique, mais le code n'est écrit qu'une fois, et toute modification de maintenance qui lui serait apportée sera automatiquement reportée dans toutes les instanciations. Les génériques permettent donc d'apporter à la programmation structurée une forme de réutilisation qui ne perturbe pas la structure totalement hiérarchique de l'application.

### Utilisation du parallélisme

Nous avons vu que les méthodes en programmation structurée se prêtent mal au développement de systèmes parallèles. Il faut cependant parfois définir des activités parallèles dans des systèmes développés en programmation structurée. Dans ce cas, on décompose le système en tâches indépendantes dès les premières étapes de la conception, ce qui permet ensuite de poursuivre l'analyse de chacune d'elles en programmation structurée «normale».
On trouvera donc surtout des tâches Ada définies directement dans des paquetages de bibliothèque, se comportant essentiellement comme des programmes principaux multiples. En Ada 95, on pourra également utiliser le modèle d'exécution distribuée qui permet effectivement d'avoir plusieurs programmes principaux faiblement couplés.
Notons encore que pour un système développé en programmation structurée ayant besoin de parallélisme, il est particulièrement utile de disposer d'un langage offrant un mécanisme de tâches intrinsèque : le surcroît de complexité est alors négligeable. Autrement, il faut soit s'appuyer sur les primitives d'un exécutif temps réel particulier, mais on y perd la portabilité, soit écrire soi-même son propre système de gestion des tâches, mais l'effort supplémentaire est loin d'être négligeable, et il risque souvent d'imposer des contraintes à travers toutes les couches du logiciel. C'est ainsi que les normes de programmation de systèmes graphiques tels que Windows ou XWindow demandent à toutes les applications utilisatrices de se bloquer volontairement périodiquement, pour permettre au système de « reprendre la main ».

## Exercices

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

# Les méthodes orientées objet
## Principes des méthodes orientées objet

Les méthodes orientées objet sont nées du désir de remédier aux problèmes de la programmation structurée, sans en perdre bien entendu les avantages. Le principal mérite de la programmation structurée vient de ce qu'elle procure une méthode rationnelle, progressive, de décomposition des programmes. Celle-ci s'effectue de façon descendante, ce qui permet de mettre en place la structure générale sans se soucier des détails d'implémentation. Mais son problème essentiel vient du couplage des différents éléments de la décomposition entre eux. Ce que l'on cherche donc, c'est une méthode descendante (par raffinements successifs), mais produisant des modules à faible couplage. De plus, les modules doivent représenter chacun une et une seule entité logique, c'est-à-dire qu'ils doivent être à forte cohésion (il ne doit pas être possible de diviser un module en deux tout en conservant le faible couplage).

### Définition

Pour remédier aux défauts de la programmation structurée, il convenait de remettre en cause son fondement même : la décomposition en actions. L'origine de cette réflexion se trouve dans un article de Parnas [Par71] intitulé « À propos des critères à utiliser pour décomposer un système en modules ». Il s'agissait bien d'une remise en cause des critères de décomposition.
    Nous proposons d'appeler «orientée objet» toute méthode dont le critère de décomposition horizontale est l'objet, en tant qu'abstraction d'un objet du monde réel.
Mais comment définit-on ce qu'est une abstraction? Prenons un exemple. Il est vraisemblable que vous qui lisez ces lignes possédez une voiture. Vous venez de lire la phrase précédente, et vous n'avez eu aucune peine à saisir sa signification. Pourtant possédez-vous vraiment une «voiture»? Non, vous possédez un certain ensemble de tôles et de plastique que vous identifiez comme appartenant à une classe appelée «voiture»; vous possédez donc un objet qui est une voiture, mais beaucoup d'autres objets ayant peu de rapport avec votre chère auto sont également des voitures. Le terme de voiture est une abstraction, qui permet de regrouper un certain nombre d'entités différentes.
Comment donc reconnaître si un objet donné est une voiture ou non? Pour avoir droit à ce titre, il faut que l'objet en question ait quatre roues, des sièges et un moteur. Nous pouvons donc définir un certain nombre de valeurs qui sont caractéristiques de la classe voiture. Mais ceci ne suffit pas à la caractériser : un avion pourrait très bien posséder les caractéristiques précédentes. Il faut en plus spécifier que la voiture sert à transporter des gens sur la route. Nous devons donc définir, en plus des valeurs, un certain nombre d'actions que la voiture peut effectuer ou subir.
On peut donc définir une abstraction comme une réduction d'une classe d'objets à un ensemble de valeurs et d'opérations communes à tous les objets de la classe, et permettant de la définir entièrement.
Le point important de cette définition est la réduction. D'ailleurs, le terme abstraire ne signifie-t-il pas étymologiquement «retirer de»? Par exemple, une propriété d'une voiture est la composition chimique de l'acier dans lequel a été fabriqué le vilebrequin. C'est une propriété inutile pour la vue de la voiture qu'a le conducteur moyen (mais pas pour l'ingénieur qui a conçu la voiture). L'abstraction va donc consister à extraire de l'énorme masse de propriétés caractérisant les objets un petit nombre d'entre elles qui vont être suffisantes pour l'idée que l'on se fait de l'objet à un moment donné. C'est donc essentiellement ce phénomène de réduction, de projection de l'objet réel dans l'espace de problème, qui va caractériser le phénomène d'abstraction.
Avec cette technique, la définition des objets s'appuie sur les propriétés naturelles de l'abstraction du monde réel. Un programme n'est donc plus une suite d'instructions à faire exécuter par une machine, mais une description d'un certain modèle du monde réel. Il s'ensuit une réorganisation complète du logiciel : si les instructions élémentaires se retrouvent quasi identiques quelle que soit la méthode de décomposition choisie, elles ne sont pas regroupées en modules de la même façon. Il n'est pas étonnant que l'ancêtre de tous les langages à objets, Simula, ait été un langage de simulation : dans quel autre contexte aurait-on plus besoin de représenter les entités du monde réel?
De cette différence fondamentale découlent des propriétés partagées par toutes les approches objet :
* L'encapsulation. L'objet n'est ni une structure de programme, ni une structure de données, mais regroupe en une même entité à la fois les attributs (données) et les opérations (sous-programmes) associés. De plus, la structure interne (implémentation) doit pouvoir être cachée afin de garantir l'indépendance de la vue externe (abstraite) par rapport à la vue interne.
* L'indépendance temporelle. Les objets étant définis par eux-mêmes, on peut définir le comportement d'un objet indépendamment du contexte dans lequel il est appelé.
* L'indépendance spatiale, ou localisation. Tous les aspects relatifs à une même entité sont physiquement dans le même module. Il est donc plus facile de faire des composants autonomes.
* ↑ Appelées méthodes dans le jargon orienté objet.

### Les objets informatiques

Un objet informatique sera donc une entité de modularisation qui rassemblera les structures de données et les opérations pertinentes pour l'utilisation qui est faite de l'objet du point de vue informatique. Différentes façons d'utiliser la notion d'objet sont possibles, que nous allons maintenant passer en revue. Bien qu'il y ait toujours possibilité d'exceptions, l'expérience montre que les «bons objets» correspondent à ces catégories. On vérifiera donc lors de la conception que ce que l'on fait correspond aux critères ci-dessous; sinon, il faut étudier s'il n'y a pas une faute de conception.
* Machine abstraite
* Type de donnée abstrait
* Gestionnaires de données
* Classification des opérations sur objets

### Objets actifs

Le parallélisme d'Ada permet de définir des objets actifs, c'est-à-dire évoluant en parallèle. Ceci s'obtient en associant une tâche à une structure de donnée; plusieurs façons de faire sont possibles, selon le degré de couplage souhaité entre la tâche et la structure de donnée.
Une première solution consiste à attacher manuellement une tâche à un objet. On peut pour cela utiliser un pointeur désignant l'objet comme discriminant de la tâche. Imaginons par exemple que nous voulions représenter un récipient qui fuit : toutes les 10 secondes, il perd un litre d'eau. Nous lui associons un «moniteur de fuite» qui diminue périodiquement la quantité restante :
Avec cette solution la tâche «moniteur» est relativement extérieure à l'objet «compteur» : rien ne nous empêche de ne pas associer de tâche à l'objet, et d'avoir un compteur qui ne fuit pas. Cela peut être le comportement désiré. Si inversement nous voulons être sûrs que tous les compteurs fuient, il faut associer la tâche plus étroitement au compteur. C'est ce que permettent les auto-pointeurs :
Remarquer que c'est le nom du type Compteur qui est utilisé ici comme préfixe de l'attribut 'Access. Tout objet déclaré du type Compteur contiendra une tâche Moniteur, dont le discriminant désignera automatiquement l'objet Compteur dont il fait partie; cette disposition est illustrée en figure 14. Ces autopointeurs ne sont autorisés que dans des types limités : en effet, l'affectation ne pourrait conserver la propriété de l'autopointeur de désigner sa propre structure englobante.
    Le type Compteur utilise le mot clé limited dans sa déclaration d'article. Un tel type est appelé «intrinsèquement limité» : l'affectation est interdite même là où l'on a la visibilité sur le type complet, et le langage garantit que tous les passages se feront par adresse; il n'y a donc jamais duplication. Les autopointeurs ne sont pas limités aux tâches : on peut les utiliser pour lier n'importe quels types.
On peut réaliser ainsi des objets réellement actifs. Noter que l'on pourrait avoir plusieurs tâches associées à un même objet.

### Sémantique de valeur et sémantique de référence

Il existe en fait deux sortes d'objets qu'il importe de bien différencier : ceux à sémantique de valeur et ceux à sémantique de référence. Ils se distinguent par la signification de l'opération d'affectation. Avec une sémantique de valeur, la signification de l'affectation :
est de transférer le contenu de X (sa valeur) dans Y. Une modification apportée par une partie du programme à X n'affecte pas les autres duplications (comme Y). La comparaison d'égalité n'implique que l'égalité des valeurs, que ces valeurs soient contenues dans un même objet ou non.
Nous parlerons de sémantique de référence chaque fois que la valeur abstraite est un identifiant : type accès, indice de tableau, ou toute autre forme de nommage dont les différentes duplications se réfèrent en fait au même contenant. La signification de l'affectation :
devient : X et Y désignent le même objet. Toute modification apportée par un élément du programme au contenu de l'objet désigné par X sera donc visible (mais pas forcément immédiatement) à tous ceux qui utilisent des copies de la référence à l'objet (comme Y). La comparaison d'égalité entre deux variables aura la signification que les deux variables désignent le même objet (ce qui impliquera évidemment l'égalité des contenus). En revanche, deux objets peuvent être trouvés différents même si les contenus sont identiques.
Les types numériques offrent typiquement une sémantique de valeur, alors que les objets implémentés par un type accès offrent une sémantique de référence. Mais il ne faut pas restreindre cette différence à la simple alternative «pointeur ou pas pointeur», encore moins à la question de passage par adresse ou par valeur. Une chaîne de caractères représentant un nom de ficher a typiquement une sémantique de référence! Il est également possible d'obtenir une sémantique de valeur sur un objet abstrait représenté par un type accès si l'on utilise un type limité muni d'une redéfinition de l'égalité et d'un sous-programme de recopie. Le choix entre type privé et type limité sera d'une grande importance pour renforcer le respect de la sémantique voulue.
La spécification de tout type de donnée abstrait doit explicitement mentionner s'il est à sémantique de valeur ou de référence. En aucun cas, un type ne doit fournir de sémantique mixte, comme dans l'exemple suivant :
Un tel objet offrirait une sémantique de référence pour son Champ_1 (c'est un pointeur) et une sémantique de valeur pour son Champ_2. Il n'est plus possible dans ce cas de décrire simplement la signification de l'affectation! Même avec une sémantique de référence, on peut vouloir transférer le contenu d'un objet dans un autre. Un type de données à sémantique de référence doit alors disposer d'une fonction de recopie différente de l'affectation. Rappelons qu'Ada fournit désormais un puissant outil de contrôle du mécanisme d'affectation : un type dérivé du type prédéfini Controlled peut définir une opération, appelée Adjust, qui est appelée automatiquement après toute affectation à une variable de ce type. Il est ainsi possible de créer un type à sémantique de valeur, même si l'implémentation utilise des pointeurs, car l'opération Adjust peut forcer une duplication de la valeur de l'objet.
    Il existe également une opération Initialize, appelée lors de la création de l'objet, et une procédure Finalize, appelée lors de la destruction de l'objet; cette dernière permet de récupérer l'espace mémoire alloué.
Remarquons que la distinction entre sémantique de valeur et sémantique de référence n'est importante que pour les types de données abstraits; les machines abstraites étant par définition uniques, l'affectation n'est pas définie pour elles. Bien que théoriquement rien ne l'impose, les gestionnaires de données doivent absolument être à sémantique de référence. Il serait évidemment désastreux du point de vue de l'efficacité, mais surtout au niveau du principe, d'avoir une sémantique de valeur (ce qui n'empêche pas, comme nous l'avons noté ci-dessus, de fournir une fonction de copie de la structure). En revanche, les éléments gérés par la structure de donnée pourront l'être soit sous forme de valeur (par exemple une liste de valeurs), soit sous forme de référence (par exemple une liste de pointeurs sur des objets). Cette distinction doit faire partie de la définition des propriétés de la structure de donnée.
À noter enfin que la valeur (interne) d'un type de donnée à sémantique de référence est un identifiant d'objet : par conséquent, une instanciation d'un gestionnaire de données gérant des valeurs avec un type à sémantique de référence sera en fait une structure de données manipulant des références. L'instanciation d'une structure de données gérant des références avec un type à sémantique de référence sera une structure manipulant des «références doubles». Ceci peut arriver, mais doit être en général évité, car leur manipulation est délicate à maîtriser, et il est difficile d'en garantir une sémantique claire.
* ↑ Ce point a été identifié par M. Gauthier, dont on trouvera les travaux dans [Gau94].

### Tout n'est pas objet

L'approche objet présente de nombreux avantages, mais cela ne signifie pas nécessairement qu'il faille tout modéliser en termes d'objets. Si l'entité du monde réel que l'on considère se résume à une suite d'actions, alors il est tout à fait souhaitable d'utiliser une approche structurée. Dans une conception orientée objet, ceci se produit dans deux cas particuliers importants : le premier et le dernier niveaux d'analyse.
Le premier niveau correspond à ce que l'on appelle généralement le programme principal. On aura défini les objets qui composent la représentation du domaine de problème, mais le programme principal est chargé d'animer, de faire vivre ces objets en appelant leurs fonctionnalités dans un certain ordre. Son analyse fait donc logiquement appel à la programmation structurée. De même, lorsqu'il s'agit de coder les corps des opérations des objets, il faudra bien utiliser une suite d'instructions du langage de programmation : là encore, une analyse en programmation structurée est appropriée.
On notera que pour un programme pas trop complexe, il n'existe pas de niveau intermédiaire entre le premier et le dernier niveaux : on se retrouve donc avec une analyse entièrement structurée. Autrement dit, l'approche objet ne renie pas la programmation structurée, mais la considère plutôt comme un cas limite «dégénéré» pour des programmes simples.
* ↑ Titre obligeamment emprunté à un article de M. Gauthier [Gau92].
* ↑ Ce qui explique qu'on ait pu utiliser si longtemps – et avec succès – la programmation structurée, jusqu'à l'explosion de complexité des logiciels que nous connaissons actuellement.

### Le choix du critère vertical

Il existe encore un problème important que nous avons laissé en suspens : le choix du critère de décomposition verticale. La notion d'abstraction d'objets du monde réel nous fournit un critère de décomposition horizontale nous permettant de diviser le problème en un ensemble d'objets, représentés par des types de données abstraits (ou des machines abstraites). Cependant, dès que le projet atteint une taille importante, il devient nécessaire d'organiser les objets à plusieurs niveaux. Saisir à la fois tous les aspects d'un objet serait trop complexe; il convient donc d'organiser la décomposition des objets.
Il existe plusieurs façons de faire qui, tout en partageant la même notion d'objet en tant que critère de décomposition horizontale, diffèrent par le critère de décomposition verticale choisi. Il n'y a donc pas une, mais des méthodes orientées objet. Ce point est rarement évoqué et conduit à d'innombrables discussions entre les partisans de telle ou telle méthode, persuadés de détenir seuls la «vraie» orientation objet.
Dans les chapitres suivants, nous présenterons plusieurs de ces méthodes. Les principales utilisent soit la composition comme critère vertical, soit la classification. D'autres organisations sont possibles, et rien ne dit que toutes les façons de faire aient déjà été publiées.

## L'approche par composition
### Principes de la méthode

* Notion de niveaux d'abstraction
* La COO par composition

### Exemple en composition

Nous voulons réaliser un «cahier de comptes» pour gérer une comptabilité personnelle. Nous allons nous appuyer sur ce que nous connaissons bien, le cahier que nous gérions auparavant à la main. Qu'est-ce donc qu'un cahier de comptes? C'est un ensemble ordonné d'écritures. Nous voyons donc apparaître deux objets : un gestionnaire de données de type machine abstraite qui est le cahier lui-même, et un type de donnée abstrait qui est la notion d'écriture.
Le cahier doit offrir les fonctionnalités habituelles d'une liste séquentielle (insérer et supprimer des éléments, parcourir la liste, etc.). Il possède une propriété supplémentaire : les écritures doivent rester triées par rapport à un ordre courant. Nous imposons donc que la fonction Insérer ajoute toujours une écriture à la bonne place. Mais pour cela, il faut pouvoir comparer plusieurs écritures en fonction d'un critère courant. Comment définir ce critère? Le plus simple est de le définir par rapport à une fonction de comparaison. Nous devons également fournir un point d'entrée pour changer de critère de comparaison. Comme le cahier est évidemment un gestionnaire de données, nous pouvons considérer qu'il existe toujours une position courante, et que la suppression d'une écriture porte toujours sur l'écriture courante. Nous pouvons décrire cette vue du cahier comme :
Une écriture comprend plusieurs éléments : une date d'opération, une date de valeur, un texte descriptif et un montant. En termes d'opérations, il faut pouvoir éditer une écriture, c'est-à-dire permettre à l'utilisateur de modifier son contenu. Enfin, nous devons fournir des fonctions de comparaison entre écritures. Nous pouvons décrire ainsi la vue abstraite des Ecriture :
Enfin, un programme de ce type n'a de sens que s'il est interactif. Il nous faut donc représenter l'interface utilisateur. Une interface digne des environnements modernes sortirait du cadre de cet exemple, mais nous pouvons toujours en faire une abstraction simplifiée : l'interface est ce qui permet au programme de recevoir des ordres de la part de l'utilisateur. Ces ordres sont des entités élémentaires. Il nous faudrait aussi différents moyen d'envoyer des données à l'utilisateur. À ce niveau, nous ne nous préoccupons pas directement des problèmes d'implémentation, aussi nous en tiendrons-nous au niveau logique : par exemple, nous pouvons prévoir d'envoyer des messages d'erreur à l'écran. Nous sommes trop haut dans les niveaux d'analyse pour spécifier des choses telles que faire apparaître une fenêtre avec un beau point d'exclamation pour signaler un problème : nous prévoyons simplement d'envoyer des messages d'erreur, indépendamment de toute représentation. Nous pouvons exprimer ceci comme :
Ayant ainsi défini les briques de base, il ne nous reste plus qu'à les assembler pour vérifier qu'elles nous permettent de réaliser le comportement désiré :
Il manque bien sûr encore de nombreuses fonctionnalités (à commencer par la sauvegarde du cahier sur fichier...), mais l'important est que nous avons mis en place une structure où le rôle de chaque module est clairement identifié : tout ce qui concerne la définition et les propriétés des écritures prises individuellement se trouve dans le paquetage Les_Ecritures; tout ce qui concerne le cahier, c'est-à-dire la gestion de l'ensemble d'écritures, est dans Cahier_Compta; tout ce qui concerne les interfaces est dans Interface_Utilisateur. À partir de là, il est facile de rajouter des fonctionnalités en sachant toujours où intervenir.
L'autre point important est que cette structure nous permet de définir des comportements simples et des invariants : par exemple, Insérer ajoute toujours l'écriture au bon endroit compte tenu du critère de tri courant. Un phénomène très intéressant, qui s'est produit lorsque nous avons développé le projet réel d'où est tiré cet exemple, montre bien l'amélioration importante de sécurité apportée par cette indépendance. Nous avions une erreur dans l'algorithme de retri de Changer_Ordre qui conduisait à des écritures incorrectement ordonnées dans certains cas. Cependant, pour visualiser le cahier, le programme «sortait» une écriture du cahier pour une modification éventuelle, puis la «re-rentrait». À ce moment, le logiciel s'apercevait que l'écriture n'était plus à sa place, et la réinsérait au bon endroit. Autrement dit le logiciel se trouvait dans un état incorrect, mais le simple fait de parcourir le cahier faisait qu'il retournait spontanément à l'état correct. Le logiciel était donc en quelque sorte «autostable». Ce résultat fort rassurant a été obtenu grâce aux principes d'indépendance temporelle et de méfiance réciproque : chaque fonctionnalité ne sait rien ni de qui l'appelle, ni en quelles circonstances. Du coup, chacun assure sa propre sécurité... et corrige éventuellement les erreurs de son voisin. Un tel comportement, qui ne peut être obtenu que par la stricte encapsulation permise par la composition, renforce la confiance que l'on peut avoir quant au bon comportement du logiciel, même face à des événements inattendus.
* ↑ Dans le programme réel qui nous a inspiré cet exemple, environ 80% du code est lié à l'interface utilisateur!

## L'approche par classification

À l'origine de cette approche se trouve l'émergence des langages orientés objet (LOO) [Mas89], dont la classification constitue le cadre méthodologique, en particulier pour maîtriser le mécanisme d'héritage qui les caractérise; on a pris l'habitude d'appeler programmation orientée objet (POO) la programmation avec ces langages. De ce fait, la classification est souvent présentée dans la littérature comme la seule méthode orientée objet. Nous avons déjà vu que l'approche par composition permettait de développer des systèmes «orientés objet» sans nécessiter le recours au mécanisme d'héritage, et nous ne saurions trop insister sur le fait que le concept d'objet couvre un domaine beaucoup plus vaste que celui de la seule POO.

### Principes de la méthode

Dans cette méthode, le critère de décomposition verticale est la classification (au sens de la classification des espèces de Linné [Lin35]). Les objets sont regroupé en classes, elles-mêmes décomposées en sous-classes plus précises, etc. Pour éviter toute confusion, on appelle instance un objet particulier appartenant à une classe.
Par exemple, on regroupera dans la classe Insecte les propriétés communes à tous les insectes. Par la suite, on pourra définir des sous-classes comme Volants ou Rampants, ne contenant respectivement que ce qui est spécifique aux insectes volants (ou rampants), mais commun à tous les insectes volants (ou rampants). Les Volants peuvent à leur tour se décomposer en Mouches, Guêpes, etc., suivant l'arborescence de la figure 17. La guêpe qui vient de vous piquer est une instance de la classe des Guêpes.
Bien entendu, une mouche est un insecte volant, qui est lui-même un insecte tout court. Une Mouche doit donc également posséder les propriétés des Volants et des Insectes : les propriétés des superclasses doivent être transmises à leurs descendants. Ce mécanisme est appelé héritage, puisqu'une classe enfant hérite des propriétés de ses ancêtres. On distingue classiquement l'héritage simple (toute classe n'est enfant que d'une seule superclasse) qui conduit à des programmes dont la topologie est un arbre, et l'héritage multiple (toute classe peut avoir plusieurs superclasses) qui conduit à des programmes dont la topologie est un graphe général acyclique. Nous reviendrons plus loin sur la question de l'héritage multiple. Au fur et à mesure que l'on descend dans l'analyse, les objets possèdent de plus en plus de propriétés (celles dont ils ont hérité, plus celles qui sont particulières à la sous-classe), applicables à un nombre de plus en plus restreint d'instances. La descente dans l'analyse conduit donc simultanément à un enrichissement et à une spécialisation des classes.
Comme nous l'avons vu au paragraphe définition, une caractéristique de l'orientation objet en général est l'encapsulation, regroupement dans un même module de structures de données et de structures de programme. Dans le vocabulaire de la classification, on a pris l'habitude d'appeler attributs les structures de données liées à un objet, et méthodes les structures de programme (opérations) associées.
* ↑ Tous les objets appartenant à une classe possèdent les mêmes propriétés, ils forment donc une classe d’équivalence au sens mathématique - d’où le nom.

### Mécanismes

La mise en application de la classification nécessite des mécanismes spéciaux au niveau du langage de programmation. On qualifie de «langage orienté objet» un langage offrant ces mécanismes. Nous allons présenter ceux offerts par Ada; d'autres langages orientés objet offrent des fonctionnalités similaires, mais non strictement équivalentes. Nous les comparerons à la fin de ce chapitre.
* Types étiquetés
* Extension de type et héritage; redéfinition de méthodes
* Classes, polymorphisme et liaison dynamique
* Liaison avec le parent
* Types abstraits
* Dissimulation d'information
* Facettes multiples

### Utilisation de l'héritage

L'héritage, comme nous venons de le voir, s'appuie sur des mécanismes particuliers au niveau du langage. Nous allons maintenant présenter quelques utilisations typiques de ces mécanismes. Nous verrons ensuite un exemple d'utilisation plus «méthodologique» au service de la classification.
* Gestionnaires de données hétérogènes
* Implémentations multiples
* Objets répartis
* ↑ Ceci tend à disparaître… ce qui fait une raison de plus d'utiliser Ada !
* ↑ N'oublions pas que les programmeurs qui étaient débutants à l'époque héroïque (début des années 60) n'ont pas encore atteint l'âge de la retraite !
* ↑ Chiffres de 1995, le moindre PC fait beaucoup mieux aujourd'hui
* ↑ Que les universitaires nous pardonnent ces quelques critiques – nous avons été des leurs pendant plus de dix ans.