Developpez.com

Club des développeurs et IT pro
Plus de 4 millions de visiteurs uniques par mois

Débat : Redéfinition de méthode et principe de substitution de Liskov

Le , par Ekinoks, Membre averti
Salut !

J'aurais quelque questions sur des problèmes de redéfinition de méthode en C++. Comme je n'ai pas réussi à trouver de solution (il en existe peu être pas), je vous soumet mes questions ^^;

1> Il m'arrive fréquemment de vouloir empêcher la redéfinition d'une méthode pour les objet qui hérite de ma classe. Il me semble qu'en Java il existe le mot clé "final" pour faire ca... Es qu'il existe un équivalant en C++ ?

2> Es qu'il existe un moyen de forcer l'exécution d'une méthode même si celle ci a était redéfinie et ce a partir de la classe mère ?
Exemple :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
struct A { 
   virtual void f() { 
      cout<<"code A"<<endl; 
   } 
}; 
 
struct B : public A { 
   virtual void f() { 
      A::f(); // Existe t'il un moyen de faire cette appelle automatiquement sans que la classe fille est a le faire ? 
      cout<<"code A"<<endl; 
   } 
};
3> Existe t'il un moyen de redéfinir des méthodes templates dans une classe fille ?

Merci pour votre aide.


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster une réponse

Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 12:11
Citation Envoyé par 3DArchi  Voir le message
Le comportement n'est pas le même lorsque pa est un B si la vérification est activée ou non : fooCertified verra la pré-condition non validée, donc STOP. Alors que pour fooUncertified il n'y a plus vérification de la pré-condition, donc GO alors qu'on voulait pas.

