Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Vocabulaire.

42 réponses
Avatar
Alexis Guillaume
Bonjour =E0 tous.

Je suis en train de concevoir une hi=E9rarchie de classe en C++ :

class base {
public:
virtual void foo();
};

Dans les commentaires, j'impose que si foo() est d=E9riv=E9e dans une
classe fille, alors cette m=E9thode d=E9riv=E9e doit aussi appeler
explicitement base::foo() apr=E8s avoir fait ce qu'elle voulait. Plus
g=E9n=E9ralement, si on a C qui h=E9rite de B et B qui h=E9rite base, et qu=
e B
et C d=E9rivent foo(), alors C::foo() doit appeler B::foo() et B::foo()
doit appeler base::foo().

Je dois r=E9diger un document de conception et j'ai bien du mal =E0
trouver comment appeler ce genre de fonctions virtuelles. Je pensais =E0
quelque chose comme "m=E9thode virtuelle cha=EEn=E9e"... Mais il y a
s=FBrement mieux et il y a peut-=EAtre m=EAme un mot qui existe d=E9j=E0 po=
ur
cela auquel cas c'est bien s=FBr lui que j'emploirai.

Merci pour vos avis =E9clair=E9s.
Alexis Guillaume.

10 réponses

1 2 3 4 5
Avatar
James Kanze
On Mar 4, 4:27 pm, (Pascal J. Bourguignon)
wrote:
Alexis Guillaume writes:
>> Il y a le pattern "chaine de responsabilité" que tu peux
>> employer. Ca a l'air d'en être une forme (je ne peux pas
>> dire avec les éléments que tu donnes).



> En effet je me renseigne sur ce pattern et ça a l'air d'être
> ce que je veux faire. Merci beaucoup ! :-)



Oui, enfin, c'est le fonctionnement normal en programmation
objet quand on surcharge une méthode, d'appeler la méthode de
la superclass.



Je dirais plutôt que c'est une symptome d'une mauvaise
conception. En général, on ne supplante que des fonctions
virtuelles pures.

C'est tellement normal, que dans les langages de programmation
objet normaux, il y a en général un mot clé ou une syntaxe
particulière, quand ce n'est pas automatique.



Par exemple, en Objective-C:



-doSomething
{
[self doSomethingBefore];
[super doSomething]; // ***
[self doSomethingAfter];
}



en Ruby:

def doSomething
doSomethingBefore
super.doSomething
doSomethingAfter
end



ou en Common Lisp:

(defmethod do-something ((self my-class))
(do-something-before)
(call-next-method) ;; ***
(do-something-after))



ou encore:



(defmethod do-something :before ((self my-class))
(do-something-before))



;; La méthode do-something est appelée automatiquement.



(defmethod do-something :after ((self my-class))
(do-something-after))



Tu es en train de documenter quelque chose qui est ce qui
devrait se faire par défaut, car c'est la façon naturelle
d'implémenter le principe de substitution de Lyskov.



Au contraire. C'est quelque chose qu'on peut faire dans tous les
langages OO, parce que dans la passée, on n'avait pas encore
compris qu'il ne le fallait pas. Mais en général, aujourd'hui,
c'est plutôt la signe d'une mauvaise conception.

Alors la question, c'est si on doit appeler cette méthode dans
la superclasse simplement pour assurer que la post-condition
est bien remplie (étant donnée la pré-condition), ou s'il y a
une autre raison (par exemple, un effet de bord particulier
que l'on veut obtenir.



En C++, au moins, s'il y a des pré- ou des post-conditions (ce
qui est le cas le plus fréquent), c'est dans une fonction
non-virtuelle dans la classe de base qu'on les implémente,
e.g. :

class Interface
{
public:
/* return type */ uneFonction( /* params */ )
{
// vérifier les préconditions...
doUneFonction( params ) ;
// vérifier les postconditions...
return ...
}

private:
virtual /* return type */ doUneFonction( /* params */ ) = 0 ;
} ;

