Mokona Guu Center

Passer des contrats avec son langage de programmation.

Publié le

« je ne fais pas de test car il est impossible que le pointeur soit NULL », «je pourrais vérifier que la valeur est positive avant de faire ma racine carrée, mais ça ferait perdre du temps »,... Une fois encore, je pars de phrases vues et entendues, que ce soit dans l'univers amateur ou professionnel, pour aborder un sujet en programmation.

Ce sujet est probablement connu de la grande majorité des professionnels, même si certains n'en sont pas des adeptes, mais il est parfois inconnu des amateurs ou des débutants qui n'ont pas étudié de langage de programmation y étant fortement lié.

Le concept est celui du « contrat ».

Petite présentation

Le contrat, en programmation, est un descendant de la formalisation des langages. Beaucoup se sont essayés à la difficile tâche de preuve des programmes. L'informatique étant très liée aux mathématiques, il est raisonnable de se poser la question : peut-on prouver que mon programme fait ce que j'attends de lui ? Tout comme l'on prouve un théorème.

Le sujet est vaste et a donné naissance à des méthodes de formalisation et leurs langages, comme la notation Z ou la méthode B.

Sans aller jusqu'à de la preuve ou de la formalisation, le besoin d'avoir un moyen de s'assurer de la validité de certaines valeurs s'est fait sentir dans les langages de programmation. C'est ainsi que sont nés les contrats.

Les contrats sont un ensemble de conditions nommées « assertions » qui peuvent être des pré-conditions à l'exécution d'un morceau de programme, des post-conditions vérifiant le résultat de ce morceau ou des invariants vérifiant que certaines valeurs ou états restent toujours dans un domaine déterminé.

C'est le langage Eiffel qui a apporté aux langages de programmation le concept de programmation par contrat. Ces concepts ont été repris et ajoutés dans des langages déjà existants ou contemporains (assert() en C++) ou intégrés dans des langages créés plus tard (les clauses in/out ainsi que les invariants de classes en D).

De l'utilité des contrats

Tout ça, c'est bien joli, mais est-ce que ça n'est pas un peu compliqué et surtout, est-ce que ça n'est pas du temps perdu ?

Il y a au moins deux façons de mal aborder les contrats lorsqu'on les découvre:

  • cela peut-être remplacé par des tests d'erreurs ;
  • c'est du code inutile qui ralenti le programme pour vérifier des choses évidentes.

Nous verrons par la suite qu'il ne faut pas confondre les tests d'erreurs et les contrats. Intéressons-nous d'abord au deuxième point.

Les contrats peuvent sembler inutile et une perte de temps, tant à l'écriture qu'à l'exécution. Ce n'est pas le seul outil en programmation qui entraîne ce type de réaction lorsqu'il n'est pas utilisé sérieusement (ou lorsque, pour une raison ou une autre, on refuse de l'utiliser). Mais comme beaucoup de ces outils,perdre du temps d'écriture sur le moment en fait potentiellement gagner beaucoup plus ultérieurement.

Comment en être certain ? Par expérience principalement. La recherche de bugs bien cachés venant de fonctions ne faisant plus tout à fait ce qu'elles sont censées faire ou bien travaillant sur des données qui ne sont pas valides fait rapidement comprendre que s'il y avait eu des tests de validité, on aurait pu éviter des moments pénibles 1.

Avec l'utilisation des contrats, on pourra au contraire sourire à chaque fois qu'un assert se déclenchera pour avertir que quelque chose ne va pas.

La perte de temps à l'exécution est bien réelle et c'est pour cela qu'écrire des contrats est plus intéressant qu'écrire des tests dans tous les coins et recoins du programme. En effets, les contrats sont des morceaux de programme optionnels. Ils peuvent, à la compilation (en C++ par exemple) ou à l'exécution (en Java par exemple), être désactivés.

Tout ce qui fait parti des contrats n'a alors plus d'influence. Les tests ne sont plus faits 2.

La profusion d'assert peut aussi être gênant pour des logiciels qui doivent tourner rapidement. Là, c'est l'expérience de leur utilisation qui doit parler. En C++, par exemple, il est possible de compiler une partie seulement du programme avec les assert() activés. Ou bien aussi définir ses propres assert() (voir plus loin) et les munir d'un niveau de sévérité.

Quand utiliser un contrat ?

Je viens de l'évoquer, les contrats ont un coût. Ils ne sont pas non plus silourds. Il ne faut donc pas hésiter à les employer.

Le cas le plus évident est la protection d'une fonction (ou d'un morceau de programme) qui n'est pas définie pour certaines valeurs pour lesquelles il serait dangereux de continuer. Ici intervient un arbitrage sur la dangerosité de continuer ou pas.

Par exemple, si une fonction fait une division, il est dangereux de laisser passer un dénominateur nul, le programme a de bonnes chances de planter (ou de lever une exception). Il est cependant possible de tester ce dénominateur et de traiter un cas particulier dans la fonction. Tout dépend de la définition de la fonction.

Si on traite le cas particulier, la fonction sera pour toujours accompagnée d'un code de test potentiellement ralentisseur et inintéressant si le cas particulier est vraiment une erreur. Si on ne traite pas le cas particulier,le contrat nous assure que la fonction ne sera pas appelée silencieusement sans avoir de retour.