Non, si tu adapte la pré condition de B de manière cohérent (qu'elle renvoie vrai quand la pré condition est valide) elle renvoie GO quand un A valide aurait renvoyé GO et STOP quand un A invalide aurait renvoyé STOP, le fait que la précondition ait été adaptée ne change pas le comportement de validation, donc le comportement général reste tout à fait identique (modulo les adaptations nécessaires à la prise en compte des spécifications de B)
Sauf que LSP dit que partout où tu as un A, tu dois pouvoir y substituer un B. Or en restreignant les pré-conditions et en ne les vérifiant pas, tu ne peux plus substituer un B à un A sans avoir une erreur.

Oui mais le noeud c'est le LSP qui dit que tu dois pouvoir substituer un B à un A. Donc une fonction qui manipule un A doit pouvoir manipuler un B sans restriction.

A te lire, pour respecter le LSP nous devrions nous limiter à un héritage d'inclusion (où une classe dérivée ne peut que rajouter des comportement sans adapter ceux de la classe de base) ou à un héritage de réalisation (ou la classe dérivée ne fait que fournir des valeurs particulières à la classe de base)...

Je suis désolé, mais je n'estimes pas être en désaccord avec LSP si j'ai un objet dérivé d'un objet de base, dont je peux sémantiquement dire qu'il est effectivement un objet de base (il ne faut pas que j'essaye de faire passer un chien pour un véhicule ), mais dont j'ai adapté un comportement (j'ai redéfini une méthode dans l'objet dérivé) et que ce comportement adapté poursuit le même objectif et fournit un résultat cohérent par rapport au comportement "de base" (et donc que l'on évite que le comportement de base consiste à crier alors que le comportement adapté consiste à manger, et que les deux comportements renvoient un GO si c'est valide et un STOP si c'est invalide)

La substitution est parfaite: je peux substituer un objet dérivé à un objet de base sans que je ne fasse mentir en quoi que ce soit l'invocation des différents comportements que l'on attend de la part de l'objet de base.
Je me demande si la confusion ne vient pas du fait que B peut être un sous-type de A, c'est à dire avoir un espace de valeur 'inférieure'. Mais là, je recite white_tentacle :
On dit souvent qu'un des "problèmes" de la programmation par contrat, c'est que B::f() inclut une précondition évidente : "l'appelant est un B", et que cette précondition est plus restrictive que celle de A::f() qui est "l'appelant est un A". C'est effectivement vrai, mais c'est un mauvais argument, dans le sens ou cette précondition est garantie par le compilateur, et ne fait pas réellement partie du contrat.


Mais pour moi, la condition n'est qu'un comportement, destiné à donner le GO ou le STOP...

A partir de là, il n'y a aucune raison pour empêcher - toujours en gardant en tête que le GO est donné si c'est valide et le STOP si c'est invalide - une adaptation de la condition à la situation réelle de l'objet en cours de traitement, même si on le fait passer pour un objet de base
Citation Envoyé par white_tentacle  Voir le message
Pars des hypothèses suivantes :

- la précondition doit être vérifiable par l'appelant (ie, l'appelant doit être en mesure de vérifier qu'il respecte sa part du contrat)
- le test du contrat dans l'appelé est totalement facultatif, il n'est là théoriquement qu'en phase de debug

Hé bien, en quoi est-ce que je ne respecte pas ces hypothèses

En faisant de la condition un comportement publique de l'objet traité, je m'assure qu'il sera utilisable de n'importe quel endroit où on trouve cet objet et donc où la condition est susceptible d'être testée

Au pire, tu peux me reprocher d'avoir mal placé mes conditions de compilation...

Plaçons les donc de telle manière à ce que les comportements vérifiant les pré et post conditions ne soient compilés qu'en mode débug
Code : Sélectionner tout
1
2
3
4
5
6
7
8
class A 
{ 
    public: 
        #if defined(DDEBUG) 
        virtual void pre() const; 
        virtual void post() const; 
        #endif 
};
car pre et post lancent des exceptions
Je pense que si tu fournis une méthode virtuelle de vérification à appeler pour savoir si tu as le droit d'appeler la fonction, tu déformes complètement l'idée de départ. Parce que dans ce cas, ce que tu fais, c'est imposer à l'appelant la structure suivante :

Code : Sélectionner tout
1
2
if(canCallF()) 
   f();

Mais, de toute manière, tu dois travailler avec des conditions (au minimum de compilation)... Utilisons donc un défine qui aille bien
Code : Sélectionner tout
1
2
3
4
5
6
7
#if defined(DDEBUG) 
#define PRE(X) X 
#define POST(X) X 
#else 
#define PRE(X) 
#define POST(X) 
#endif
(j'ai peut etre fait quelque erreurs )
qui seraient utilisés sous une forme proche de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
 
A a; 
/* B hérite de A, et redéfini potentiellement les différentes fonctions */ 
B b; 
PRE(a.pre()) 
a.go(); 
POST(a.post()) 
 
PRE(b.pre()) 
b.go(); 
POST(b.post(),b)
En utilisant des termes plus explicites que pre, go, post et rollback pour les différentes méthodes, nous atteignons le troisième but de la programmation par contrat qui est de donner des indications sur la sémantique du programme

Du fait du lancement de l'exception, en mode debug, tu te trouve face à la remontée de cette exception jusqu'au plus haut, et donc avec la possibilité de la récupérer "en temps utiles".

Au pire, tu te trouvera dans une situation où tu placera un try... catch te permettant de récupérer une exception qui ne sera jamais lancée (si on a quitté le mode débug)

Même en partant de tes hypothèses (que j'admets tout à fait comme justifiées), les miennes ne sont pas en désaccord avec les tiennes
Et finalement, si tu le faisait dans ta classe, plutôt que de l'imposer à l'appelant, ça donnerait :
Code : Sélectionner tout
function safe_f() pre Nothing
safe_f() n'a pas de préconditions, et du coup, tu n'as pas "restreint" les préconditions d'appel...

Pour aller encore plus loin, suppose que j'ai les fonctions suivantes :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A { 
int carre(int x) pre Nothing, post ret >= 0 { return x * x; } 
virtual int f(int x) pre x >= 0, post ret >= 0 { return (int) sqrt(x); } 
}; 
 
class B { 
virtual int f(int x) pre x >= 2, post ret >=0 { return (int) sqrt(x-2); } 
} 
 
int main() 
{ 
   A* a1 = new A(); 
   A* a2 = new B(); 
   a1->f(a1->carre(1)); // super ! 
   a2->f(a2->carre(1)); // aïe ! 
}

Pourquoi aie si tu a mis comme condition que x devait etre plus grand ou égal à 2 lorsque tu travaille sur un objet de type B, c'est que cette condition est justifiée et que le comportement risque de placer l'objet B dans un état incohérent

Au contraire, je serais presque embêté de laisser passer un objet dérivé devenu incohérent parce que, du point de vue de l'objet de base, la cohérence est assurée
Ce serait extrêmement vicieux. J'ai un A, j'appelle carre dessus, la postcondition sur le résultat me garantit qu'il est >= 0. La précondition sur A::f me dit que j'ai besoin que mon entier soit >= 0, ça tombe bien, c'est ce que carre me garantit !

Si j'autorise B à restreindre les préconditions de A, mon programme va planter lamentablement, car je n'ai plus rien de tangible auquel me fier. J'ai perdu tout le bénéfice de mon contrat.

Si, la cohérence du type réellement utilisé

Je comprend ton point de vue, mais je trouve le mien bien plus logique:

Dés le moment où tu peux substituer un objet à un autre, et que tu teste la cohérence de ton objet avant ou après une action, il me semble normal que ce soit le type réel qui détermine la cohérence de ce dernier (si du moins les règles de cohérences doivent être adaptées)

Je sais que, bien que d'un point de vue purement géométrique, un carré est un rectangle "particulier", il ne faut pas permettre à carré d'hériter de rectangle parce que cela ne correspond à rien de parler de "longueur" et de "largeur" sur un carré.

Par contre, un carré peut parfaitement hériter de losange (si on utilise comme donnée les quatre coins, et qu'il n'y a pas de données sur la taille des diagonales)

En effet, les deux seules différences tangible entre un carré et un losange sont (l'une dépendant directement de l'autre) l'angle formé par les différents coté et la taille des diagonales.

Les règles à appliquer pour t'assurer de la cohérence d'un carré sont plus strictes que celles à appliquer pour un losange (il faut soit s'assurer que les coté forment des angles droits, soit s'assurer que les diagonales sont égales... ce qui est peut être la solution la plus simple à mettre en oeuvre)

Lors de la vérification du contrat d'un losange, tu va veiller à ce que ce soit bel et bien un losange: quatre cotés égaux, que ce soit avant ou après le comportement qui doit le modifier.

Mais lors de la vérification du contrat d'un carré, le fait qu'il garde quatre cotés égaux n'est déjà pas trop mal (au moins, ca reste un losange, donc le type de base), mais y rajouter le fait qu'il garde des diagonales égales est encore mieux, parce que, justement, cela fait partie des conditions sans lesquelles... tu n'a pas un carré...

Or, même si tu fais passer ton carré pour un losange, et que tu le manipule en tant que tel, ce qui importe, c'est que, lorsque tu voudra le récupérer en tant que carré, il soit resté un carré, et qu'il ne soit pas devenu losange...

Il est donc logique, même si tu fais passer ton carré pour un losange, que les conditions testent le fait qu'il est (ou est resté)... un carré
Avatar de white_tentacle white_tentacle - Membre émérite https://www.developpez.com
le 05/02/2009 à 14:10
Ce serait extrêmement vicieux. J'ai un A, j'appelle carre dessus, la postcondition sur le résultat me garantit qu'il est >= 0. La précondition sur A::f me dit que j'ai besoin que mon entier soit >= 0, ça tombe bien, c'est ce que carre me garantit !

Si j'autorise B à restreindre les préconditions de A, mon programme va planter lamentablement, car je n'ai plus rien de tangible auquel me fier. J'ai

Si, la cohérence du type réellement utilisé

Mais je ne *connais pas* le type réellement utilisé. Le gros bénéfice de la PPC, c'est qu'elle me *garantit* que ce qui est valable pour un A, l'est pour tout B qui dérive de A.

Je sais que, bien que d'un point de vue purement géométrique, un carré est un rectangle "particulier", il ne faut pas permettre à carré d'hériter de rectangle parce que cela ne correspond à rien de parler de "longueur" et de "largeur" sur un carré.

Par contre, un carré peut parfaitement hériter de losange (si on utilise comme donnée les quatre coins, et qu'il n'y a pas de données sur la taille des diagonales)

Non. Il y a le même problème entre losange et carré, qu'entre rectangle et carré. Tout dépend de ton interface, et du contrat de tes fonctions.

Pourquoi aie si tu a mis comme condition que x devait etre plus grand ou égal à 2 lorsque tu travaille sur un objet de type B, c'est que cette condition est justifiée et que le comportement risque de placer l'objet B dans un état incohérent

Lorsque j'ai écrit main, je me suis basé sur ce que je connaissais. Le contrat de A::f. Je n'ai pas connaissance du contrat des classes dérivées. Je ne peux pas, leur ensemble est virtuellement infini. En supposant que je n'ai aucune règle sur le contrat dans les classes dérivées, je ne sais plus rien. J'ai un contrat, mais il ne me sert à rien, car je ne peux plus me baser dessus.

Ces règles sont là comme des garde-fous. Grâce à ces règles, je sais que je n'ai *pas le droit* d'écrire le B::f que j'ai écrit, et donc, que j'ai la *garantie* que ma fonction, qui fonctionne pour un A, fonctionnera pour tout B ou C qui dérive de A, *sans que je doive modifier ma fonction appelante*.

Je crois qu'on a un problème de communication . J'ai l'impression que ce que tu défends, c'est que "on peut s'en sortir sans" et "c'est trop restrictif". Mon point de vue à moi, c'est que les garanties qu'on gagne, valent largement les restrictions qu'on s'impose.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 15:53
Ce serait extrêmement vicieux. J'ai un A, j'appelle carre dessus, la postcondition sur le résultat me garantit qu'il est >= 0. La précondition sur A::f me dit que j'ai besoin que mon entier soit >= 0, ça tombe bien, c'est ce que carre me garantit !

Si j'autorise B à restreindre les préconditions de A, mon programme va planter lamentablement, car je n'ai plus rien de tangible auquel me fier. J'ai perdu tout le bénéfice de mon contrat.

Si, la cohérence du type réellement utilisé

Mais je ne connais pas le type réellement utilisé. Le gros bénéfice de la PPC, c'est qu'elle me garantit que ce qui est valable pour un A, l'est pour tout B qui dérive de A.

Mais à la limite, on s'en fout...

Tu sais que tu vérifie le contrat, point barre.

Ce que dit le contrat, à l'extrême limite, on s'en fout, on sais juste qu'il est là pour veiller à ce que l'objet reste cohérent.

Si, parce que tu dérive de ton objet de base, les conditions du contrat doivent évoluer, je ne vois absolument pas pourquoi l'empêcher... de toutes manière, tu manipulera ton objet dérivé comme un objet de base: les comportements que tu invoquera à son sujet sont des comportements que tu sais que l'objet de base implémente.

Je dirais même que tu te fout royalement de savoir si le comportement de l'objet dérivé est adapté ou non...

La seule chose qui compte, c'est que si le comportement est adapté et que le contrat nécessite de "addenda" pour permettre au comportement d'avoir lieu, c'est que ce soit le contrat de l'objet dérivé qui prenne le pas sur le contrat de l'objet de base, point-barre, parce qu'un objet dérivé peut être considéré comme étant dans un état cohérent alors que l'objet de base, dans les mêmes circonstances, serait considéré comme étant dans un état incohérent, et vice versa...

Ce qui importe, c'est que, lorsque l'on récupère l'objet réel (avec son type réel), l'objet soit bel et bien dans un état cohérent.

S'il appartient au concepteur de l'objet de base de déterminer le contrat qui assure que l'objet est dans un état cohérent, et qu'il admet le fait que cet objet puisse être dérivé, c'est au concepteur de l'objet dérivé de déterminer le contrat qui assure qu'il est dans un état cohérent.

Et l'utilisateur final n'a qu'à s'inquiéter de savoir s'il souhaite la vérification ou non du contrat, en sachant que, s'il la demande pour l'objet dérivé, il aura la certitude qu'il respecte les normes édictées pour cet objet particulier, ET que, s'il la demande pour l'objet de base (un objet réellement de base, et non un objet "qui se fait passer pour" un objet de base), ce seront les normes édictées pour cet objet de base qui seront prises en compte...

Je ne vois absolument pas ce qui est choquant là dedans...

Je sais que, bien que d'un point de vue purement géométrique, un carré est un rectangle "particulier", il ne faut pas permettre à carré d'hériter de rectangle parce que cela ne correspond à rien de parler de "longueur" et de "largeur" sur un carré.

Par contre, un carré peut parfaitement hériter de losange (si on utilise comme donnée les quatre coins, et qu'il n'y a pas de données sur la taille des diagonales)

Lorsque j'ai écrit main, je me suis basé sur ce que je connaissais. Le contrat de A::f. Je n'ai pas connaissance du contrat des classes dérivées. Je ne peux pas, leur ensemble est virtuellement infini. En supposant que je n'ai aucune règle sur le contrat dans les classes dérivées, je ne sais plus rien. J'ai un contrat, mais il ne me sert à rien, car je ne peux plus me baser dessus.

Mais en tant qu'utilisateur de l'objet, tu n'en a de toutes façons pas besoin...

Ce n'est pas à l'utilisateur de l'objet de décider du contrat, c'est à son concepteur, dés lors, si même tu viens à avoir 150 classes qui dérivent de ta classe de base, tu peux avoir entre 0 et 150 adaptation du contrat, c'est au concepteur de voir si elles sont nécessaires et opportunes

En tant qu'utilisateur, tu sais que, quel que soit le type réel que tu aura choisi pour ta variable, si tu utilise le mode débug, tu aura une erreur si les conditions particulières du contrat ne sont pas respectées

Honnêtement, je ne vois pas ce qui peut te chagriner là dedans.
Ces règles sont là comme des garde-fous. Grâce à ces règles, je sais que je n'ai pas le droit d'écrire le B::f que j'ai écrit, et donc, que j'ai la garantie que ma fonction, qui fonctionne pour un A, fonctionnera pour tout B ou C qui dérive de A, sans que je doive modifier ma fonction appelante.

Mais, pour autant que la vérification reste cohérente avec le type réel (que le contrat du type B soit cohérent avec le type B), tu garde exactement les même gardes fous... et ce n'est, je le redis encore, pas à l'utilisateur de s'en inquiéter, mais à celui qui crée les types en question...

Cela reste tout à fait transparent pour l'utilisateur, et c'est bien ce qu'il faut.

L'utilisateur sais qu'un contrat permet d'assurer la cohérence des données s'il passe en mode débug, et il sait que le concepteur du type aura pris ses précautions pour que, si le contrat nécessite une adaptation au type réel, ce soit cette adaptation qui sera prise en compte...
Je crois qu'on a un problème de communication . J'ai l'impression que ce que tu défends, c'est que "on peut s'en sortir sans" et "c'est trop restrictif". Mon point de vue à moi, c'est que les garanties qu'on gagne, valent largement les restrictions qu'on s'impose.

J'ai l'impression qu'il y a effectivement un problème de communication...

Moi, ce que je défend, c'est le fait que chaque type doit être responsable des propres contrats qu'il doit appliquer, ce qui n'empêche absolument pas d'avoir un type dérivé qui utilise (tout ou partie) du "contrat type" de son type de base, en sachant que, si une adaptation de ce "contrat type" est nécessaire, cette adaptation doit être prise en compte lors de la vérification

[EDIT]De manière beaucoup plus simplement exprimée: si tu adaptes un type, tu adaptes peut-être aussi les conditions dans lesquelles ce type est utilisé.

Tu n'est pas obligé d'adapter les conditions d'utilisation, mais si tu le fait, il est normal que ce soit ces conditions adaptées qui servent de garde fou
Avatar de white_tentacle white_tentacle - Membre émérite https://www.developpez.com
le 05/02/2009 à 16:06
Je crois que notre problème de communication, c'est qu'on n'a pas la même définition de ce qu'est un contrat.

Le contrat, tel que je l'entends (et tel qu'il est défini par B. Meyer), implique nécessairement deux parties, l'appelant et l'appelé. Les préconditions, c'est la partie qui concerne l'appelant. C'est son boulot de s'assurer que, lorsqu'il appelle l'appelé, il respecte les préconditions.
Les postconditions (et les invariants), c'est la partie qui concerne l'appelé. C'est son boulot de s'assurer que, lorsqu'il rend la main, les postconditions sont respectées. C'est un engagement très fort, que prend chacune des parties, et sur laquelle l'autre peut compter.

La cohérence de l'objet, c'est encore autre chose. Tu peux faire du contrat en fonctionnel pur, et tu peux très bien imaginer un contrat qui accepte que ton objet soit renvoyé dans un état incohérent.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 16:26
Citation Envoyé par white_tentacle  Voir le message
Je crois que notre problème de communication, c'est qu'on n'a pas la même définition de ce qu'est un contrat.

Le contrat, tel que je l'entends (et tel qu'il est défini par B. Meyer), implique nécessairement deux parties, l'appelant et l'appelé. Les préconditions, c'est la partie qui concerne l'appelant. C'est son boulot de s'assurer que, lorsqu'il appelle l'appelé, il respecte les préconditions.
Les postconditions (et les invariants), c'est la partie qui concerne l'appelé. C'est son boulot de s'assurer que, lorsqu'il rend la main, les postconditions sont respectées. C'est un engagement très fort, que prend chacune des parties, et sur laquelle l'autre peut compter.

La cohérence de l'objet, c'est encore autre chose. Tu peux faire du contrat en fonctionnel pur, et tu peux très bien imaginer un contrat qui accepte que ton objet soit renvoyé dans un état incohérent.

J'admets volontiers ne pas connaitre la définition donnée par meyer (j'irai la chercher toute suite après avoir répondu) du contrat, mais quand bien même...

Si l'appelant doit s'assurer qu'il peut appeler un comportement donné sur un objet, il est normal - à mon sens - qu'il s'en assure... sur l'objet réellement manipulé, et non sur l'objet sur lequel il *croit* (parce que c'est la forme sous laquelle on le lui passe) qu'il travaille