C'est l'idiome consacré -- il n'y a que dans les cas où il n'y a
pas de contrat (typiquement, une inversion de l'appel) où on
aurait une fonction virtuelle publique.

Dans le premier cas, on n'a rien besoin de documenter car ça
doit être toujours le cas, et il n'y a pas de raison pour
qu'une sous-classe ne puisse pas s'assurer de la
post-condition par d'autres voies que la superclasse (même si
en général, un bon programmeur étant faignant essayera plutôt
d'appeler la super classe que de réinventer la roue).



Dans le second cas, il vaudrait mieux avoir une méthode Y
distincte dans la super classe, et documenter simplement que
toute surcharge de la méthode originale X doit s'assurer que
cette méthode Y est appelée, que ce soit directement ou via la
méthode X de la superclasse.



Exactement.

En somme, je crois qu'on est d'accord. Sauf que plutôt que de
compter sur la classe dérivée pour assurer le contrat de la
classe de base, il vaut mieux utiliser l'idiome standard, avec
une fonction virtuelle privée.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Avatar
James Kanze
On Mar 4, 7:19 pm, Fabien LE LEZ wrote:
On Wed, 04 Mar 2009 18:12:36 +0100, Wykaaa :



>> Ben oui, mais justement, C++ n'est pas un langage objet, et
>> n'a d'ailleurs pas la notion de "méthode".



>Ah bon ? et une fonction membre c'est quoi ?



Une fonction membre.



Le mot "méthode" est généralement employé dans les langages où
les fonctions membres sont toutes virtuelles.



Le mot « méthode » n'a pas une définition universelle qui vaut
pour tous les langages. Au départ, il servait à désigner la
façon qu'un objet réagissait à un message (dans un context où on
pouvait envoyer n'importe quel message à n'importe quel objet,
quitte à ce que sa réaction soit de dire qu'il ne le comprend
pas). Depuis, chaque langage a créer ses propres définitions.

En C++, le mot n'est pas défini. Si on parle d'une méthode C++,
chacun qui écoute risque de comprendre quelque chose de
différent. Mieux vaut donc ne pas se servir du mot.

En C++, une fonction membre n'est pas virtuelle, sauf si on le
demande explicitement. Du coup, utiliser le mot "méthode"
n'apporte qu'une inutile confusion : signifie-t-il "fonction
membre" ou "fonction membre virtuelle" ?



Ou fonction member non-static, ou fonction tout court ?

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Avatar
James Kanze
On Mar 4, 6:59 pm, Jean-Marc Bourguet wrote:
(Pascal J. Bourguignon) writes:



>> Une fonction virtuelle pure a peut avoir un corps bien que
>> ce soit assez rare (sauf peut être quand c'est le
>> destructeur).



> Ça doit être "nouveau". Quelle est la syntaxe pour l'écrire?



C'est déjà dans la deuxième édition de TC++PL, en 1991. J'ai
pas cherché dans des documents antérieurs.



La première édition n'avait pas de fonctions virtuelles pures.
Ça existe depuis qu'il y a des fonctions virtuelles pures.

Dans la pratique, évidemment, aujourd'hui, on ne supplante que
des fonctions virtuelles pures, qui n'ont en général pas
d'implémentations, et donc, la question de comment ou quand
appeler la fonction supplantée ne se pose pas.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Avatar
James Kanze
On Mar 4, 5:20 pm, (Pascal J. Bourguignon)
wrote:
Jean-Marc Bourguet writes:
> (Pascal J. Bourguignon) writes:



>> Alexis Guillaume writes:



>> >> Il y a le pattern "chaine de responsabilité" que tu peux
>> >> employer. Ca a l'air d'en être une forme (je ne peux pas
>> >> dire avec les éléments que tu donnes).



>> > En effet je me renseigne sur ce pattern et ça a l'air
>> > d'être ce que je veux faire. Merci beaucoup ! :-)



