Pouvoir: avoir l'autorisation de... à ne pas confondre avec être capable de...
Le fait que tu soit capable d'invoquer un comportement suite à une décision inappropriée de conception ne signifie absolument pas qu'il soit opportun de te donner la permission de l'invoquer.
La preuve avec un code tout simple:
donne, à l'exécution
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 class Base { public: virtual void foo() {std::cout<<"la fonction peut etre appelee de l'exterieur" <<std::endl;} }; class Derivee : public Base { protected : virtual void foo() {std::cout<<"la fonction ne peut servir qu'a usage interne" <<std::endl;} }; void bar(Base & b) { b.foo(); } int main() { Base a; Derivee b; bar(a); bar(b); }Comment pourrais tu estimer que le programme fonctionne correctement, alors que la même fonction appelée exactement dans les mêmes circonstances te dit que... la fonction est limitée à usage interne
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 la fonction peut etre appelee de l'exterieur la fonction ne peut servir qu'a usage interne
Mais les causes sont différentes, le raisonnement est, lui aussi, tout à fait différent, et, surtout, les solutions potentielles sont tout à fait différentes...Pourtant, ce choix aboutit à la même conséquence que le changement de visibilité : la fonction f est appelable au travers de l'interface de base, mais plus sur le type réel de l'objet (pour une raison différente, mais la conséquence est la même).
D'un coté, tu décide d'accepter un héritage qui n'a pas lieu d'être, parce que tu ne peux décemment pas estimer, en vertu de LSP qu'il y a une des partie de l'interface publique qui est valide pour la classe de base et invalide pour la classe dérivée.
Comme j'ai déjà listé les différentes possibilités que tu as pour résoudre ce problème, je te reporte simplement à mes messages précédents.
D'un autre, tu as simplement manqué de recul par rapport à l'ensemble de ta conception et il "suffit" de renommer la fonction dans une seule classe de base (et éventuellement dans les classes dérivées qui la redéfinissent)
Parce que toute fonction qui peut être appelée depuis un objet du type de la classe de base doit pouvoir être appelée depuis une instance du type de la classe dérivée.Pourquoi ce code devrait-il fonctionner en vertu du LSP ? Où est la substitution ?
Il n'y a pas substitution, mais le fait qu'une fonction puisse être appelée est bel et bien une propriété de la classe de base.
Si ce n'est que B hérite de A, et que donc, si l'appel d'une fonction est valide pour A, il doit aussi... être valide pour B, quitte à ce que le comportement observé soit adapté aux besoins de B.Nulle part dans ce code on a substitué un B à un A. On a fait quelque chose sur un A, puis on essaie de faire quelque chose d'invalide avec un B.On met énormément l'accent sur la substituabilité avec LSP, mais ce n'est pas le seul point sur lequel il est applicable.Mais nulle part il y a substitution, et certainement pas au niveau des objets.
Nous ne pouvons, en effet, envisager de substituer des objets que si LSP est validé, mais, à coté de cela, la validité de la relation EST-UN elle-même dépend du respect de LSP.
Et donc, au delà de la subsituabilité entre un objet du type dérivé et un objet du type de base, c'est carrément tout ce que permet le type de base qui doit être permis par le type dérivé.
Comme je te le répète depuis plusieurs interventions, tu ne peux pas dissocier une instance ou un objet du type qu'il (elle) représente.Enlève moi tous ces types que je ne saurais voir, je dis depuis le début que le LSP s'applique aux instances. Je reformule, donc :
Quand je crée une golf 4 TDI, je crée une instance, mais pas une instance quelconque: une instance de voiture (et non une instance d'éléphant, par exemple).
Le type regroupe (au niveau des fonctions membres) les comportements qui sont acceptables et les conditions dans lesquelles un objet (ou une instance) acceptera que l'on y fasse appel.
A l'inverse, une instance ou un objet représente "physiquement" (ou du moins en mémoire) "quelque chose" (pour ne pas réutiliser le terme objet) qui accepte que l'on invoque un certains nombre de comportements, déterminés par le type, aux conditions déterminées par le type.
j'ai répondu un peu plus haut, avec mon exemple de codeEt là, fondamentalement, je ne vois plus où est le problème à restreindre la visibilité .
Tu as, effectivement loupé quelque chose...Eiffel offre des mécanismes de ce type, au moyen de contrats (même si eiffel offre d'autres mécanismes qui ne respectent pas le LSP ). Mais je ne parle pas d'écrire du code qui ne vérifie pas le LSP (ce qui est très facile), je parle spécifiquement d'écrire du code qui aurait un comportement incohérent, en utilisant la fonctionnalité dont on est en désaccord pour savoir si elle respecte le LSP, et que l'incohérence soit due à une substitution d'objet. Ce qui me semble impossible, mais j'ai pu louper quelque chose.
Cf mon code de début d'intervention
De plus, je le répète encore une fois, LSP et un principe de conception, tout comme peut l'être celui de préciser l'accessibilité d'un membre ou d'une fonction membre.
Si tu veux t'assurer que l'ensemble de ton programme fonctionne de manière cohérente, tu te dois de respecter les différents principes, règles et lois de la conception orientée objet.
Si tu prend le risque de ne pas les respecter, tu cours celui d'avoir des comportements dont rien dans le processus de compilation (quel que soit le langage) ne te permet de te rendre compte que "tu joues avec le feu", et qui produiront des résultats au minimum aberrants, au pire, carrément catastrophiques.
Mais les règles que le langage définit par lui-même ne traitent en gros que de la syntaxe à utiliser et de terme que le compilateur est capable de comprendre "par lui-même", plus quelques règles pour savoir quand il faut faire une copie ou non de l'objet manipulé, ou des règles de destruction impliciteEt celles que le langage accepte. Ce qui est loin d'être négligeable dans certains langages.
Reprenons, si tu veux bien...C'est tout le fond de notre désaccord. Pour moi, ce n'est pas nécessairement incohérent, et c'est rau contraire endu nécessaire par... le LSP .
Tu es d'accord que, si tu écrit une classe proche de
c'est parce que tu as déterminé, au moment de la conception, que foo te serait utile pour les objets de type Derivee, quelle pourrait être redéfinie pour les types qui héritent de Derivee, mais qu'il n'est pas opportun de laisser l'utilisateur de ta classe (celui qui créera une instance de Derivee) appeler cette fonction n'importe quand.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 class Derivee { protected: virtual void foo(){std::cout<<"fonction a usage interne" <<std::endl;} };
Jusque là, je présumes que tu sera d'accord avec le raisonnement, non
A coté de cela, si tu as une classe proche de
C'est parce que tu as déterminé, toujours au moment de la conception, que foo serait utile pour les objets du type Base, qu'elle pourrait être redéfinie pour les types qui héritent de Base, et que l'on peut laisser l'utilisateur de ta classe (celui qui créera une instance de Base) invoquer cette fonction "à n'importe quel moment, depuis n'importe où", s'il le juge opportun.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 class Base { public: virtual void foo(){std::cout<<"fonction accessible de partout" <<std::endl;} };
Jusqu'ici, nous sommes, à mon avis, toujours tout à fait d'accord
Là où les choses se gâtent, c'est si l'on décide de faire en sorte que Derivee hérite de Base.
En effet, l'utilisateur (ou toi même) pourrait(s) décider d'écrire une fonction proche de
pour les objets du type Base, il n'y a aucun problème : foo est déclarée accessible depuis l'extérieur, et on peut donc parfaitement envisager d'écrire un tel code.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 void bar(Base & b) { b.foo(); }
Par contre, pour les objets du type Derivee, pour lesquels tu as explicitement dit à l'utilisateur de ne pas utiliser foo, auquel tu as clairement stipulé "à usage interne uniquement", il en va tout autrement.
En effet, s'il décide d'écrire la fonction principale sous la forme de
Le fait qu'il ne puisse pas invoquer foo depuis l'instance d de Derivee est normal et c'est le comportement que l'on attend d'un objet de type Derivee: foo est à usage interne uniquement.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 int main() { Derivee d; // d.foo() // refusé à la compilation : foo() est protected dans ce contexte bar(d); /* utilisation ultérieure de d; */ return 0; }
L'incohérence se situe au niveau de l'appel de bar en lui passant d en paramètre.
Je te rappelle que d (qui prend le nom de b dans bar) est et ..."redevient" (même si ce terme est particulièrement mal choisi)... un objet de type Derivee une fois que l'on a quitté la portée de bar.
Comment pourrait on justifier que le simple fait d'appeler une fonction qui n'est même pas déclarée comme une amie de Derivee permette d'appeler un comportement réputé comme étant à usage interne pour Derivee
D'un coté, tu dis à l'utilisateur qu'il ne doit pas essayer d'invoquer foo sur un objet de type Derivee, parce qu'elle intervient dans la "popote interne" de l'objet en question, mais, de l'autre, tu le félicite d'avoir été "assez bête pour perdre le type réel de l'objet" en créant une fonction qui, elle, pourra accéder à la fonction sans aucune restriction uniquement parce que tu as fais un choix de conception que tu n'aurais jamais accepté si tu avais appliqué les principe de conception au bon moment.
De plus, je tiens quand même à préciser que le prototype de bar "ment" bien souvent à son utilisateur, dans le sens où elle accepte "n'importe quel type susceptible de se faire passer pour Base", et que donc, à partir du moment où l'héritage est décidé, elle acceptera sans distinction n'importe quel (instance du ) type dérivé.
Mais, encore une fois, comme bar(d) aura pour conséquence l'appel d'un comportement que nous n'aurions jamais pu invoquer si l'on avait considéré d pour ce qu'il est (à savoir... un objet de type Derivee), nous n'aurions jamais du donner la possibilité à bar d'accepter qu'on lui transmette un objet de type Derivee comme paramètre.
Partager