Cela n'empêche absolument pas que l'invocation du comportement qui consiste à donner le feu vert (ca y est, je sais que je peux appeler le comportement), se fasse sur base d'un comportement "de vérification" connu du type de base

De la même manière, si les posts conditions sont du ressort de l'appelé, il me semble normal qu'il respecte les conditions qui sont adapté au type réel, et non les conditions "pas forcément d'actualité" de son type de base

Enfin, le C++ n'est pas un langage purement fonctionnel, et comme je n'ai une approche que trop succinte des langages purement fonctionnels, je ne prendrai pas le risque de parler de leur point de vue...

Mais, je t'avouerai qu'en tant que programmeur OO, ma priorité première est la cohérence des objets que je manipule.

Je suis peut-être en porte à faux avec les gurus sur ce point de vue, mais, de mon point de vue, un objet incohérent est un objet qui m'enverra dans le décors.

Et donc, si je passe un contrat avec un objet, c'est bel et bien pour m'assurer que, quoi qu'il arrive, cet objet restera cohérent (soit parce que j'aurai effectivement eu l'occasion d'invoquer un comportement dessus, soit parce que l'objet m'aura signalé toute incohérence provoquée par ce comportement)

Mais, de mon point de vue, le contrat doit donc s'appliquer sur l'objet réel, avec les conditions qui sont propres à cet objet réel, même s'il se fait passer pour autre chose
Avatar de white_tentacle white_tentacle - Membre émérite https://www.developpez.com
le 05/02/2009 à 16:44
Si l'appelant doit s'assurer qu'il peut appeler un comportement donné sur un objet, il est normal - à mon sens - qu'il s'en assure... sur l'objet réellement manipulé, et non sur l'objet sur lequel il *croit* (parce que c'est la forme sous laquelle on le lui passe) qu'il travaille