>> Oui, enfin, c'est le fonctionnement normal en programmation
>> objet quand on surcharge une méthode, d'appeler la méthode
>> de la superclass.



> Bizarre, dans la plupart des cas, ce n'est pas ce que je
> fais (a commencer par tous les cas ou la fonction membre est
> pure dans la classe de base).



Oui, les méthodes virtuelles pures sont pratiques pour
permettre au compilateur de vérifier qu'on a bien fourni une
implémentation dans les sous-classes. Cependant ce n'est pas
adapté à un style de programmation plus dynamique, et ça donne
plus de travail à l'utilisateur de la classe abstraite.



En général je préfère définir une implémentation par défaut
pour toutes mes méthodes, même dans les classes "abstraites".



Ça me semble une violation du principe de la séparation des
concernes. La classe de base définit l'interface (le contrat),
non l'implémentation. Et dans la pratique, je ne vois pas
comment elle pourrait offir une implémentation par défaut. Une
implémentation consiste en un ensemble de fonctions membres qui
fonctionne d'une façon cohérente ensemble. Alors, on les
remplace toutes, ou on n'en remplace aucune.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Avatar
Michael DOUBEZ
Pascal J. Bourguignon wrote:
Michael DOUBEZ writes:

Pascal J. Bourguignon wrote:
Alexis Guillaume writes:

Il y a le pattern "chaine de responsabilité" que tu peux employer. Ca a
l'air d'en être une forme (je ne peux pas dire avec les éléments que tu
donnes).


En effet je me renseigne sur ce pattern et ça a l'air d'être ce que je
veux faire. Merci beaucoup ! :-)


Oui, enfin, c'est le fonctionnement normal en programmation objet
quand on surcharge une méthode, d'appeler la méthode de la superclass.
C'est tellement normal, que dans les langages de programmation objet
normaux, il y a en général un mot clé ou une syntaxe particulière,
quand ce n'est pas automatique.


Ce qui ne marche que dans le cas où l'héritage est simple. Comme C++
supporte l'héritage multiple, c'est assez difficile de fournir un tel
mot clé.



Tu n'as pas bien fait attention aux exemples Common Lisp.
(call-next-method) appelle la méthode suivante, en tenant compte de
l'héritage multiple. Ce n'est pas forcément celle d'une super-classe.
Mais je n'entrerai pas plus dans la richesse du système objet de
Common Lisp.



Je ne parle pas de CLOS mais de tous les autres langages objets dont tu
parles et qui ont un modèle avec héritage simple/interface multiple
(extend/implement).

Lisp est un peu à part puisqu'il se rapproche plus d'un méta langage, on
peut bien y faire ce qu'on veut.

[snip]
Une autre solution serait d'utiliser une méthode crochet, c'est à dire
qu'on défini dans la super classe une méthode WX qui s'assure que la
méthode Y est appelée en même temps que X, la superclasse ne
définissant pas X (ou fournissant une définition par défaut vide), en
spécifiant simplement qu'une sous-classe doit surcharger X (comme elle
le veut, la méthode Y étant appelée automatiquemetn par WX.


Le problème ici est qu'il peut y avoir un nombre indéfini de surcharge
de la classe et chaque surcharge intermédiaire doit être appelée.



Bien entendu, toutes ces méthodes sont virtuelles, et quand la super
classe envoit le message this->X(), c'est la méthode de la sous-classe
dont this est exactement l'instance qui est appelée. Tout ce qu'on lui
demande c'est d'assurer la post-condition. Le mieux pour elle c'est
d'appeler la méthode X de la superclasse mais si elle ne le fait pas
ce n'est pas grave, et de toutes façons WX s'assure que Y soit
appelée.



Ce qui ne marche que pour un niveau d'hériatge à moins que je n'ai mal
compris. Ici, cette post condition doit être réitérée dans chaque sous
classe pour elle même et pour sa future classe dérivée. A moins de
définir n fonctions membres (1 de plus dans chaque sous classe) je vois
pas comment faire.

