« Méthodes de génie logiciel avec Ada/Première partie » : différence entre les versions

Contenu supprimé Contenu ajouté
m Formatage, ajout de div style="text-align: center;", ajout de strong
DannyS712 (discussion | contributions)
m <source> -> <syntaxhighlight> (phab:T237267)
Ligne 188 :
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):
<sourcesyntaxhighlight lang="fortran">
DO 10 I = 1.5
</syntaxhighlight>
</source>
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):
<sourcesyntaxhighlight lang="fortran">
DO10I = 1.5
</syntaxhighlight>
</source>
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<ref>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.</ref>)...
 
Plus prosaïquement, si vous lisez dans un programme:
<sourcesyntaxhighlight lang="ada">
for I in 1..MILLE loop
...
end loop;
</syntaxhighlight>
</source>
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:
<sourcesyntaxhighlight lang="ada">
MILLE : constant := 10_000;
</syntaxhighlight>
</source>
il exécutera donc la boucle dix mille fois...<ref>Cet exemple n'est pas irréaliste; nous l'avons effectivement trouvé dans un projet réel...</ref>
 
Ligne 232 :
:''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:
<sourcesyntaxhighlight lang="ada">
LISTE := [P in [1..N] | N mod P = 0];
</syntaxhighlight>
</source>
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<ref>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.</ref>. 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<ref>Ou si nous ne sommes pas prêts à payer en temps d'exécution le prix de la facilité offerte par ce langage...</ref>, nous devrons exprimer notre conception à ce niveau. En Ada, ceci s'exprimerait comme:
<sourcesyntaxhighlight lang="ada">
declare
type Index is new Positive range 1..N;
Ligne 256 :
end loop;
end;
</syntaxhighlight>
</source>
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<ref>Plus vraisemblablement, dans un programme correctement écrit, elle serait déclarée d'un type dérivé d'Integer.</ref>. 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.
Ligne 264 :
 
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:
<sourcesyntaxhighlight lang="c">
main()
{
Ligne 282 :
}
}
</syntaxhighlight>
</source>
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é<ref>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.</ref> ; 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...
 
Ligne 308 :
 
Dans beaucoup de programmes, et quel que soit le langage de programmation, il est fréquent de trouver des déclarations comme :
<sourcesyntaxhighlight lang="ada">
Compteur : Integer;
</syntaxhighlight>
</source>
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.
Ligne 317 :
* 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? Etudier le domaine de problème et refléter ses connaissances dans l'expression du langage, ce qui aurait donné :
<sourcesyntaxhighlight lang="ada">
type Valeurs_à_compter is range 0..40_000;
Compteur : Valeurs_à_compter;
</syntaxhighlight>
</source>
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é.
 
Ligne 349 :
 
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<ref>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.</ref>. 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 :
<sourcesyntaxhighlight lang="ada">
type Couleurs is (Noir, Rouge, Bleu, Vert,
Jaune, Magenta, Cyan, Blanc);
</syntaxhighlight>
</source>
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 :
<sourcesyntaxhighlight lang="ada">
Noir : constant := 0;
Rouge : constant := 1;
...
</syntaxhighlight>
</source>
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.
 
Ligne 364 :
 
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 :
<sourcesyntaxhighlight lang="ada">
type Mémoire is array (0..32767) of Integer;
type Page is array (0..511) of Integer;
Ligne 370 :
M : Mémoire;
P : Page
</syntaxhighlight>
</source>
Le problème était que le compilateur refusait l'affectation suivante :
<sourcesyntaxhighlight lang="ada">
P := M(0..511); -- Tranche des 512 premiers
-- éléments de M
</syntaxhighlight>
</source>
alors que la boucle suivante, qui était sémantiquement strictement équivalente, était acceptée :
<sourcesyntaxhighlight lang="ada">
for I in 0..511 loop
P (I) := M (I);
end loop;
</syntaxhighlight>
</source>
Evidemment, 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.
 
Ligne 395 :
:''Etudiant:'' 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:
<sourcesyntaxhighlight lang="ada">
type Mémoire is array (Natural range <>) of Integer;
subtype Page is Mémoire (0..511);
Ligne 401 :
M : Mémoire(0..32767);
P : Page;
</syntaxhighlight>
</source>
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. A ce plus bas niveau, la sémantique était plus pauvre, et il avait pu violer l'abstraction.
 
Ligne 412 :
 
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 :
<sourcesyntaxhighlight lang="ada">
for I in A .. B loop
...
end loop;
</syntaxhighlight>
</source>
Il vous répondra immédiatement :
<sourcesyntaxhighlight lang="ada">
for (I = A; I <= B; I++) {
...
}
</syntaxhighlight>
</source>
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<ref>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).</ref>, 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.
 
Ligne 434 :
 
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:
<sourcesyntaxhighlight lang="ada">
I : Integer; -- Ce qu'il ne faut pas faire !
S1 : String(1..10);
Ligne 441 :
I := Calcul_compliqué; -- (1)
S1(I) := S2(I); -- (2)
</syntaxhighlight>
</source>
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:
<sourcesyntaxhighlight lang="ada">
subtype Index is Integer range 1..10;
I : Index;
Ligne 451 :
I := Calcul_compliqué; -- (1)
S1(I) := S2(I); -- (2)
</syntaxhighlight>
</source>
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.
 
Ligne 498 :
:''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:
<sourcesyntaxhighlight lang="ada">
type Compte_1 is new Integer range 0..1000;
type Compte_2 is new Natural range 0..1000;
type Compte_3 is new Comptable range 0..1000;
subtype Compte_4 is Comptable range 0..1000;
</syntaxhighlight>
</source>
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.
 
Ligne 509 :
 
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):
<sourcesyntaxhighlight lang="ada">
procedure Assert (Condition : Boolean) is
begin
Ligne 516 :
end if;
end Assert;
</syntaxhighlight>
</source>
On peut alors exprimer les invariants sous la forme:
<sourcesyntaxhighlight lang="ada">
Assert( Taille (Pile) >= 2 );
</syntaxhighlight>
</source>
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.