Ce que tu décris, c'est une forme de programmation, où l'appelant demanderait à l'appelé s'il a le droit de l'appeler. Le problème de cette approche, c'est qu'elle impose de faire les vérifications au runtime. Ça me fait penser à une forme de programmation défensive, où tu confierais ta défense à l'appelant...

L'avantage de la programmation par contrat, c'est que, du fait des contraintes supplémentaires que tu t'es imposées, tu sais quelles vérifications tu as besoin de faire, et quelles vérifications tu n'as pas besoin de faire.

Notamment, tu sais que tu peux toujours écrire :
Code : Sélectionner tout
int a = sqrt(carre(x));
Mais pas :
Code : Sélectionner tout
int b = sqrt(cube(x));
Autrement dit, grâce à la description de mes pré et post conditions, je sais que le premier va toujours marcher, sans avoir à rien vérifier, et je sais que le deuxième peut merder; et donc, que mon code est faux, et que je dois vérifier quelque chose. Ça me semble un bénéfice non négligeable !

Une fois que tu as admis ce bénéfice sur l'approche fonctionnelle, essaie de transposer le mécanisme de contrat au monde de la POO, de l'héritage, en conservant ce bénéfice.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 17:16
Hé bien, pourquoi ne pas accepter que le code suivant fournit effectivement les garanties nécessaire
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A 
{ 
    public: 
        virtual void doSomething() 
        { 
            /* me jette si je n'ai pas le droit de faire ce qu'on attend 
             * du comportement 
             */ 
            assert(la vérif qui va bien ) 
            /* je fais ce qu'il faut */ 
            /* et je me fais jeter si la connerie a été faite */ 
            assert(la vérif finale) 
        } 
}; 
class B : public A 
{ 
    public: 
        virtual void doSomething() 
        { 
            /* me jette si je n'ai pas le droit de faire ce qu'on attend 
             * du comportement 
             */ 
            assert(la vérif qui va bien, adaptée à mon B ) 
            /* je fais ce qu'il faut */ 
            /* et je me fais jeter si la connerie a été faite */ 
            assert(la vérif finale, adaptée à mon B) 
        } 
};
Si je l'appelle sous la forme de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
int main() 
{ 
    A *a = new A(); 
    A *b = new B(); 
    a->doSomething(); 
    b->doSomething(); 
   delete a; 
   delete b; 
}
chacun appliquant les règles qui leur sont fixées, et, si, une autre personne a besoin de créer une fonction qui lui va bien, elle peut tout à fait créer sa fonction sur base d'une référence ou d'un pointeur sur A et appeler le comportement adéquat sous la forme de
Code : Sélectionner tout
1
2
3
4
void foo(A* a) 
{ 
    a->doSomething(); 
}
ou de
Code : Sélectionner tout
1
2
3
4
void foo(A /*const*/ & a) 
{ 
    a.doSomething(); 
}
mais le contrat de chaque objet reste tout à fait valide par rapport à l'objet réellement utilisé...