Ce qu'il y ici c'est qu'il est difficile d'exprimer dans le langage la
séquence (next-class-to-call) pour réaliser le chainage; d'ailleur on
préfèrera la composition pour ce genre de cas.

Pour le cas de l'OP, il est peut ête possible de faire quelque chose
avec des templates (le problème est le method-hiding)), j'y réfléchirai.

--
Michael
Avatar
pjb
James Kanze writes:

On Mar 4, 4:27 pm, (Pascal J. Bourguignon)
wrote:
Alexis Guillaume writes:
>> Il y a le pattern "chaine de responsabilité" que tu peux
>> employer. Ca a l'air d'en être une forme (je ne peux pas
>> dire avec les éléments que tu donnes).



> En effet je me renseigne sur ce pattern et ça a l'air d'être
> ce que je veux faire. Merci beaucoup ! :-)



Oui, enfin, c'est le fonctionnement normal en programmation
objet quand on surcharge une méthode, d'appeler la méthode de
la superclass.



Je dirais plutôt que c'est une symptome d'une mauvaise
conception. En général, on ne supplante que des fonctions
virtuelles pures.



Pas en POO. L'héritage est un élément essentiel de la POO.

OO = {héritage, encapsulation, polymorphisme, abstraction}.


Si on se contente de définir des interfaces d'un côté, et de les
implémenter d'un autre, on exclu l'héritage, et on ne programme pas en
OO.


C'est tellement normal, que dans les langages de programmation
objet normaux, il y a en général un mot clé ou une syntaxe
particulière, quand ce n'est pas automatique.


[...]

En somme, je crois qu'on est d'accord. Sauf que plutôt que de
compter sur la classe dérivée pour assurer le contrat de la
classe de base, il vaut mieux utiliser l'idiome standard, avec
une fonction virtuelle privée.



On est d'accord, cependant le principe de substitution de Lyskov va
plus loin: une sous-classe peut accepter une version plus large de la
pré-condition, et assurer une version plus stricte de la
post-condition.

Ainsi, l'idiome que tu appelles "consacré" n'est pas du tout adapté.


class Automobile {
public:
virtual void attachWheel(Wheel* w);
// PRE: w->isCircular() and (0<=this->wheelCount()<this->maxWheelCount())
// POST: this->hasWheel(w) and (this->wheelCount() == 1 + old this->wheelCount())
// and (for all v in this->wheels(), (v == w) or (old this->hasWheel(v)))

// ...
};


class RusticAutomobile : public Automobile {
virtual void attachWheel(Wheel* w);
// PRE: (0<this->wheelCount()<=this->maxWheelCount())
// POST: this->hasWheel(w) and (0<this->wheelCount()<=this->maxWheelCount())
// and (for all v in this->wheels(), (v == w) or (old this->hasWheel(v)))

// ...
};


void RusticAutomobile::attachWheel(Wheel* w)
{
assert(0<this->wheelCount()<=this->maxWheelCount()); // RusticAutomobile precond
if(not w->isCircular()){
Hammer* h=new Hammer(Hammer::BIG);
while(not w->isCircular()){
h->bangOn(w);
}
}
assert(w->isCircular() and (0<this->wheelCount()<=this->maxWheelCount()));
if(this->wheelCount()==this->maxWheelCount()){
this->removeWheel(this->selectOldestWhell());
}
assert(w->isCircular()
and (0<this->wheelCount()<this->maxWheelCount())); // Automobile precond
Automobile::attachWheel(w);
assert(this->hasWheel(w) and (this->wheelCount() == 1 + old this->wheelCount())
and (for all v in this->wheels(), (v == w) or (old this->hasWheel(v))));
// Automobile postcond
// ==>
assert(this->hasWheel(w) and (0<this->wheelCount()<=this->maxWheelCount())
and (for all v in this->wheels(), (v == w) or (old this->hasWheel(v))));
// RusticAutomobile postcond
}


