« Méthodes de génie logiciel avec Ada/Cinquième 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 255 :
* Argent
L'argent est un type numérique comportant des francs et des centimes. Les seules opérations nécessaires a priori sont l'addition et la soustraction. Nous en faisons cependant un paquetage autonome, car cela correspond à une notion indépendante du reste du problème. Un type décimal est particulièrement adapté et comporte les opérations nécessaires. Le type Argent est donc un objet (ou plutôt un type) terminal du projet. Quelles limites devons-nous utiliser pour ce type? Actuellement, la plus grosse pièce courante en France est la pièce de 20F, mais il ne paraît pas impossible de voir un jour apparaître des pièces de 50 ou même 100F. La prudence nous recommande donc de prévoir cinq chiffres significatifs (avec les centimes).
<sourcesyntaxhighlight lang="ada">
package Définition_Argent is
type Argent is delta 0.01 digits 5;
end Définition_Argent;
</syntaxhighlight>
</source>
* Boisson
Tout d'abord, nous nous apercevons qu'à ce point de l'analyse, rien n'est spécifique des produits liquides; nous pourrions aussi bien distribuer des cacahuètes... Nous allons donc remplacer le terme boisson par produit.
Ligne 265 :
Qu'est-ce qui caractérise un produit? Il lui est associé un prix et chaque produit a une façon bien particulière d'être fourni. Ce peut être élémentaire (cas du paquet de cacahuètes: il suffit d'ouvrir un relais pour laisser tomber le paquet), mais d'autres doivent être réellement fabriqués; pour un café par exemple, il faut laisser tomber de la poudre de café (et éventuellement de la poudre de lait), du sucre, une petite cuillère, une quantité plus ou moins grande d'eau... Deux solutions sont possibles à ce niveau:
** Nous considérons que la notion de produit est une classe générale, munie d'un attribut «prix» et d'une méthode de fabrication propre à chaque élément de la classe. Comme aucun produit réel ne peut être un «produit» sans être quelque chose de plus précis, cette classe doit être abstraite. C'est une solution typiquement «classification»: nous créerons les produits réels grâce au mécanisme d'héritage à partir de la classe Produit. Nous exprimerions cette solution de la façon suivante:
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
package Classe_Produit is
Ligne 273 :
procedure Fabriquer (Le_Produit: Produit) is abstract;
end Classe_Produit;
</syntaxhighlight>
</source>
** Nous considérons que le produit n'est qu'un identifiant auquel sont associés de façon extérieure un tarif (qui fournit le prix de chaque produit) et une recette (qui définit comment fabriquer le produit). Une telle approche ne fait pas appel aux notions de la classification ni à l'héritage.
 
Ligne 281 :
 
Nous choisirons donc la seconde solution, mais les raisons qui nous y ont amené devront être soigneusement consignées dans le document de maintenance. Nous notons que nous identifions alors trois nouveaux objets: le tarif, la recette et l'unité de fabrication. La notion de produit devient alors extrêmement ténue: elle ne sert qu'à faire la liaison entre un bouton sur lequel on appuie, un prix et une recette. Nous pouvons la représenter par n'importe quel type discret. La tendance naturelle en Ada serait d'utiliser un type énumératif:
<sourcesyntaxhighlight lang="ada">
type Produit is (Café, Thé, Orange, Soda);
</syntaxhighlight>
</source>
Cette solution a nécessairement un aspect limitatif: les boissons doivent être choisies à la compilation et tout changement nécessite une recompilation de tout le système... ce que nous cherchons précisément à éviter. Revenons un peu sur ce qu'est un produit pour la vue que nous en avons maintenant. C'est finalement une notion qui nous permet de faire la liaison entre un bouton poussé par un utilisateur et une recette préparée par l'unité de fabrication. Le «parfum» choisi est sans importance pour nous! Ce que nous appelons Produit n'est qu'un numéro de bouton sur le panneau de contrôle. A un bouton est associée une recette (et un prix) et la notion de Produit devient un simple identifiant neutre. Type énumératif, type entier, que vaut-il mieux choisir? Difficile à dire à ce niveau, tout au plus pouvons-nous nous dire qu'il doit s'agir d'un type discret. Comme nous n'avons pas vraiment de critère de choix, mais que les raffinements ultérieurs pourront peut-être nous guider, il suffit d'exprimer l'état de nos réflexions en déclarant:
<sourcesyntaxhighlight lang="ada">
with ADPT;
package Définition_Produit is
type Produit is new ADPT.Type_Discret;
end Définition_Produit;
</syntaxhighlight>
</source>
Nous n'avons a priori besoin d'aucune opération sur les produits (on ne va pas, par exemple, additionner deux produits). Comme pour l'argent, nous en faisons un paquetage séparé, car même s'il ne s'agit après tout que d'une simple déclaration de type, la notion qu'elle recouvre est importante.
* Unité de fabrication
L'unité de fabrication est une sorte de machine stupide, à qui l'on dit de confectionner un produit... et qui le fait. Elle n'a pas à se préoccuper du qui ni du quoi (comme de savoir si le consommateur a payé). Inversement, elle doit nous permettre d'ignorer totalement (à ce niveau) comment on fait pour fabriquer le produit. Du point de vue des spécifications, ceci s'exprime comme:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Unité_Fabrication is
procedure Confectionner (Le_produit : Produit);
end Unité_Fabrication;
</syntaxhighlight>
</source>
Avec cette vue il ne peut s'agir que d'une machine abstraite. Doit-elle être active? Il est clair que le dispositif physique peut très bien évoluer en parallèle. Mais en pratique, pendant que la machine confectionne un produit, elle reste totalement bloquée et n'accepte aucune autre opération. Dans ces conditions, mieux vaut la laisser passive.
* Monnayeur
Le monnayeur gère tout ce qui est lié à l'argent. Il doit pouvoir nous indiquer le montant entré par le consommateur. Il doit également pouvoir rendre la monnaie sur le prix d'un produit. Notons que le monnayeur doit être actif en permanence, car le consommateur peut introduire des pièces à n'importe quel moment<ref>Dans un système bien fait, le consommateur doit pouvoir introduire une pièce, puis appuyer sur «Café» et enfin introduire sa deuxième pièce. Il est très difficile d'obtenir ce comportement sans parallélisme... d'ailleurs certains distributeurs ne l'autorisent pas.</ref>. Il s'agira donc vraisemblablement d'une machine abstraite active.
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
with Définition_Produit; use Définition_Produit;
Ligne 310 :
procedure Rendre_Monnaie (Du_produit: Produit);
end Monnayeur;
</syntaxhighlight>
</source>
* Menu
Le menu nous indique le produit choisi par le consommateur. Il gère les choix de l'utilisateur et ne renvoie qu'un choix valide. Ceci s'exprime comme:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Menu is
function Choix_Consommateur return Produit;
end Menu;
</syntaxhighlight>
</source>
* Tarif
Le tarif nous indique le prix de chaque produit:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
with Définition_Argent; use Définition_Argent;
Ligne 327 :
function Le_Prix (De : Produit) return Argent;
end Tarif;
</syntaxhighlight>
</source>
Ne peut-on en faire directement une table? Certainement, mais alors nous sommes lié au choix final du type de Produit. Si pour une raison ou une autre nous décidons que Produit n'est pas un type discret (ce pourrait être un type accès par exemple), il nous sera beaucoup plus facile d'évoluer si nous gardons le tarif sous la forme d'une fonction. Inversement, si nous implémentons la fonction au moyen d'une table, il suffira de mettre la fonction «en ligne» pour ne payer aucune pénalité d'efficacité<ref>Cette dernière remarque est pour le principe. Il est très peu vraisemblable que nous ayons un problème d'efficacité à
ce niveau.</ref>. Décider à ce niveau d'utiliser une table serait un exemple typique de surspécification – spécifier à partir d'une implémentation particulière, au lieu d'abstraire le besoin réel.
Ligne 334 :
<li>Ecrire le corps de l'objet à concevoir</li>
Dans la phase initiale, l'objet à concevoir est simplement le programme principal. Comme tout programme principal Ada, il s'agit d'une simple procédure sans paramètres.
<sourcesyntaxhighlight lang="ada">
with Définition_Produit, Définition_Argent, Menu,
Monnayeur, Unité_Fabrication, Tarif;
Ligne 351 :
end loop;
end Principal;
</syntaxhighlight>
</source>
 
<li>Contester</li>
Ligne 359 :
 
Faut-il introduire une fonctionnalité supplémentaire pour demander à l'unité de fabrication si un produit peut être confectionné? La détermination risque d'être très difficile: si l'on peut vérifier que les différents ingrédients nécessaires sont disponibles, ce n'est quand même pas une garantie que le produit peut être réalisé. Un réservoir de poudre peut être en panne, un robinet collé... Nous devons donc de toute façon nous attendre qu'un produit ne puisse être réalisé: il faut prévoir une exception dans l'unité de fabrication.
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Unité_Fabrication is
Ligne 365 :
Confection_Impossible : exception;
end Unité_Fabrication;
</syntaxhighlight>
</source>
Du coup, cela résout le problème précédent: le programme principal tente la fabrication; en cas d'échec, il invalide le produit. Nous ajoutons donc une fonctionnalité au menu pour invalider le produit; par symétrie, il nous faut également une fonctionnalité pour le revalider:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Menu is
Ligne 374 :
procedure Revalider (Le_produit : Produit);
end Menu;
</syntaxhighlight>
</source>
* Cas de l'annulation
A tout moment (tout au moins tant que la distribution n'a pas commencé), le consommateur peut annuler sa commande. Nous n'avons encore rien prévu pour le gérer. Remarquons que ce cas n'était pas prévu au cahier des charges; dans une optique industrielle, il faudrait prévenir le client afin de vérifier que notre interprétation du comportement souhaité correspond bien à ses désirs.
Ligne 383 :
 
Qui doit gérer cet objet? A priori, il correspond à un bouton de la face avant du distributeur, donc il devrait faire partie du menu. D'autre part, sur les distributeurs réels, le bouton d'annulation se situe plutôt du côté du monnayeur. Enfin nous pouvons toujours en faire une entité indépendante... Mettons-le pour l'instant dans le menu, mais ne soyons pas étonné si la suite de l'analyse vient à le déplacer. Cela nous donne:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Menu is
Ligne 396 :
end Annulation;
end Menu;
</syntaxhighlight>
</source>
Cette notion de point de non-retour est également importante pour le monnayeur: si l'utilisateur continue d'introduire des pièces, elles doivent être ignorées. On va donc avoir un changement d'état dans le monnayeur: dans un premier temps, on attend d'avoir atteint au moins le montant du produit. Une fois ce montant atteint, on fait passer le monnayeur dans un état «bloqué» où il rejette systématiquement toute pièce introduite. Ce comportement peut être obtenu par logiciel, mais au fond il vaut mieux le faire par matériel: il suffit de piloter un clapet qui laisse retomber directement les pièces introduites dans le gobelet de rendu de la monnaie. Ce clapet est au repos dans l'état «bloqué», de façon à ne pas voler l'utilisateur en cas de panne de courant. Il faut évidemment introduire un moyen de faire repasser le monnayeur dans l'état «actif». De plus, il faut pouvoir demander au monnayeur de rendre toutes les sommes introduites. Notre spécification va donc devenir:
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
with Définition_Produit; use Définition_Produit;
Ligne 408 :
procedure Rembourser;
end Monnayeur;
</syntaxhighlight>
</source>
<references />
* Cas de la monnaie épuisée
Qu'appelle-t-on monnaie épuisée? Les distributeurs qui rendent la monnaie disposent tous du voyant correspondant, mais sa signification n'est pas du tout évidente. En effet, le monnayeur peut ne pas être capable de garantir le rendu de monnaie dans tous les cas, tout en étant capable de le faire dans certains cas... Il faut séparer le problème du voyant de celui du rendu effectif. En dessous d'un certain nombre de pièces, le monnayeur allume automatiquement le voyant. Ceci est interne au monnayeur et ne nous concerne pas pour l'instant. Si maintenant le monnayeur est dans l'incapacité de rendre la monnaie qui lui a été demandée, il doit lever une exception (appelons-la Rendu_Impossible). Cette exception peut être rattrapée par le logiciel appelant qui prendra les mesures nécessaires, comme d'annuler la demande et de demander de rendre la monnaie avec un montant égal à la somme entrée... ce qui est évidemment toujours possible. La spécification du monnayeur devient:
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
with Définition_Produit; use Définition_Produit;
Ligne 423 :
Rendu_Impossible : exception;
end Monnayeur;
</syntaxhighlight>
</source>
Par analogie, nous allons rajouter une procédure Activer au Menu pour autoriser les choix de l'utilisateur, comme nous l'avions noté précédemment:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Menu is
Ligne 439 :
end Annulation;
end Menu;
</syntaxhighlight>
</source>
* Autres cas exceptionnels
Nous ne voyons pas a priori d'autres cas d'exception; mais si un problème imprévu se produisait, il faut en tout état de cause rendre son argent à l'utilisateur. Il convient donc de prévoir un traitement d'exception «rattrape-tout».
* Le problème de l'attente active
On appelle «attentes actives» les boucles qui ne font qu'attendre qu'un événement se produise, comme dans le cas de la boucle d'attente sur le montant introduit. Une telle boucle est toujours gênante, car elle accapare l'unité centrale sans rien faire d'utile. Il faut toujours mettre un delay dans ce cas, autrement cette boucle pourrait empêcher d'autres parties du programme de s'exécuter... y compris celles chargées de changer la condition de boucle. Mais la meilleure solution consiste à les éviter. Pour cela, il faut se poser la question suivante: quel est le besoin de plus haut niveau qui nous a conduit à cette boucle? En l'occurrence, c'est d'attendre qu'un certain montant soit introduit. Attendre le prix d'un produit pourrait parfaitement être un service rendu par le monnayeur, faisant ainsi disparaître l'attente active, au moins à ce niveau d'abstraction. L'implémentation la fera peut-être réapparaître, à moins que nous ne trouvions une autre solution. Au moins, en reportant ce problème vers les couches plus profondes, ouvrons-nous la possibilité d'autres solutions. La spécification finale du monnayeur devient donc:
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
with Définition_Produit; use Définition_Produit;
Ligne 456 :
Rendu_Impossible : exception;
end Monnayeur;
</syntaxhighlight>
</source>
 
<li>Nouvelle version du programme principal</li>
Nous pouvons maintenant récrire le programme principal en fonction des modifications que nous avons apportées:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit, Définition_Argent, Menu,
Monnayeur, Unité_Fabrication, Tarif;
Ligne 501 :
Rembourser;
end Principal;
</syntaxhighlight>
</source>
</ol>
 
===Tester le plan d'abstraction===
Nous avons maintenant un schéma général qui paraît satisfaisant. Les spécifications que nous venons d'établir peuvent être compilées pour vérifier qu'elles sont cohérentes. Nous pouvons vérifier dynamiquement le plan d'abstraction en fournissant des corps maquettes tels que:
<sourcesyntaxhighlight lang="ada">
with ADPT;
package body Unité_Fabrication is
Ligne 516 :
end Confectionner;
end Unité_Fabrication;
</syntaxhighlight>
</source>
On trouvera en annexe les corps maquettes des autres paquetages. On obtient ainsi une première version exécutable modélisant le comportement du programme à haut niveau, sans avoir encore décidé des modalités de réalisation des couches inférieures.
===Documenter le plan d'abstraction===
Ligne 535 :
===Le produit===
Il est temps maintenant de décider de la représentation effective du Produit. Nous avons déjà vu que ce devait être un type discret. Le premier niveau de maquettage n'a fait apparaître aucun besoin nouveau: il s'agit d'un simple identifiant totalement neutre; nous avons vu qu'à la limite, on pouvait assimiler le produit à un numéro de bouton. Un type énumératif serait parfaitement acceptable, puisque nous n'avons besoin d'aucune opération arithmétique, mais présente cependant un inconvénient: il est plus difficile à faire évoluer, par exemple si l'on crée un nouvel appareil disposant d'un nombre supérieur de boutons. Nous choisirons donc de le représenter par un type entier. Les bornes peuvent être données par une constante du paquetage, ou dans un paquetage global définissant la configuration matérielle, en particulier le nombre de boutons (et donc le nombre de produits pouvant être distribués).
<sourcesyntaxhighlight lang="ada">
package Définition_Produit is
Max_produit : constant := 10;
type Produit is range 1..Max_produit;
end Définition_Produit;
</syntaxhighlight>
</source>
===Le monnayeur===
<ol style="list-style-type:lower-alpha">
<li>Reprendre les spécifications</li>
Nous sommes maintenant chargé d'étudier le problème du monnayeur. A ce stade, nous oublions tout ce que nous avons pu dire du distributeur en général pour étudier ce qu'est un monnayeur. Il apparaît immédiatement que celui que nous avons défini est trop spécifique. Rendre_Monnaie a comme paramètre un Produit; ceci induit un couplage supplémentaire totalement inutile. De plus, rien ne dit que le monnayeur fasse partie d'un appareil où la notion de produit ait un sens; ce pourrait être par exemple une machine à sous (type «bandit manchot»), auquel cas il peut arriver que l'on soit amené<ref>Certes rarement !</ref> à «rendre» un montant supérieur à celui introduit! Il est beaucoup plus logique que Rendre_Monnaie et Attendre aient un argument de type Argent: le montant à rendre ou à attendre. Le monnayeur n'a plus à savoir pour quelles raisons on attend ou on rend cet argent. Cette modification permet de faire disparaître la procédure Rembourser, qui introduisait d'ailleurs un couplage temporel, le montant à rendre dépendant du montant introduit. D'autres question de sémantique délicate auraient pu se poser, comme celle de savoir si l'on avait le droit de rembourser lorsque le monnayeur n'était pas dans l'état bloqué... Notre spécification devient:
<sourcesyntaxhighlight lang="ada">
with Définition_Argent; use Définition_Argent;
package Monnayeur is
Ligne 555 :
Rendu_Impossible : exception;
end Monnayeur;
</syntaxhighlight>
</source>
Bien sûr, il faut immédiatement prévenir le niveau supérieur que nous souhaitons modifier les spécifications. Le programme principal va devenir:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit, Définition_Argent, Menu,
Monnayeur, Unité_Fabrication, Tarif;
Ligne 599 :
Rendre_Monnaie (Montant => Montant_Introduit);
end Principal;
</syntaxhighlight>
</source>
Un logiciel à caractère temps réel comme celui-ci doit fonctionner dans tous les cas, même ceux qui sont hautement improbables. Nous devons vérifier qu'aucune condition de course ne peut se produire suite à notre modification: en effet l'instruction
<sourcesyntaxhighlight lang="ada">
Rendre_Monnaie (Montant_Introduit-Le_Prix (De => Choisi));
</syntaxhighlight>
</source>
n'est pas atomique; si l'utilisateur introduit une pièce entre le moment où la valeur de la monnaie à rendre est calculée et le moment où la procédure est appelée, il y a un risque de rendre un montant incorrect. C'est pourquoi le Montant_Introduit est calculé après avoir bloqué le monnayeur; si l'utilisateur continue d'introduire des pièces, elles retomberont directement dans la sébile sans être encaissées et ne gêneront donc pas le programme. Pensons également qu'une pièce peut être introduite après que nous sommes revenu de la procédure Attendre; ce n'est pas gênant, car elle sera comptabilisée normalement et nous en tiendrons donc compte au niveau de Rendre_Monnaie.
 
Ligne 625 :
 
Nous disposons donc d'un objet que nous appellerons le compteur, protégé contre des accès simultanés. Les opérations nécessaires sont de lui ajouter une valeur, de connaître la valeur courante, de le remettre à 0 et d'attendre qu'un certain montant soit introduit. Ceci s'exprime naturellement comme:
<sourcesyntaxhighlight lang="ada">
protected Compteur is
procedure Ajouter (Montant : Argent);
Ligne 632 :
entry Attendre (Montant : Argent);
end Compteur;
</syntaxhighlight>
</source>
Les procédures Activer et Bloquer doivent piloter le clapet. Un clapet est (extérieurement) un objet simple: il possède un état ouvert et un état fermé et les opérations pour l'ouvrir ou le fermer. Ceci s'exprime comme:
<sourcesyntaxhighlight lang="ada">
package Clapet is
procedure Ouvrir;
procedure Fermer;
end Clapet;
</syntaxhighlight>
</source>
Faut-il faire un paquetage (de type machine abstraite), alors que l'on pourrait avoir une simple procédure (avec un paramètre ouvert/fermé)? Oui, car si la spécification est simple, l'implémentation peut être plus subtile. Par exemple, il faut garantir qu'on ne sort de la procédure Ouvrir (ou Fermer) que lorsque le clapet est effectivement ouvert ou fermé. Comme il s'agit d'un dispositif mécanique, ce n'est pas immédiat et il faut introduire un retard ou un contact annexe pour vérifier la position du relais. Cette dernière solution est un peu luxueuse pour nous (après tout, en cas de panne, le risque ne dépasse pas quelques francs), mais est indispensable pour les relais gouvernant des dispositifs de sécurité, comme ceux qui commandent les signaux des trains.
 
Pour pouvoir rendre la monnaie, il nous faut à l'évidence savoir combien il nous reste de pièces de chaque sorte et déclencher physiquement la chute d'une ou plusieurs pièces. Le réservoir de pièces doit donc fournir ces fonctionnalités. Comme nous devrons avoir un réservoir associé à chaque sorte de pièce, nous en faisons un type de donnée abstrait:
<sourcesyntaxhighlight lang="ada">
package Réservoirs_De_Pièces is
Capacité_Max : constant := 100;
Ligne 655 :
...
end Réservoirs_De_Pièces;
</syntaxhighlight>
</source>
Remarquons au passage que nous avons dû définir un type pour la valeur retournée par Nombre_De_Pièces. Il aurait été tentant d'utiliser Integer... Mais il existe un problème bien réel: un magasin de pièces a nécessairement une capacité limitée. Nous préférons refléter cette propriété de l'objet physique dans les types du programme. Enfin, on ne peut exclure qu'une chute de pièce soit commandée alors qu'il ne reste rien dans le réservoir: nous devons donc prévoir une exception Réservoir_Vide pour ce cas.
 
Ligne 661 :
 
Nous parlons ici des vraies pièces de monnaie, notion qui varie selon le pays, les fluctuations de la politique monétaire... Il serait bon de concentrer dans un seul paquetage tout ce qui dépend de la définition des pièces. Ceci comprend l'éventail des pièces disponibles, leurs valeurs, les réservoirs associés et la façon d'obtenir un montant donné à partir des pièces disponibles. Nous en faisons un paquetage distinct, pour pouvoir le modifier sans perturber le reste de l'application et pour lui garder une bonne autonomie; mais comme il fait logiquement partie du monnayeur et n'a aucune raison d'être accessible de l'extérieur, nous en ferons un enfant privé. Ceci nous donne:
<sourcesyntaxhighlight lang="ada">
with Réservoirs_De_Pièces; use Réservoirs_De_Pièces;
private package Monnayeur.Définition_Pièces is
Ligne 674 :
return Répartition_Pièces;
end Monnayeur.Définition_Pièces;
</syntaxhighlight>
</source>
Notons que la fonction Répartir est essentiellement un algorithme (non évident au demeurant) qu'il sera approprié d'analyser suivant les techniques de programmation structurée.
* Ecrire le corps de l'objet à concevoir
Nous pouvons maintenant écrire le corps de l'objet monnayeur. Le compteur et la tâche sont des objets séparés, mais inclus dans le monnayeur. Nous séparerons donc leurs corps pour pouvoir compiler (et donc vérifier) l'utilisation que nous en faisons avant de songer à leur implémentation:
<sourcesyntaxhighlight lang="ada">
with Clapet, Monnayeur.Définition_Pièces;
package body Monnayeur is
Ligne 719 :
end Rendre_Monnaie;
end Monnayeur;
</syntaxhighlight>
</source>
* Contester
La spécification du compteur telle qu'elle est répond à notre besoin; mais est-elle suffisante dans le cas général? Il reste un point important non spécifié: que se passe-t-il si plusieurs tâches appellent Attendre avant que le montant désiré ait été atteint? En particulier, que se passe-t-il si plusieurs appels à Attendre ne spécifient pas le même montant attendu? Bien sûr, on peut dire que cela n'a pas d'importance, vu qu'il n'y aura jamais qu'une seule tâche qui appelle Attendre dans notre système. Cependant, ceci est particulier à notre utilisation. Dans une conception orientée objet, nous devons réfléchir aux propriétés de l'objet indépendamment de toute utilisation particulière. Ce surcroît de travail peut paraître inutile, mais se justifiera dès la première réutilisation et l'expérience montre que ceci arrive souvent un peu plus tard dans le même projet.
Ligne 728 :
 
Etudions la réalisation de chacune de ces possibilités. Tout d'abord, il existe une difficulté technique commune aux trois: il faut attendre d'atteindre un montant qui figure en paramètre de l'appel à Attendre. Or la garde d'une entrée ne peut dépendre des paramètres de l'appelant. Autrement dit, pour connaître le montant à attendre, il faut accepter l'appel d'entrée. Ce problème est connu depuis longtemps [Wel83] et a été résolu en Ada 95 par l'instruction requeue. La «base commune» aux trois solutions peut s'écrire ainsi<ref>Dans cette partie de la discussion, nous ne montrons pour simplifier que la partie du type protégé Compteur liée à l'entrée Attendre. Il faut bien entendu fournir aussi les autres fonctionnalités.</ref>:
<sourcesyntaxhighlight lang="ada">
protected Compteur is
entry Attendre (Montant : Argent);
Ligne 748 :
end Attendre;
end Compteur;
</syntaxhighlight>
</source>
:La clause when True de l'entrée Attendre peut sembler bizarre, mais il est obligatoire de fournir une garde pour une entrée de type protégé. Ne peut-on en faire une procédure? Non, car seule une entrée a le droit de faire un requeue. La mention with abort de l'instruction requeue autorise une éventuelle annulation de la demande par avortement ou fin d'appel temporisé.
<references />
Ligne 755 :
 
La solution la plus simple consiste à lever une exception. L'entrée Attendre ne peut être appelée qu'une seule fois après une remise à zéro. Mais que faire si quelqu'un se trouve en attente lors de la remise à zéro? Comme nous n'autorisons qu'une seule tâche à utiliser le compteur, le mieux est de lever également une exception. Laquelle? Puisqu'il s'agit d'un comportement qui résulte d'une violation des règles d'utilisation de l'objet par le programmeur, le mieux est de lever Program_Error. Notre compteur devient:
<sourcesyntaxhighlight lang="ada">
protected Compteur is
entry Attendre (Montant : Argent);
Ligne 789 :
end Remise_A_Zéro;
end Compteur;
</syntaxhighlight>
</source>
Une variante consiste à interdire l'appel d'Attendre si quelqu'un est déjà en attente, mais à autoriser plusieurs attentes même s'il n'y a pas de remise à zéro intermédiaire:
<sourcesyntaxhighlight lang="ada">
protected Compteur is
entry Attendre (Montant : Argent);
Ligne 814 :
end Attendre;
end Compteur;
</syntaxhighlight>
</source>
Nous n'avons pas examiné les solutions permettant de libérer plusieurs tâches au fur et à mesure de l'incrément du compteur; elles sont nettement plus compliquées, aussi proposons-nous de nous en tenir à l'une de celles que nous avons étudiées. La plus simple est finalement la dernière: pas d'exception à traiter, expression naturelle du comportement par les gardes du type protégé. Nous la choisirons donc, mais nous retiendrons que d'autres étaient acceptables.
 
Un œil critique remarquera aisément que le compteur protégé que nous venons de définir semble très utile; tellement utile même qu'il est vraisemblable que de nombreux projets pourraient avoir besoin de fonctionnalités semblables. Ne pourrait-on en faire un composant réutilisable? Demandons-nous donc ce qu'est un compteur. C'est quelque chose qui contient une valeur, qui nous permet de l'augmenter et de connaître sa valeur courante. Nous pourrions nous limiter à compter des nombres entiers, mais a priori rien n'empêche de compter d'autres choses; la seule nécessité est que la notion d'additionait un sens pour les valeurs à compter, ainsi bien entendu qu'une valeur nulle pour la remise à zéro. Enfin, comme nous voulons pouvoir attendre d'atteindre une valeur, il est nécessaire de disposer d'une fonction de comparaison. Ceci s'exprime comme suit:
<sourcesyntaxhighlight lang="ada">
generic
type A_Compter is private;
Ligne 832 :
procedure Remise_A_Zéro;
end Compteur_Protégé_Générique;
</syntaxhighlight>
</source>
On trouvera le corps du compteur générique en annexe. Le corps final et définitif (ce n'est plus une maquette) du monnayeur devient:
<sourcesyntaxhighlight lang="ada">
with Compteur_Protégé_Générique, Réservoirs_De_Pièces,
Monnayeur.Définition_Pièces, Clapet;
Ligne 873 :
end Rendre_Monnaie;
end Monnayeur;
</syntaxhighlight>
</source>
 
<li>Tester le plan d'abstraction</li>
Pour pouvoir tester le plan d'abstraction, il nous manque le corps de la tâche Surveillance (qui fait logiquement partie de ce plan). Celle-ci est intimement liée au matériel, aussi allons-nous nous contenter de fournir un corps maquette. Les corps maquettes des modules Clapet et Définition_Pièces (qui n'ont rien de particulier) sont fournis en annexe.
<sourcesyntaxhighlight lang="ada">
with Ada.Text_IO; use Ada.Text_IO;
separate (Monnayeur) task body Surveillance is
Ligne 899 :
end loop;
end Surveillance;
</syntaxhighlight>
</source>
:La procédure Get_Immediate, de Text_IO, permet de lire un caractère au clavier sans attendre de retour chariot.
 
Ligne 926 :
 
Le paquetage définissant la classe des distributeurs ne peut (et ne doit) être utilisé que par l'unité de fabrication; il n'y a aucune raison de le laisser accessible de l'extérieur. Nous en ferons donc un enfant privé. Nous pouvons exprimer ces décisions de conception comme:
<sourcesyntaxhighlight lang="ada">
with System.Storage_Elements; use System.Storage_Elements;
private package Unité_Fabrication.Distributeur is
Ligne 937 :
abstract tagged limited null record;
end Unité_Fabrication.Distributeur;
</syntaxhighlight>
</source>
Nous ne pouvons exclure le cas où l'on demanderait au distributeur de confectionner un Produit épuisé, ou simplement celui où un ingrédient viendrait à manquer en cours de fabrication. La procédure Servir_Dose de chaque implémentation doit lever l'exception Confection_Impossible si elle échoue pour quelque raison que ce soit.
 
A partir de ce type de base, on dérive différents types de distributeurs d'ingrédients concrets, correspondant aux différents modèles d'appareils physiques: distributeur de liquide, de poudre, distributeur élémentaire (petite cuillère ou paquet de cacahuète). Certains produits peuvent avoir besoin de paramètres: quantité fournie, temps de réaction d'un relais, que l'on fournira grâce à des discriminants supplémentaires lors de la dérivation; encore faut-il définir quelque part les types correspondants. Où? Ces types sont des types globaux pour le sous-système de l'unité de fabrication; le plus simple est de les mettre dans la spécification de Unité_Fabrication, ce qui les rendra visibles pour toutes les unités enfants. Mais comme il n'y a pas de raison de les rendre visible de l'extérieur du sous-système, il faut les mettre dans la partie privée de Unité_Fabrication. Comme il n'y a pas de modification de la partie visible du paquetage, nous savons que ceci n'aura aucune conséquence sur les couches de plus haut niveau. Le paquetage devient donc:
<sourcesyntaxhighlight lang="ada">
with Définition_Produit; use Définition_Produit;
package Unité_Fabrication is
Ligne 952 :
cl : constant Volume_Livré := 1;
end Unité_Fabrication;
</syntaxhighlight>
</source>
:Remarquer la définition de la constante ms: elle autodocumente que l'unité de mesure est la milliseconde et permet d'écrire une valeur sous la forme (plus lisible) 3*ms. En cas d'évolution du logiciel, si par exemple on décide d'utiliser le dix-millième de seconde comme unité du type, il suffira de changer cette valeur pour ne pas perturber les utilisateurs. La même remarque s'applique à cl.
 
Pour le distributeur de boissons, nous avons besoin de trois sortes de distributeurs d'ingrédients: le modèle simple qui laisse tomber un objet (petite cuillère), paramétré en fonction du temps pour s'assurer que l'objet est bien tombé, le distributeur de poudre (sucre, cacao) et le distributeur de liquide, paramétré par la quantité à distribuer. Ces paquetages sont des enfants privés du distributeur. Ceci nous donne:
<sourcesyntaxhighlight lang="ada">
with Unité_Fabrication.Distributeur;
with System.Storage_Elements; use System.Storage_Elements;
Ligne 991 :
new Distributeur.Instance (Adresse) with null record;
end Unité_Fabrication.Distributeur_Liquide;
</syntaxhighlight>
</source>
Nous n'avons défini pour l'instant que des types de distributeurs; il nous faut maintenant définir des instances. On peut comparer ceci au montage de l'appareil réel: nous allons chercher dans les stocks des distributeurs d'ingrédients afin de les monter dans l'appareil. L'ensemble des distributeurs montés constitue la configuration de l'appareil. Nous pouvons décrire ceci comme:
<sourcesyntaxhighlight lang="ada">
with Unité_Fabrication.Distributeur_Simple;
with Unité_Fabrication.Distributeur_Poudre;
Ligne 1 011 :
(Adresse => 16#1FE#, Volume => 25*cl);
end Unité_Fabrication.Configuration;
</syntaxhighlight>
</source>
Une recette est une liste d'ingrédients associée à un produit. La recette d'un produit est fournie par un livre de recettes. En bonne logique, il faut une opération pour ajouter une recette au livre; en pratique, on peut supposer que les recettes sont construites dans le corps du paquetage. On pourra ajouter cette fonctionnalité par la suite, par exemple pour permettre de changer les recettes «sur place» au moyen d'un terminal portable. Ce paquetage est une machine abstraite qui n'est utilisée que par l'unité de fabrication; nous en faisons donc un paquetage enfant.
<sourcesyntaxhighlight lang="ada">
with Unité_Fabrication.Distributeur, Définition_Produit;
use Unité_Fabrication.Distributeur, Définition_Produit;
Ligne 1 024 :
function La_Recette (De : Produit) return Recette;
end Unité_Fabrication.Livre_de_recettes;
</syntaxhighlight>
</source>
Nous pouvons maintenant écrire le corps de Unité_Fabrication:
<sourcesyntaxhighlight lang="ada">
with Unité_Fabrication.Distributeur,
Unité_Fabrication.Livre_de_recettes;
Ligne 1 039 :
end Confectionner;
end Unité_Fabrication;
</syntaxhighlight>
</source>
Nous avons pu supprimer les dépendances à ADPT: le corps de ce paquetage est définitif.
 
<li>Tester le plan d'abstraction</li>
Il manque encore l'implémentation des différents distributeurs d'ingrédients, ainsi que du livre de recettes. On peut facilement fournir des corps maquettes pour les ingrédients, comme:
<sourcesyntaxhighlight lang="ada">
with ADPT;
package body Unité_Fabrication.Distributeur_Simple is
Ligne 1 055 :
Temps_Ouvert'Image (De.Ouverture) & " ms");
end Servir_Dose;
</syntaxhighlight>
</source>
end Unité_Fabrication.Distributeur_Simple;
On trouvera en annexe les autres corps maquettes correspondant aux différentes formes de distributeurs. En ce qui concerne le livre de recettes, on peut lui fournir un premier corps comme:
<sourcesyntaxhighlight lang="ada">
with Unité_Fabrication.Configuration;
use Unité_Fabrication.Configuration;
Ligne 1 077 :
end La_Recette;
end Unité_Fabrication.Livre_de_recettes;
</syntaxhighlight>
</source>
Bien sûr, à terme il faudra remplacer ce corps en utilisant une table, mais celui-ci est suffisant pour nous permettre de tester l'ensemble du logiciel, y compris dans ses aspects temporels.