Honnetement, et j'admets être peut être en porte à faux avec meyer, je ne vois pas pourquoi les conditions qui s'appliquent à un A ne pourraient pas - si l'adaptation à l'objet le nécessite - s'adapter pour un B...

Au moins, de mon coté, je garde la certitude que mes objets restent cohérents, et c'est la première chose que je demanderai toujours à mes comportements, parce qu'un objet incohérent m'enverra, tôt ou tard, dans le mur
Avatar de Médinoc Médinoc - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 17:26
Le problème, c'est que si un contrat devient dynamique, ce n'est plus un contrat.
Ça équivaut à changer les règles du jeu en cours de partie, et bien que cela soit permis pour le Calvinball, ça n'est pas permis pour la programmation par contrat.
Avatar de white_tentacle white_tentacle - Membre émérite https://www.developpez.com
le 05/02/2009 à 17:27
Hé bien, pourquoi ne pas accepter que le code suivant fournit effectivement les garanties nécessaire

Il les fournit, mais, au runtime, avec un coût d'exécution associé (qui ne devient vraiment pas négligeable dans le cas, par exemple, d'un appel récursif).

Le contrat, lui, te les fournit dès la conception !

Ce que tu fais toi, c'est de la programmation défensive. Ça donne de bonnes garanties, mais avec un coût associé. La PPC te permet de t'affranchir totalement de ce coût, quand ton programme est correct.