Mais ce qui est intéressant maintenant, c'est que comme
preCondition(Automobile::attachWheel) ==> preCondition(RusticAutomobile::attachWheel)
et
postCondition(RusticAutomobile::attachWheel) ==> postCondition(Automobile::attachWheel)

on peut substituer une automobile rustique partout où on a une
automobile, et ça continuera à fonctionner.

Automobile* a = new RusticAutomobile();
road->materialize(a); // road n'a pas besoin de savoir qu'il existe des
// automobiles rustiques, pour lui, toutes les
// automobiles sont des Automobiles.

Et on pourra, dans le code qui sait que l'automobile est rustique, y
attacher des roues carrées.

RusticAutomobile* r = new RusticAutomobile();
r->attachWheel(new Wheel(Wheel::SQUARE));
road->materialize(r);

--
__Pascal Bourguignon__
Avatar
pjb
James Kanze writes:

On Mar 4, 5:20 pm, (Pascal J. Bourguignon)
wrote:
Jean-Marc Bourguet writes:
> (Pascal J. Bourguignon) writes:



>> Alexis Guillaume writes:



>> >> Il y a le pattern "chaine de responsabilité" que tu peux
>> >> employer. Ca a l'air d'en être une forme (je ne peux pas
>> >> dire avec les éléments que tu donnes).



>> > En effet je me renseigne sur ce pattern et ça a l'air
>> > d'être ce que je veux faire. Merci beaucoup ! :-)



>> Oui, enfin, c'est le fonctionnement normal en programmation
>> objet quand on surcharge une méthode, d'appeler la méthode
>> de la superclass.



> Bizarre, dans la plupart des cas, ce n'est pas ce que je
> fais (a commencer par tous les cas ou la fonction membre est
> pure dans la classe de base).



Oui, les méthodes virtuelles pures sont pratiques pour
permettre au compilateur de vérifier qu'on a bien fourni une
implémentation dans les sous-classes. Cependant ce n'est pas
adapté à un style de programmation plus dynamique, et ça donne
plus de travail à l'utilisateur de la classe abstraite.



En général je préfère définir une implémentation par défaut
pour toutes mes méthodes, même dans les classes "abstraites".



Ça me semble une violation du principe de la séparation des
concernes. La classe de base définit l'interface (le contrat),
non l'implémentation. Et dans la pratique, je ne vois pas
comment elle pourrait offir une implémentation par défaut. Une
implémentation consiste en un ensemble de fonctions membres qui
fonctionne d'une façon cohérente ensemble. Alors, on les
remplace toutes, ou on n'en remplace aucune.



Alors toi, quand tu créé une nouvelle espèce de chien, tu remplace
toute la biologie et la réimplémente autrement? Il n'y pas assez
d'éléments pour autant d'espèces!

Bien au contraire, les promesses de la POO, à savoir réutilisation,
factorisation, productivité, etc vienne du fait que l'on puisse
implémenter une nouvelle fonctionnalité en créant une sous-classe et
en implémentant que les méthodes qui diffèrent de la super-classe, en
général il suffit de surcharger un trés petit nombre de méthodes.

--
__Pascal Bourguignon__
Avatar
pjb
Michael DOUBEZ writes:

Pascal J. Bourguignon wrote:
Michael DOUBEZ writes:
Une autre solution serait d'utiliser une méthode crochet, c'est à dire
qu'on défini dans la super classe une méthode WX qui s'assure que la
méthode Y est appelée en même temps que X, la superclasse ne
définissant pas X (ou fournissant une définition par défaut vide), en
spécifiant simplement qu'une sous-classe doit surcharger X (comme elle
le veut, la méthode Y étant appelée automatiquemetn par WX.


Le problème ici est qu'il peut y avoir un nombre indéfini de surcharge
de la classe et chaque surcharge intermédiaire doit être appelée.


