• Aucun résultat trouvé

2.1 Syntaxe

2.2.5 Héritage et sous-typage multiple

Comme évoqué dans la Section 1.2, la programmation par contrat offre un support à la mise en oeuvre des langages à objet, avec notamment une prise en charge de l’héritage. Basé sur un concept de réutilisabilité et d’adaptabilité, l’héritage offre notamment la possibilité au programmeur de redéfinir des fonctionnalités héritées dans le but de les spécialiser afin de satisfaire à des exigences spécifiques. De la même manière qu’il est possible de redéfinir l’implémentation de la méthode lors de l’héritage, il est aussi possible de redéfinir l’implémentation de ses contrats. Toutefois, la redéfinition des contrats suit une règle spécifique. Effectivement, comme l’ont défini Liskov et Wing (Liskov et Wing, 1994) une instance d’un type A doit pouvoir être remplacée par une instance de type B, tel que B sous-type de A, sans que cela ne modifie la cohérence du programme. En effet, grâce au polymorphisme le choix de l’implémentation à appeler sera effectué à l’exécution en fonction du type dynamique de l’instance qui sera associé à l’appel, il est alors primordial que peu importe le sous-type associé à l’appel l’implémentation sélectionnée les contrats initialement définis seront respectés.

L’exemple illustré par la Figure 2.9 va nous permettre d’illustrer nos propos. Nous avons une hiérarchie de classes où B est un sous-type de A. A introduit une méthode foo et B la spécialise. Un client peut donc utiliser deux implémentations de la méthode foo en fonction du type dynamique soit A ou B. Cet exemple va permettre d’identifier les différentes sémantiques d’héritages à l’oeuvre en fonction

du type de contrat : pré-condition, post-condition et invariant de classe. 1 class A 2 fun foo do #...# 3 end 4 5 class B 6 super A 7 fun foo do #...# 8 end

Figure 2.9 Relation d’héritage en Nit.

Les pré-conditions sont définies comme contravariantes. En effet, si la classe B vient renforcer les pré-conditions de la méthode foo cela voudrait dire que cette nouvelle spécialisation viendrait rendre certains appels qui étaient corrects du point de vue du contrat initialement défini dans A, incorrect du point de vue de la nouvelle spécialisation fournie par B. Or, cette situation ne respecte pas le principe de Liskov (Liskov et Wing, 1994). Pour que ce principe soit respecté la pré-condition de la méthode foo de B doit accepter l’ensemble des éléments qui avait été défini comme acceptable dans la spécification de la version initiale. La spécialisation d’une pré-condition peut alors uniquement être affaiblie, cet affaiblissement revient à dire que la nouvelle définition peut prendre une gamme d’entrée plus large.

Les post-conditions définissent la relation inverse et sont considérées comme co- variants. En effet, si la classe B vient affaiblir les post-conditions de la méthode foo cela signifie que la méthode peut retourner un résultat non acceptable du point de vue du client. Or, comme énoncé précédemment, la spécialisation permet d’adapter la méthode à un nouveau contexte et non d’effectuer un travail com- plètement différent. Les post-conditions ne peuvent donc pas être affaiblies, mais

peuvent seulement être renforcées afin de venir restreindre l’ensemble des résultats acceptables.

L’héritage pour les invariants de classe se base sur le principe suivant, une instance de la classe B peut être considérée comme une instance particulière de A, il est donc nécessaire que l’ensemble des contraintes de cohérence qui s’applique à une instance de A s’applique aussi à une instance de B. C’est pourquoi les invariants de classe sont définis comme covariants de la même manière que les post-conditions. Les invariants de classe peuvent alors seulement être renforcés afin de garantir que l’état d’une instance de B sera acceptable du point de vue d’une instance de A. L’invariant d’une classe est alors représenté par la conjonction de l’ensemble des invariants hérités ainsi que de ceux définis dans celle-ci. L’invariant d’une sous classe est ainsi toujours plus fort ou égal aux invariants de chacun de ces parents. Nous pouvons résumer la mise en oeuvre de la sémantique d’héritage des contrats de la manière suivante :

— La pré-condition d’une méthode peut se traduire par une disjonction entre l’ensemble des pré-conditions héritées.