mais le contrat de chaque objet reste tout à fait valide par rapport à l'objet réellement utilisé...

Comme dit, le contrat, il concerne deux "objets", l'appelant et l'appelé.

Ah, une dernière chose. Dans ton exemple, doSomething n'a pas de précondition, puisqu'elle a un comportement défini quelque soient ses entrées (soit, ne rien faire, soit, exécuter le traitement).
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 17:58
Citation Envoyé par Médinoc  Voir le message
Le problème, c'est que si un contrat devient dynamique, ce n'est plus un contrat.
Ça équivaut à changer les règles du jeu en cours de partie, et bien que cela soit permis pour le Calvinball, ça n'est pas permis pour la programmation par contrat.

Ah, voilà enfin quelque une réponse qui vaudrait la peine de creuser

Et malgré tout, ce n'est pas en cours de partie, que je propose de les adapter, mais bel et bien en cours de conception, lorsque je me rend compte qu'une règle (sans doute trop générale) empêche un comportement particulier de fonctionner comme il le devrait...

Après tout, si j'en viens à décider de dériver un objet au départ d'un autre, c'est quand même - de prime abord - parce que mon étude des besoins ou ma conception me fait prendre conscience du fait que le type d'origine ne correspond pas forcément à un besoin particulier...

Mais, qui dit besoin particulier dit - potentiellement -des restrictions particulières...