Bien entendu, toutes ces méthodes sont virtuelles, et quand la super
classe envoit le message this->X(), c'est la méthode de la sous-classe
dont this est exactement l'instance qui est appelée. Tout ce qu'on lui
demande c'est d'assurer la post-condition. Le mieux pour elle c'est
d'appeler la méthode X de la superclasse mais si elle ne le fait pas
ce n'est pas grave, et de toutes façons WX s'assure que Y soit
appelée.



Ce qui ne marche que pour un niveau d'hériatge à moins que je n'ai mal
compris. Ici, cette post condition doit être réitérée dans chaque sous
classe pour elle même et pour sa future classe dérivée. A moins de
définir n fonctions membres (1 de plus dans chaque sous classe) je
vois pas comment faire.

Ce qu'il y ici c'est qu'il est difficile d'exprimer dans le langage la
séquence (next-class-to-call) pour réaliser le chainage; d'ailleur on
préfèrera la composition pour ce genre de cas.

Pour le cas de l'OP, il est peut ête possible de faire quelque chose
avec des templates (le problème est le method-hiding)), j'y
réfléchirai.



Ça marchera aussi avec une hierarchie plus profonde. On peut
surcharger la méthode à chaque niveau, mais effectivement chaque
surcharge doit assurer les post-conditions, et donc a la possibilité
d'appeler la superclasse pour implémenter la partie commune, qui ne
change pas d'une classe à un sous-classe.


class Point {
protected:
float x,y;
public:
virtual void draw()
// subclasses must call this method to ensure side effects.
{
/* a point is infinitely small, so it is invisible */
Log<<"Point at "<<x<<", "<<y;
}
};

class PhysicalPoint : public Point {
protected:
float diameter;
public:
virtual void draw(){
this->Point::draw(); // no effect on Screen...
Screen->drawCircle(x,y,diameter);
}
};


class ColoredPoint : public PhysicalPoint {
protected:
Color color;
public:
virtual void draw(){
Screen->setForegroundColor(color)
this->PhysicalPoint::draw();
}
};


class CircularPoint : public ColoredPoint {
public:
// nothing to override, default is circular.
};

class SquarePoint : public ColoredPoint {
public:
virtual void draw(){
this->Point::draw();
Screen->setForegroundColor(color)
Screen->drawRectangle(x-diameter/2,y-diameter/2,
x+diameter/2,y+diameter/2);
}
};



--
__Pascal Bourguignon__
Avatar
James Kanze
On Mar 5, 12:16 pm, (Pascal J. Bourguignon)
wrote:
James Kanze writes:
> On Mar 4, 5:20 pm, (Pascal J. Bourguignon)
> wrote:
>> Jean-Marc Bourguet writes:
>> > (Pascal J. Bourguignon) writes:



[...]
> Ça me semble une violation du principe de la séparation des
> concernes. La classe de base définit l'interface (le
> contrat), non l'implémentation. Et dans la pratique, je ne
> vois pas comment elle pourrait offir une implémentation par
> défaut. Une implémentation consiste en un ensemble de
> fonctions membres qui fonctionne d'une façon cohérente
> ensemble. Alors, on les remplace toutes, ou on n'en remplace
> aucune.



Alors toi, quand tu créé une nouvelle espèce de chien, tu
remplace toute la biologie et la réimplémente autrement? Il
n'y pas assez d'éléments pour autant d'espèces!



Je ne crois pas que l'analogie tient.

Bien au contraire, les promesses de la POO, à savoir
réutilisation, factorisation, productivité, etc vienne du fait
que l'on puisse implémenter une nouvelle fonctionnalité en
créant une sous-classe et en implémentant que les méthodes qui
diffèrent de la super-classe, en général il suffit de
surcharger un trés petit nombre de méthodes.



Si la fonctionalité est réelement nouvelle, on ne trouvera rien
dans une classe existante qui aidera.