Si on ne traite pas le cas particulier ET que l'on ne s'assure pas d'un contrat, on sera parfois prévenu par un crash immédiat (ou une exception) et parfois, cela entraînera des bugs plus obscures, conséquences de cette valeur invalide.

En passant, on voit alors un attrait supplémentaire du contrat : il documente la fonction au niveau du programmeur qui doit travailler dessus. En effet, si le choix a été fait de refuser le dénominateur nul, la présence du contrat en avertira l'utilisateur de la fonction (s'il a accès au code, bien entendu).

Un cas classique de protection de domaine de validité est la protection contre les pointeurs NULL (en C++). On peut décider que, de toute façon, le programme plantera lors de l'accès au pointeur 3.On peut aussi préférer que l'exécution nous indique clairement que quelque chose ne va pas juste avant le plantage.

Si à un moment vous vous dites « bah, de toute façon, ce pointeur ne peut pas être NULL. », c'est qu'il est peut-être prudent d'ajouter un assert. De manière générale, toute réflexion sur l'impossibilité d'avoir un ensemble de valeurs particulières devrait faire sonner une cloche dans votre tête. S'il est impossible qu'une valeur soit atteinte, autant s'en assurer avec un contrat.Cela ne coûte pas grand chose et peut faire gagner beaucoup de temps.

Prudence

Un contrat n'est pas un endroit où faire des choses nécessaires au déroulement du programme. Un contrat ne fait que tester un prédicat plus ou moins complexe. Il faut donc veiller à ce que ce prédicat ne contienne pas d'effets de bords. Autrement dit, le contrat ne doit pas influer sur les objets qu'il teste.

En effet lorsque le programme tournera dans un mode sans contrats, plus rien de ce que faisaient les contrats ne sera exécuté.

Si cela peut sembler évident, il faut garder cette règle en tête car si, sauf moment de fatigue, on n'écrit pas soi même du code de contrat influant le programme, il est tout à fait possible que l'on utilise sans le savoir une fonction qui a un effet de bord.

Prudence aussi sur ce qu'est un contrat. Le contrat s'assure d'un domaine de validité, de conditions d'exécution du programme. Si ces conditions ne sont pas valides, le programme le signale brutalement. De plus, les asserts ne sont pas destinés à être présents dans une version finale d'un logiciel.

Pour ces raisons, il ne faut pas les confondre avec des tests d'erreurs ou du traitement d'exceptions.

Par exemple, traiter la présence d'un fichier sélectionné par l'utilisateur pour son chargement. Si le fichier n'existe pas, c'est une erreur d'utilisation qui doit entraîner un avertissement (message d'erreur, boite d'avertissement ou autre) vers l'utilisateur mais certainement pas l'arrêt du programme ou l'affichage d'une boite incompréhensible à la destination del'équipe de développement 4.

De même si ce fichier n'est pas reconnu par l'application.

Si le fichier est reconnu par l'application mais contient des données non valides, un arbitrage est nécessaire. Le choix entre un assert et une erreur utilisateur va beaucoup dépendre de l'application, de son utilisation et de ses utilisateurs prévus ainsi que de la nature de l'invalidité de la donnée.

Produits dérivés

Les langages qui définissent dans leurs fondations le principe de contrat sont généralement accompagnés de tout ce qu'il faut et leur comportement n'est pas forcément modifiable.

Dans des langages qui n'ont qu'un concept faible du contrat (en gros, la fonction assert() et puis c'est tout) mais qui permettent la compilation decode conditionnel, il est possible d'ajouter ses propres dérivés de l'assert()de base qui souvent affiche le prédicat invalide et s'arrête brutalement.

On peut décliner ses propres assert() de plusieurs manières : un assert() avec affichage graphique, un assert() qui écrit dans un log, un assert() qui signal que le prédicat est faux mais continue son exécution (un warning d'exécution),ce même avertissement qui ne s'affichera qu'une seule fois. Les possibilités sont nombreuses.

Un cousin du contrat à l'exécution est le « static assert ». Disponible dans les langages qui permettent la résolution de code à la compilation (C++ et D par exemple), ils permettent d'évaluer des conditions lors de cette compilation plutôt que d'attendre l'exécution. Cela ne concerne bien entendu que ce qui peut être déterminé à ce moment.

Par exemple, une fonction peut vouloir s'assurer que le sizeof() d'une classe n'a pas changé. Ainsi, lorsqu'il change, le programmeur est averti qu'une fonction a besoin d'une d'attention particulière. Cela peut être aussi la vérification d'une constante qui doit rester entre certaines bornes pour des raisons matérielles.

Assertons

J'espère vous avoir convaincu du bien fondé des contrats en programmation. Ou au minimum de vous avoir donné envie d'y jeter un coup d'œil.


  1. ces moments qui arrivent toujours en même temps qu'une deadline importante. 

  2. cela veut signifie donc que les bugs qui n'apparaissent que dans un mode sans assert() ne peuvent pas bénéficier des assert(). C'est une tautologie, mais c'est important à retenir. 

  3. ce qui n'est pas forcément le cas ! Il existe des cas un peu exotiques d'accès à des pointeurs NULL qui passent. 

  4. il n'est cependant pas exclu d'accompagner le message d'erreur utilisateur par l'écriture dans un journal de détails supplémentaires à destination des développeurs pour que ceux-ci comprennent le déroulement de l'utilisation lors d'une erreur plus grave.