Maintenant, indique moi où cette logique "théorique" se place dans un contexte de CalvinBall, et j'arrêterai de discuter
Avatar de Médinoc Médinoc - Expert éminent sénior https://www.developpez.com
le 05/02/2009 à 18:04
Pour moi, ce que tu dis reflète plus une erreur dans la conception du contrat, et non un cas "normal".

Mais c'est une autre remarque de white_tentacle qui me fait réfléchir:
Ah, une dernière chose. Dans ton exemple, doSomething n'a pas de précondition, puisqu'elle a un comportement défini quelque soient ses entrées (soit, ne rien faire, soit, exécuter le traitement).

Cela veut dire que le contrat ne protège que des comportements indéfinis.
Si une fonction "marche" dans certains cas et retourne une valeur d'erreur dans d'autres, ça reste des comportements définis, qui peuvent donc faire partie du contrat.

Simplement, ça donne un contrat où les postconditions ne sont pas constantes, mais conditionnelles.
Exemple: "La fonction de calcul retourne ERROR_INVALID_PARAMETER si la valeur d'entrée n'est pas supporté" fait partie du contrat. Dans ce cas, le contrat ne dit pas "la valeur d'entrée doit être supportée", il dit seulement que le comportement n'est pas indéfini si une valeur non-supportée est passée en paramètre.
Offres d'emploi IT
Ingénieur analyste programmeur (H/F)
Safran - Auvergne - Montluçon (03100)
Responsable transverse - engagement métiers H/F
Safran - Ile de France - Corbeil-Essonnes (91100)
Spécialiste systèmes informatiques qualité et référent procédure H/F
Safran - Ile de France - Colombes (92700)

Voir plus d'offres Voir la carte des offres IT
Contacter le responsable de la rubrique C++