Ce que tu décris, c'est le modèle de conception « template
method » (qui n'a rien à voir avec les templates C++). C'est
une façon de prévoir la customisation d'un composant. Un autre
serait le modèle de stratégie, encore plus souple (et robuste).

Quant à la réutilisation, ça vient surtout de l'encapsulation
dans des composants bien délimités. Ce qui suppose un peu de
programmation par contrat, et donc, des contraints sur la
dérivation.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
Avatar
James Kanze
On Mar 5, 12:13 pm, (Pascal J. Bourguignon)
wrote:
James Kanze writes:
> On Mar 4, 4:27 pm, (Pascal J. Bourguignon)
> wrote:
>> Alexis Guillaume writes:
>> >> Il y a le pattern "chaine de responsabilité" que tu peux
>> >> employer. Ca a l'air d'en être une forme (je ne peux pas
>> >> dire avec les éléments que tu donnes).



>> > En effet je me renseigne sur ce pattern et ça a l'air d'être
>> > ce que je veux faire. Merci beaucoup ! :-)



>> Oui, enfin, c'est le fonctionnement normal en programmation
>> objet quand on surcharge une méthode, d'appeler la méthode de
>> la superclass.



> Je dirais plutôt que c'est une symptome d'une mauvaise
> conception. En général, on ne supplante que des fonctions
> virtuelles pures.



Pas en POO. L'héritage est un élément essentiel de la POO.



Oui, mais l'héritage de quoi ? De l'interface, ou de
l'implémentation ? En C++, on fait la distinction.

OO = {héritage, encapsulation, polymorphisme, abstraction}.



Si on se contente de définir des interfaces d'un côté, et de
les implémenter d'un autre, on exclu l'héritage, et on ne
programme pas en OO.



Qu'est-ce que tu racontes là ? L'implémentation d'une
interface, c'est bien l'héritage.

C'est vrai qu'il y a un certain temps, au début, quand il n'y
avait que du Smalltalk, la situation était différente. Mais
notre compréhension a évolué depuis. Ainsi que des démandes de
robustesse et de la vérification statique des types.

>> C'est tellement normal, que dans les langages de
>> programmation objet normaux, il y a en général un mot clé
>> ou une syntaxe particulière, quand ce n'est pas
>> automatique.
> [...]



> En somme, je crois qu'on est d'accord. Sauf que plutôt que
> de compter sur la classe dérivée pour assurer le contrat de
> la classe de base, il vaut mieux utiliser l'idiome standard,
> avec une fonction virtuelle privée.



On est d'accord, cependant le principe de substitution de
Lyskov va plus loin: une sous-classe peut accepter une version
plus large de la pré-condition, et assurer une version plus
stricte de la post-condition.



Certes. Une sous-classe peut définir une extension de
l'interface ou du contrat, à condition qu'elle respecte aussi le
contrat initial. Et le client, pour y accéder, utilise
dynamic_cast pour accéder à l'interface étendue. Mais quel
rapport avec un appel à une fonction virtuelle qu'on a
supplantée ; la vérification du contrat se fait dans des
fonctions non-virtuelles.

Ainsi, l'idiome que tu appelles "consacré" n'est pas du tout
adapté.



Au contraire, il marche très bien dans ce cas-là.

class Automobile {
public:
virtual void attachWheel(Wheel* w);
// PRE: w->isCircular() and (0<=this->wheelCount()<this->maxWheelC ount())
// POST: this->hasWheel(w) and (this->wheelCount() == 1 + old thi s->wheelCount())
// and (for all v in this->wheels(), (v == w) or (old this- >hasWheel(v)))

// ...
};



class RusticAutomobile : public Automobile {
virtual void attachWheel(Wheel* w);
// PRE: (0<this->wheelCount()<=this->maxWheelCount())
// POST: this->hasWheel(w) and (0<this->wheelCount()<=this->maxWhee lCount())
// and (for all v in this->wheels(), (v == w) or (old this- >hasWheel(v)))



// ...
};



