programmez-en-d/contrats.whata

332 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Programmation par contrat"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
La programmation par contrat est une approche en conception logicielle qui traite les parties d'un logiciel comme des entités individuelles qui se rendent mutuellement des services. Cette approche considère que le programme peut fonctionner en accord avec sa spécification tant que le fournisseur et le consommateur du service obéissent tous deux à un [* contrat].
Les fonctionnalités du D en matière de programmation par contrat considèrent les fonctions comme les unités de services du logiciel. Comme les tests unitaires, la programmation par contrat est basée sur les assertions.
La programmation par contrat en D est implémentée avec trois types de blocs de code~ :
- les blocs [c in] des fonctions~ ;
- les blocs [c out] des fonctions~ ;
- les blocs [c invariant] des structures et des classes.
Nous verrons les blocs [c invariant] et l'[* héritage par contrat] dans un chapitre ultérieur, après avoir couvert les structures et les classes.
[ = Les blocs [c in] pour les conditions d'entrée
L'exécution correcte des fonctions dépend habituellement de la correction des valeurs de leurs paramètres. Par exemple, une fonction racine carrée peut avoir besoin que son paramètre ne soit pas négatif. Une fonction qui traite des dates peut avoir besoin que le numéro de mois soit compris entre 1 et 12.
Nous avons déjà vu de telles vérifications dans le [[part:assert | chapitre sur [c assert] et [c enforce]]]. Les conditions sur les valeurs des paramètres peuvent être forcées par des assertions à l'intérieur des définitions de fonctions~ :
[code=d <<<
string momentVersChaine(in int heure, in int minute)
{
assert((heure >= 0) && (heure <= 23));
assert((minute >= 0) && (minute <= 59));
return format("%02s:%02s", heure, minute);
}
>>>]
En programmation par contrat, les mêmes vérifications sont écrits dans les blocs [c in] des fonctions. Quand un bloc [c in] ou [c out] est utilisé, le contenu réel de la fonction doit être placé dans un bloc [c body]~ :
[code=d <<<
import std.stdio;
import std.string;
string momentVersChaine(in int heure, in int minute)
in
{
assert((heure >= 0) && (heure <= 23));
assert((minute >= 0) && (minute <= 59));
}
body
{
return format("%02s:%02s", heure, minute);
}
void main()
{
writeln(momentVersChaine(12, 34));
}
>>>]
Un avantage d'un bloc [c in] est que toutes les préconditions sont rassemblées et séparée du corps de la fonction lui-même. De cette manière, le corps de la fonction est libre de toute assertion concernant les préconditions. Au besoin, il est toujours possible est même conseillé d'avoir d'autres assertions dans le corps de la fonction qui ne correspondent pas à des préconditions et qui protégeraient des erreurs de programmation dans le corps de la fonction.
Le code qui est à l'intérieur du bloc [c in] est exécuté automatiquement à chaque fois que la fonction est appelée. L'exécution de la fonction elle-même ne commence que si toutes les assertions du bloc [c in] passent. Cela prévient l'exécution de la fonction avec des préconditions non respectée et, par conséquent, évite de produire des résultats incorrects.
Toute assertion qui échoue dans le bloc [c in] indique que le contrat a été violé par l'appelant.
]
[ = Les blocs [c out] pour les postconditions
L'autre côté du contrat implique les garanties que la fonction donne. Un exemple de fonction avec une postcondition serait une fonction qui retourne le nombre de jour en février~ ; la valeur retournée sera toujours 28 ou 29.
Les postconditions sont vérifiées dans les blocs [c out] des fonctions.
La valeur qu'une fonction retourne avec l'instruction [c return] n'a pas besoin d'être définie comme une variable à l'intérieur la fonction, il n'y a habituellement pas de nom faisant référence à cette valeur de retour. Ceci peut être problématique parce que les assertions dans le bloc [c out] ne peuvent pas utiliser la valeur de retour par son nom.
D propose une solution à ce problème en donnant un moyen de nommer la valeur de retour juste après le mot-clé [c out]. Ce nom représente précisément la valeur que la fonction est en train de retourner~ :
[code=d <<<
int joursEnFevrier(in int annee)
out (resultat)
{
assert((resultat == 28) || (resultat == 29));
}
body
{
return estAnneeBissextile(annee) ? 29 : 28;
}
>>>]
Même si [c resultat] est un nom raisonnable pour une valeur de retour, d'autre noms parfaitement valides peuvent aussi être utilisés.
Certaines fonctions n'ont pas de valeur de retour ou la valeur de retour n'a pas besoin d'être vérifiée. Dans ce cas, le bloc [c out] n'oblige pas à spécifier un nom :
[code=d <<<
out
{
// ...
}
>>>]
De manière similaire aux blocs [c in], les blocs [c out] sont exécutés automatiquement après l'exécution du corps de la fonction.
Une assertion qui échoue dans un bloc [c out] indique que le contrat a été violé par la fonction.
Évidemment, les blocs [c in] et [c out] sont optionnels. En incluant les blocs [c unittest], qui sont également optionnels, les fonctions D peuvent être constituées d'un nombre de blocs pouvant aller jusqu'à 4~ :
- [c in]~ : optionnel~ ;
- [c out]~ : optionnel~ ;
- [c body]~ : obligatoire mais le mot-clé [c body] peut être omis s'il n'y a pas de bloc [c in] ni de bloc [c out]~ ;
- [c unittest]~ : optionnel et ne fait techniquement pas partie de la définition de la fonction mais est couramment défini juste après la fonction.
Voici un exemple qui utilise tous ces blocs~ :
[code=d <<<
import std.stdio;
/*
* Distribue la somme entre deux variables.
*
* Distribue d'abord à la première variable, mais ne lui donne
* jamais plus de 7. Le reste de la somme est distribué
* à la seconde variable.
*/
void distribuer(in int somme, out int premiere, out int seconde)
in
{
assert(somme >= 0);
}
out
{
assert(somme == (premiere + seconde));
}
body
{
premiere = (somme >= 7) ? 7 : somme;
seconde = somme - premiere;
}
unittest
{
int premiere;
int seconde;
// Les deux doivent valoir 0 si la somme vaut 0
distribuer(0, premiere, seconde);
assert(premiere == 0);
assert(seconde == 0);
// Si la somme est plus petite que 7, tout doit être donné
// à premiere
distribuer(3, premiere, seconde);
assert(premiere == 3);
assert(seconde == 0);
// Test d'une condition limite
distribuer(7, premiere, seconde);
assert(premiere == 7);
assert(seconde == 0);
// Si la somme est plus grande que 7, la première doit recevoir 7
// et le reste doit être donné à la seconde
distribuer(8, premiere, seconde);
assert(premiere == 7);
assert(seconde == 1);
// Une grande valeur quelconque
distribuer(1_000_007, premiere, seconde);
assert(premiere == 7);
assert(seconde == 1_000_000);
}
void main()
{
int premiere;
int seconde;
distribuer(123, premiere, seconde);
writeln("premiere : ", premiere, ", seconde : ", seconde);
}
>>>]
Le programme peut être compilé et exécuté dans la console avec la commande suivante :
[code <<<
$ dmd essai.d -w -unittest // ou : gdc -Wall -funittest essai.d -o essai
$ ./essai
premiere : 7, seconde : 116
>>>]
Même si le travail de la fonction lui-même consiste en seulement deux lignes, il y a un total de 19 lignes non triviales qui prennent en charge sa fonctionnalité. On peut discuter du fait qu'autant de code en plus est superflu pour une si petite fonction. Cependant, les bogues ne sont jamais intentionnels. Le programmeur écrit toujours du code qui [* devrait] fonctionner correctement, et qui finit souvent par contenir divers types de bogues.
Quant les attentes sont explicitement spécifiées dans les tests unitaires et dans les contrats, les fonctions qui sont initialement correctes ont plus de chance de rester correctes. Je vous recommande de tirer parti de toutes les fonctionnalités qui améliorent la correction des programmes. Les tests unitaires comme les contrats sont des outils efficaces pour cela. Ils aident à réduire le temps passé à déboguer (au dépens du temps passé à écrire le code).
]
[ = Désactiver la programmation par contrats
Contrairement au test unitaire, la programmation par contrat est activée par défaut. L'option [c -release] de [c dmd] et les options [c -fno-in] [c -fno-out] [c -fno-invariants] ou [c -frelease] de [c gdc] désactivent la programmation par contrats~ :
[code=bash <<<
dmd essai.d -w -release // ou gdc essai.d -Wall -frelease essai
>>>]
Quand la programmation par contrat est désactivée, le contenu des blocs [c in], [c out] et [c invariant] est ignoré.
]
[ = Blocs [c in] [* versus] [c enforce]
Nous avons vu dans le [[part:assert | chapitre sur [c assert] et [c enforce]]] qu'il est parfois difficile de choisir entre [c assert] ou [c enforce] pour faire des vérifications. De manière similaire, il est parfois difficile de choisir entre les assertions dans des blocs [c in] et des vérifications avec [c enforce] à l'intérieur du corps des fonctions.
La possibilité de désactiver la programmation par contrat est une indication que celle-ci est là pour protéger contre les erreurs de programmation. Pour cette raison, la décision devrait ici être basée sur les mêmes règles que ce que nous avons vues dans le [[part:assert | chapitre sur [c assert] et [c enforce]]~ :
- Si une vérification est là pour prévenir une erreur de programmation, alors elle devrait être dans le bloc [c in]. Par exemple, si la fonction est appelée seulement depuis d'autres endroits du programme pour construire une fonctionnalité, les valeurs des paramètres sont entièrement sous la responsabilité du programmeur. Pour cette raison, les préconditions d'une telle fonction devraient être vérifiées dans son bloc [c in].
- [
Si la fonction ne peut pas jouer son rôle pour n'importe quelle autre raison, des valeurs de paramètres incorrects inclus, alors elle doit lever une exception et [c enforce] est un moyen pratique de le faire.
Pour voir un exemple de cela, définissons une fonction qui retourne une tranche du milieu d'une autre tranche. Supposons que cette fonction est là pour être utilisée par les utilisateurs du module, et non une fonction interne utilisée uniquement par le module lui-même. Comme les utilisateurs de ce module peuvent appeler cette fonction avec divers paramètres potentiellement incorrects, il peut être approprié de vérifier les valeurs des paramètres à chaque fois que la fonction est appelée. Il serait insuffisant de ne les vérifier que pendant le développement, après lequel les contrats peuvent être désactivés.
Pour cette raison, la fonction suivante vérifie ses paramètres en appelant [c enforce] dans le corps de la fonction au lieu d'une assertion dans le bloc [c in]~ :
[code=d <<<
import std.exception;
inout(int)[] milieu(inout(int)[] trancheOriginale, size_t largeur)
out (resultat)
{
assert(resultat.length == largeur);
}
body
{
enforce(trancheOriginale.length >= largeur);
immutable debut = (trancheOriginale.length - largeur) / 2;
immutable fin = debut + largeur;
return trancheOriginale[debut .. fin];
}
unittest
{
auto slice = [1, 2, 3, 4, 5];
assert(milieu(slice, 3) == [2, 3, 4]);
assert(milieu(slice, 2) == [2, 3]);
assert(milieu(slice, 5) == slice);
}
void main()
{}
>>>]
Le problème se pose moins pour les blocs [c out]. Comme la valeur de retour de toute fonction relève de la responsabilité du programmeur, les postconditions doivent toujours être vérifiées dans le bloc [c out]. La fonction que l'on vient de voir suit cette règle.
]
- Un autre critère à considérer lors du choix entre les blocs [c in] et [c enforce] est de se demander si la situation est rattrapable. Si elle est rattrapable par les couches de code de plus haut niveau, alors il peut être plus approprié de lever une exception (ce qu'[c enforce] rend pratique).
]
[ = Exercice
Écrivez un programme qui augmente le nombre total de points de deux équipes de foot selon le résultat d'un match.
Les deux premiers paramètres de cette fonction sont les buts que les deux équipes ont marqués. Les deux autres paramètres sont les points de chaque équipe avant le match. Cette fonction doit ajuster les points des équipes selon les buts qu'elles ont marqués. Pour rappel, l'équipe gagnante prend 3 points et l'équipe perdante ne prend pas de point. Dans le cas d'un match nul, les deux équipes prennent un point chacune.
De plus, la fonction devrait indiquer l'équipe gagnante~ : 1 si la première équipe a gagné, 2 si la seconde équipe a gagné, 0 si le match a fini par un match nul.
Partez du programme suivant et complétez les quatre blocs de la fonction de façon appropriée. Ne supprimez pas les assertions dans la fonction [c main]~ ; elles montrent comment cette fonction doit se comporter.
[code=d <<<
int ajouterPoints(in int buts1,
in int buts2,
ref int points1,
ref int points2)
in
{
// ...
}
out (resultat)
{
// ...
}
body
{
int gagnante;
// ...
return gagnante;
}
unittest
{
// ...
}
void main()
{
int points1 = 10;
int points2 = 7;
int gagnante;
gagnante = ajouterPoints(3, 1, points1, points2);
assert(points1 == 13);
assert(points2 == 7);
assert(gagnante == 1);
gagnante = ajouterPoints(2, 2, points1, points2);
assert(points1 == 14);
assert(points2 == 8);
assert(gagnante == 0);
}
>>>]
[p Note~ : | il peut être plus judicieux de retourner une valeur énumérée depuis cette fonction~ :]
[code=d <<<
enum ResultatJeu
{
premiereGagne, secondeGagne, nul
}
ResultatJeu ajouterPoints(in int buts1,
in int buts2,
ref int points1,
ref int points2)
// ...
>>>]
J'ai choisi de retourner un [c int] pour cet exercice, de cette manière, la valeur de retour peut être comparée aux valeurs 0, 1 et 2.
[[part:corrections/contrats | … La solution]]
]