— La post-condition d’une méthode peut se traduire par une conjonction entre l’ensemble des post-conditions héritées.

— L’invariant d’une classe peut se traduire par une conjonction entre l’en- semble des invariants hérités.

Grâce à cette représentation, nous pouvons garantir que lors de la mise en oeuvre de l’héritage, la clause totale d’une pré-condition sera équivalente ou plus faible que la spécification originale et que la clause totale d’une post-condition et de l’invariant de classe sera plus forte ou équivalente à la spécification originale. Cette sémantique d’héritage est mise en oeuvre de manière identique dans l’en-

semble des implémentations étudiées. Toutefois, nous pouvons noter que certains choix de définition syntaxiques ne se prêtent pas à une mise en oeuvre systématique de cette sémantique. C’est notamment le cas avec la méthode utilisée par JCon- tractor (Abercrombie et Karaorman, 2002; Karaorman et Abercrombie, 2005) qui est d’utiliser des méthodes dédiées ainsi que pour la définition des contrats à l’aide d’aspect, ce sera alors au programmeur de prendre en charge cette séman- tique d’héritage. Nous pouvons aussi noter une différence d’interprétation lors de la redéfinition de méthode pour le langage D (D Language Foundation, 2020), où lorsqu’une méthode est redéfinie sans pré-condition cela signifie que toutes les valeurs en entrée sont maintenant considérées comme acceptables.

2.3 Conclusion

En conclusion, la programmation par contrats passe par la mise en oeuvre d’une syntaxe et d’une sémantique associée. Toutefois, celles-ci varient fortement en fonction des implémentations et du langage hôte où les contrats sont mis en oeuvre. Le Tableau 2.1 résume les différentes caractéristiques des implémentations étudiées dans ce chapitre. Bien qu’évoquées dans ce chapitre les implémentations Jass (Bartetzko et al., 2001) ainsi que JContractor (Abercrombie et Karaorman, 2002) ne sont plus d’actualité, il est toutefois pertinent de les citer afin d’avoir un panel de comparaison plus large sur les différentes stratégies de réalisation.

Jass JContractor OpenJML

AspectJML Cofoja D Eiffel

Code Contracts Syntaxe Mots-clés (2.1.1) × × Commentaire (2.1.2) × × Annotation (2.1.3) × Méthode (2.1.4) × × Sémantique Politique de visibilité (2.2.1) × × × Vérouillage (2.2.2) × × × Configuration (2.2.2) × × × × Résult (2.2.4) × × × × × × × Old (2.2.4) × × × × × × Héritage (2.2.5) × × × × ×

Tableau 2.1 Résumé de la mise en oeuvre syntaxique et sémantique de la pro- grammation par contrat dans les solutions étudiées.

ÉTUDE DES APPROCHES ET IMPLÉMENTATIONS

Ce chapitre fait l’étude de la mise en oeuvre technique des implémentations sui- vantes : les compilateurs ISE Eiffel (Eiffel, 2019) et Liberty Eiffel (Liberty Eif- fel, 2019), le compilateur DMD (D Language Foundation, 2020), le préprocesseur Code Contracts (Microsoft, 2018),les compilateurs OpenJML (Cok, 2014), Aspect- JML (Rebêlo et al., 2014) et le préprocesseur Cofoja (Minh Lê, 2016). Dans un premier temps, la Section 3.1 aborde l’aspect d’analyse du code fourni par le pro- grammeur afin de traiter les contrats. Nous identifions deux méthodes principales afin d’analyser et de générer du code source, à savoir l’utilisation de préprocesseur (3.1.1) et la définition d’un compilateur spécifique (3.1.2). Dans un second temps, la Section 3.2 détaille les différentes stratégies mises en oeuvre pour effectuer la vérification des contrats, avec l’étude de la représentation des contrats dans la Sous-Section 3.2.1, la Sous-Section 3.2.2 aborde la mise en oeuvre de la représen- tation des différentes règles d’héritage définies dans la Section 2.2.5, pour finir nous abordons dans la Sous-Section 3.2.3 le contrôle de l’évaluation des contrats.

3.1 Analyse et génération des contrats

Documents relatifs