void RusticAutomobile::attachWheel(Wheel* w)
{
assert(0<this->wheelCount()<=this->maxWheelCount()); // RusticAutom obile precond
if(not w->isCircular()){
Hammer* h=new Hammer(Hammer::BIG);
while(not w->isCircular()){
h->bangOn(w);
}
}
assert(w->isCircular() and (0<this->wheelCount()<=this->maxWheelCou nt()));
if(this->wheelCount()==this->maxWheelCount()){
this->removeWheel(this->selectOldestWhell());
}
assert(w->isCircular()
and (0<this->wheelCount()<this->maxWheelCount())); // Automobi le precond
Automobile::attachWheel(w);
assert(this->hasWheel(w) and (this->wheelCount() == 1 + old this- >wheelCount())
and (for all v in this->wheels(), (v == w) or (old this->h asWheel(v))));
// Automobile postcond
// ==>
assert(this->hasWheel(w) and (0<this->wheelCount()<=this->maxWheelC ount())
and (for all v in this->wheels(), (v == w) or (old this->h asWheel(v))));
// RusticAutomobile postcond

}



Mais ce qui est intéressant maintenant, c'est que comme
preCondition(Automobile::attachWheel) ==> preCondition(RusticAutomobi le::attachWheel)
et
postCondition(RusticAutomobile::attachWheel) ==> postCondition(Automo bile::attachWheel)



on peut substituer une automobile rustique partout où on a une
automobile, et ça continuera à fonctionner.



Automobile* a = new RusticAutomobile();
road->materialize(a); // road n'a pas besoin de savoir qu'il existe des
// automobiles rustiques, pour lui, toutes les
// automobiles sont des Automobiles.



Et on pourra, dans le code qui sait que l'automobile est
rustique, y attacher des roues carrées.



RusticAutomobile* r = new RusticAutomobile();
r->attachWheel(new Wheel(Wheel::SQUARE));
road->materialize(r);



Ce qui est très beau, mais il ignore l'essentiel de l'idiome
consacré. Dans l'idiome consacré, attachWheel ne serait pas
virtuelle, mais appelerait une fonction privée (doAttachWheel)
qui elle serait virtuelle. Si on appelle attachWheel à travers
l'interface Automobile, on doit respecter le contrat de
Automobile::attachWheel ; il n'y a que si on l'appelle à
travers l'interface RusticAutomobile qu'on a droit d'attacher
une roue carrée.

class Automobile {
public:
void attachWheel(Wheel& w)
{
assert( w.isCircular()
&& wheelCount() >= 0 // mais c'est plutôt
// une invariant, je
crois...
&& wheelCount() < maxWheelCount() ) ;
Automobile old( *this ) ;
doAttachWheel( w ) ;
assert( hasWheel( w )
&& wheelCount() == old.wheelCount() + 1
&& ... ) ;
}

private:
virtual void doAttachWheel( Wheel& w ) = 0 ;
};

class RusticAutomobile : public Automobile {
public:
void attachWheel(Wheel& w) ;

private:
virtual void doAttachWheel( Wheel& w )
{
attachWheel( w ) ;
}
};

void RusticAutomobile::attachWheel(Wheel& w)
{
assert( wheelCount() >= 0 && wheelCount() <= maxWheelCount
() ) ;
if( ! w.isCircular() ) {
Hammer h( Hammer::BIG ) ;
while( ! w.isCircular() ) {
h.bangOn( w ) ;
}
}
assert( w.isCircular() ) ;
if( wheelCount() == maxWheelCount() ) {
removeWheel( selectOldestWheel() ) ;
}
// Ce qu'il faut pour attacher la roue...
}

Réalistiquement, j'imagine que le mechanisme pour attacher les
roues est différent pour un RusticAutomobile que pour d'autres
voitures, mais s'il y a plusieurs types finaux dont on attache
les roues de la même façon, on pourrait bien se servir d'un
mix-in pour réfacturer le comportement commun. Mais c'est une
hiérarchie à part, qui n'implémente pas (normalement, en tout
cas) l'interface publique de Automobile (et qui ne se charge
donc pas des pré- et des post-conditions de cette interface).

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34
1 2 3 4 5