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

POO - Hériter Rectangle d'un Square

42 réponses
Avatar
David FLEURY
Bonjour,

(posté ailleurs mais sans réponse, je tente ma chance ici, étant plus
porté sur le C++)

dans http://www.objectmentor.com/resources/articles/lsp.pdf à propos du
LSP, l'exemple est donné à base d'un Square héritant d'un Rectangle (à
ne pas faire donc)

Auriez-vous une idée d'exemple qui poserait problème pour le cas d'un
Rectangle héritant d'un Square (ou si, à votre avis, le deuxième exemple
que j'ai codé suffit)?

Voici l'argument mis en avant dans l'article (en C++), je reprends
l'exemple donné dans l'article original :
(Un Square héritant d'un rectangle)

// --------------------------------------------------------------------
#include <iostream>

using namespace std;

class Rectangle {
public:
Rectangle( int w = 0, int h = 0 ) : width_(w), height_(h) {}

virtual int width() const { return width_; }
virtual int height() const { return height_; }

virtual void width( int w ) { width_ = w; }
virtual void height( int h ) { height_ = h; }

private:
int width_;
int height_;
};

class Square : public Rectangle {
public:
Square( int w = 0 )
{
width( w );
}

virtual void width( int w ) {
Rectangle::width( w ); Rectangle::height( w );
}

virtual void height( int h ) {
Rectangle::width( h ); Rectangle::height( h );
}
};

// -------- Utilisation du client -------
void g( Rectangle& r ) {
r.width( 5 );
r.height( 4 );

assert( r.width() * r.height() == 20 );
}

int main()
{
Rectangle r( 2, 3 );
g( r );

Square s( 4 );
g( s );
}

Est-ce que celui là pour l'autre sens serait recevable ?
(un Rectangle héritant d'un Square)

// ---------------------------------------------------------------------
#include <iostream>

using namespace std;

class Square {
public:
Square( int w = 0 ) { width( w ); }

int width() const { return width_; }
void width( int w ) { width_ = w; }

virtual double area() const { return width() * width(); }

private:
int width_;
};

class Rectangle : public Square {
public:
Rectangle( int w = 0, int h = 0 )
{
width( w );
height( h );
}

int height() const { return height_; }
void height( int h ) { height_ = h; }

virtual double area() const { return width() * height(); }

private:
int height_;
};

// -------- Utilisation du client -------
void g( Square& r ) {
r.width( 5 );

assert( r.area() == 25 );
}

int main()
{
Square s( 4 );
g( s );

Rectangle r( 2, 3 );
g( r );
}


Cordialement.

10 réponses

1 2 3 4 5
Avatar
Pascal J. Bourguignon
David FLEURY writes:

(posté ailleurs mais sans réponse, je tente ma chance ici, étant plus
porté sur le C++)

dans http://www.objectmentor.com/resources/articles/lsp.pdf à propos
du LSP, l'exemple est donné à base d'un Square héritant d'un Rectangle
(à ne pas faire donc)

Auriez-vous une idée d'exemple qui poserait problème pour le cas d'un
Rectangle héritant d'un Square (ou si, à votre avis, le deuxième
exemple que j'ai codé suffit)?



Dans le cadre de la logique de Hoare, qui sert pour la preuve des
programmes, on défini pour chaque instruction de base une pré-condition
et une post-condition:

{ P }
I;
{ Q }

Si la pré-condition P est vraie avant d'exécuter l'instruction I, et si
on exécute l'instruction I, alors la post-condition Q sera vraie après
l'exécution de I.

Par exemple :

{ objet.hauteur = α ∧ objet.largeur = α ∧ increment = β }
objet.incrementer_largeur(increment);
{ objet.hauteur = α+β ∧ objet.largeur = α+β ∧ increment = β }

(On utilise des lettres grecques pour dénoter des "variables"
mathématiques).

Donc, le problème qui se pose dans le cadre de la programmation objet,
c'est que la procédure exécutée dépend de la classe de l'objet auquel on
envoit le message. Si on veut que P & I ⇒ Q reste vrai quand objet est
une instance d'une sous-classe, il faut qu'on ait les relations logiques
suivantes:

{ Pc } Classe::méthode(i); { Qc }
{ Ps } Sousclasse::méthode(i); { Qs }
Pc ⇐ Ps ∧ Qs ⇒ Ps

C'est à dire que la pré-condition attendue par une méthode d'une
sous-classe doit être moins forte que la pré-condition attendue d'une
super-classe, et au contraire, que la post-condition obtenue d'une
méthode d'une sous-classe doit être plus forte que la post-condition
obtenue de la super-classe.

Ceci permet de substituer sans difficulter une instance d'une
sous-classe partout où on a une instance d'une superclasse.

C'est le principe de susbstitution de Lyskov.
http://en.wikipedia.org/wiki/Liskov_substitution_principle
http://fr.wikipedia.org/wiki/Principe_de_substitution_de_Liskov



Note que ce principe ne dit rien à propos des classes et de leur
hierarchie, mais seulement à propos des méthodes et de leur pré- et
post-conditions.

Comme dans ton exemple tu n'as spécifié aucun couple de pré- et
post-conditions, on ne peut rien dire sur sa validité. Le programme
fait peut être exactement ce qu'il est supposé de faire.

Note aussi que l'on peut supposer comme pré-condition: Vrai, et comme
post-condition: Faux pour toutes les méthodes. Et donc le principe de
susbstitution de Lyskov est assuré trivialement.

Vrai ⇐ Vrai ∧ Faux ⇒ Faux.


Maintenant, il est vrai qu'il y a quelque chose d'étrange dans la
relation entre carré et rectangle. D'habitude, dans une sous-classe on
a plus d'attributs, car les instances d'une sous-classe sont plus
spécifiques. Mais si on établi Rectangle comme super-classe et Carré
comme sous-classe on obtenient une sous-classe avec "moins" d'attributs
que sa super-classe.

Cependant, à mon avis, il est préférable de les hierarchiser de cette
manière, car ça correspond à une définition ensembliste plus simple:
L'ensemble des rectangles a un sous-ensemble de rectangles dont les
côtés sont égaux: les carrés.

Mais ça n'empêche pas de préfére dire que les rectangles sont des sortes
de carrés applatis.


Premier cas:

classe Rectangle
largeur
hauteur

méthode Rectangle::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α > 0 [Pc]
post-condition: self.largeur = α [Qc]


classe Carré, sous-classe de Rectangle

méthode Carré::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α > 0 [Ps]
post-condition: self.largeur = α ∧ self.hauteur = α [Qs]


Preuve du principe de substitution:

Pc ⇐ Ps ∧ Qs ⇒ Ps
⇔ nouvelle_largeur = α ∧ α > 0 ⇐ nouvelle_largeur = α ∧ α > 0
∧ (self.largeur = α ∧ self.hauteur = α) ⇒ self.largeur = α
⇔ Vrai



Deuxième cas:

classe Carré
côté

méthode Carré::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α > 0 [Ps]
post-condition: self.côte = α [Qs]


classe Rectangle
largeur
hauteur

méthode Rectangle::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α > 0 [Ps]
post-condition: self.côte = α ∧ self.largeur = α
∧ self.hauteur = α [Qs]


Preuve du principe de substitution:

Pc ⇐ Ps ∧ Qs ⇒ Ps
⇔ nouveau_côté = α ∧ α > 0 ⇐ nouveau_côté = α ∧ α > 0
∧ (self.côte = α ∧ self.largeur = α ∧ self.hauteur = α) ⇒ self.côté = α
⇔ Vrai


Donc ça marche aussi. Rien ne nous empêche maintenant de définir des
méthodes spécifiques à la sous-classe:

méthode Rectangle::changer_largeur (nouvelle_largeur)

pre-condition: nouvelle_largeur = α ∧ α > 0
∧ hauteur = θ [Ps]

post-condition: self.côte = max(α,θ)
∧ self.largeur = α
∧ self.hauteur = θ [Qs]

En conclusion: il faut définir les pré- et post-conditions de chaque
méthode (et les invariants de chaque classe, mais c'est une autre
question).

--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
Marc
"Pascal J. Bourguignon" wrote:

Maintenant, il est vrai qu'il y a quelque chose d'étrange dans la
relation entre carré et rectangle. D'habitude, dans une sous-classe on
a plus d'attributs, car les instances d'une sous-classe sont plus
spécifiques. Mais si on établi Rectangle comme super-classe et Carré
comme sous-classe on obtenient une sous-classe avec "moins" d'attributs
que sa super-classe.



C'est assez courant que la relation "être un cas particulier de" et la
relation "contenir un" soient inversées (nombres réels vs complexes,
entiers vs rationnels, etc), ce qui rend parfois les choses peu
pratiques dans les langages comme le C++ où l'héritage implique le
second.
Avatar
Pascal J. Bourguignon
Marc writes:

"Pascal J. Bourguignon" wrote:

Maintenant, il est vrai qu'il y a quelque chose d'étrange dans la
relation entre carré et rectangle. D'habitude, dans une sous-classe on
a plus d'attributs, car les instances d'une sous-classe sont plus
spécifiques. Mais si on établi Rectangle comme super-classe et Carré
comme sous-classe on obtenient une sous-classe avec "moins" d'attributs
que sa super-classe.



C'est assez courant que la relation "être un cas particulier de" et la
relation "contenir un" soient inversées (nombres réels vs complexes,
entiers vs rationnels, etc), ce qui rend parfois les choses peu
pratiques dans les langages comme le C++ où l'héritage implique le
second.



On peut toujours créer une super-classe/sur-ensemble.

Mais il faut considérer que un Rectangle tel que largeur=hauteur doit
être égal à un carré de même côté. La question est alors de savoir si
on interne les objets "égaux" ou si on accepte des duplicatats.

Aussi, certains langages permettent de changer la classe d'un objet en
cours de route, ça peut aider. Ainsi, en Common Lisp, si un rectangle
obtenient une largeur égale à sa hauteur, on peut lui faire changer de
classe pour devenir carré, et vice-versa. Ça simplifie ce genre de
problème.



--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
David FLEURY
Je n'ai jamais trop réfléchi en terme de pré/post mais plutôt
d'invariant (qui serait donc toujours vrai à n'importe qu'elle moment de
la vie de l'instance et pour n'importe qu'elle méthode).
Je vois ici plutôt les pré conditions comme une validation des arguments
uniquement. Les post-conditions étant des invariants sur lesquelles
l'utilisateur d'une classe peut valider l'usage qu'il en fait.

[...]

Donc la difficulté, il me semble vient de trouver les pré/post
conditions adéquates, celles dépendant de l'usage qu'on fait de ces
classes. Je crois que dans l'article, il parle qu'on ne peut connaître
la validité d'une hiérarchie de manière isolée.

Si j'ajoute des post-conditions aux exemples donnés.
le principe de substitution ne fonctionne plus.


Premier cas:

classe Rectangle
largeur
hauteur

méthode Rectangle::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Pc]
post-condition: self.largeur = α [Qc]



post-condition = self.largeur = α ∧ self.hauteur = self.ancienne_hauteur
(Je ne sais pas écrire que la hauteur n'a pas été affectée par la
méthode changer_largeur)


classe Carré, sous-classe de Rectangle

méthode Carré::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Ps]
post-condition: self.largeur = α ∧ self.hauteur = α [Qs]



Maintenant, le principe n'est plus vrai que la fonction modifie la
hauteur ce qui est contraire à la post-condition de la classe mère.



Deuxième cas:

classe Carré
côté

méthode Carré::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α> 0 [Ps]
post-condition: self.côte = α [Qs]


classe Rectangle
largeur
hauteur

méthode Rectangle::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α> 0 [Ps]
post-condition: self.côte = α ∧ self.largeur = α
∧ self.hauteur = α [Qs]





C'est pour cet exemple, où j'ai plus de mal à trouver une post-condition
qui ne soit pas valable en n'ayant à disposition que l'interface de la
classe Carré (encore plus avec une implémentation séparant côté et
largeur/hauteur - même si je ne suis plus sûr de voir l'intérêt pour une
dérivation dans ce cas, du code devant être dupliqué pour maintenir la
cohérence entre ses membres.)


Preuve du principe de substitution:

Pc ⇐ Ps ∧ Qs ⇒ Ps
⇔ nouveau_côté = α ∧ α> 0 ⇐ nouveau_côté = α ∧ α> 0
∧ (self.côte = α ∧ self.largeur = α ∧ self.hauteur = α) ⇒ self.côté = α
⇔ Vrai


Donc ça marche aussi. Rien ne nous empêche maintenant de définir des
méthodes spécifiques à la sous-classe:

méthode Rectangle::changer_largeur (nouvelle_largeur)

pre-condition: nouvelle_largeur = α ∧ α> 0
∧ hauteur = θ [Ps]

post-condition: self.côte = max(α,θ)
∧ self.largeur = α
∧ self.hauteur = θ [Qs]



C'est là, où une post-condition (un invariant) de classe invaliderait
cette hiérarchie.

post-conditions : angle(self.diagonales)= 90° ∧ area( côté * côté)


En conclusion: il faut définir les pré- et post-conditions de chaque
méthode (et les invariants de chaque classe, mais c'est une autre
question).



Ah, je n'avais pas vu que l'invariant de classe était évoqué ici.
Tous les définir, ça risque de prendre du temps...

Donc, j'ai l'impression qu'une hiérarchie de classe se valide par ses
pré/post conditions et invariant, qui en valide ainsi l'usage qu'on en fait.
Avatar
Laurent Georget - slrn
On Mon, 16 Apr 2012 00:45:33 +0200,
Pascal J. Bourguignon wrote :
Marc writes:

"Pascal J. Bourguignon" wrote:

Maintenant, il est vrai qu'il y a quelque chose d'étrange dans la
relation entre carré et rectangle. D'habitude, dans une sous-classe on
a plus d'attributs, car les instances d'une sous-classe sont plus
spécifiques. Mais si on établi Rectangle comme super-classe et Carré
comme sous-classe on obtenient une sous-classe avec "moins" d'attributs
que sa super-classe.



C'est assez courant que la relation "être un cas particulier de" et la
relation "contenir un" soient inversées (nombres réels vs complexes,
entiers vs rationnels, etc), ce qui rend parfois les choses peu
pratiques dans les langages comme le C++ où l'héritage implique le
second.



On peut toujours créer une super-classe/sur-ensemble.

Mais il faut considérer que un Rectangle tel que largeur=hauteur doit
être égal à un carré de même côté. La question est alors de savoir si
on interne les objets "égaux" ou si on accepte des duplicatats.

Aussi, certains langages permettent de changer la classe d'un objet en
cours de route, ça peut aider. Ainsi, en Common Lisp, si un rectangle
obtenient une largeur égale à sa hauteur, on peut lui faire changer de
classe pour devenir carré, et vice-versa. Ça simplifie ce genre de
problème.




Oui, mais on change alors de paradigme de programmation puisque le lisp
s'appuie sur une vision fonctionnelle des choses.

Pour ma part, j'ai toujours appris qu'une relation d'héritage n'est valide
que si elle a un sens sémantiquement. Par exemple, on peut faire hériter
Voiture de Véhicule car une Voiture est une spécialisation de Véhicule
(un cas d'école longtemps rabaché...:) ). Mais dans le cas des objets
mathématiques ; effectivement ça ne marche plus vraiment.

Je pense cependant que la relation "Carré hérite de Rectangle" a plus de
sens. Par exemple, si jamais on voulait étendre la classe pour
introduire le calcul d'aire. La relation valable pour le rectangle
A=hauteur * largeur resterait valide pour le carré (A=côté*côté),
tandis qu'une modélisation inverse rendrait nécessaire la redéfinition
de la méthode pour le rectangle. Et, évidemment, c'est le cas pour pas
mal de propriétés du rectangle. Donc, pour garantir la maintenabilité,
je privilégierai ce choix d'héritage.

Avez-vous des exemples de méthodes qui fonctionnent dans l'autre sens ?
C'est-à-dire, en supposant que Carré hérite de Rectangle, des méthodes
qui sont établies pour le carré et reste valide pour le rectangle par
héritage, alors que dans la modélisation inverse, elle nécessiterait une
redéfinition ?
Avatar
Pascal J. Bourguignon
David FLEURY writes:

Premier cas:

classe Rectangle
largeur
hauteur

méthode Rectangle::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Pc]
post-condition: self.largeur = α [Qc]



post-condition = self.largeur = α ∧ self.hauteur = self.ancienne_hauteur
(Je ne sais pas écrire que la hauteur n'a pas été affectée par la
méthode changer_largeur)



Par convention, toutes les variables qui ne sont pas mentionnées dans
les pré- et post-conditions sont inchangées. Mais on peut expliciter le
fait que les conditions ne disent rien sur l'état de la hauteur:

méthode Rectangle::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α>0
∧ self.hauteur = β [Pc]
post-condition: self.largeur = α
∧ self.hauteur = γ [Qc]


classe Carré, sous-classe de Rectangle

méthode Carré::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Ps]
post-condition: self.largeur = α ∧ self.hauteur = α [Qs]



Maintenant, le principe n'est plus vrai que la fonction modifie la
hauteur ce qui est contraire à la post-condition de la classe mère.



Seulement si β=γ. Mais si β≠γ, alors il n'y a aucun problème. C'est
donc à toi de choisir si tu as un problème ou non, en choisissant les
pré- et post-conditions de la classe mère.



Deuxième cas:

classe Carré
côté

méthode Carré::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α> 0 [Ps]
post-condition: self.côte = α [Qs]


classe Rectangle
largeur
hauteur

méthode Rectangle::changer_côté (nouveau_côté)
pre-condition: nouveau_côté = α ∧ α> 0 [Ps]
post-condition: self.côte = α ∧ self.largeur = α
∧ self.hauteur = α [Qs]





C'est pour cet exemple, où j'ai plus de mal à trouver une
post-condition qui ne soit pas valable en n'ayant à disposition que
l'interface de la classe Carré (encore plus avec une implémentation
séparant côté et largeur/hauteur - même si je ne suis plus sûr de voir
l'intérêt pour une dérivation dans ce cas, du code devant être
dupliqué pour maintenir la cohérence entre ses membres.)


Preuve du principe de substitution:

Pc ⇐ Ps ∧ Qs ⇒ Ps
⇔ nouveau_côté = α ∧ α> 0 ⇐ nouveau_côté = α ∧ α> 0
∧ (self.côte = α ∧ self.largeur = α ∧ self.hauteur = α) ⇒ self.côté = α
⇔ Vrai


Donc ça marche aussi. Rien ne nous empêche maintenant de définir des
méthodes spécifiques à la sous-classe:

méthode Rectangle::changer_largeur (nouvelle_largeur)

pre-condition: nouvelle_largeur = α ∧ α> 0
∧ hauteur = θ [Ps]

post-condition: self.côte = max(α,θ)
∧ self.largeur = α
∧ self.hauteur = θ [Qs]



C'est là, où une post-condition (un invariant) de classe invaliderait
cette hiérarchie.



C'est à toi de voir.


post-conditions : angle(self.diagonales)= 90° ∧ area( côté * côté)



Si tu introduit des couples pré- et post-conditions qui sont incohérents
avec tes définitions, c'est ton problème. C'est à toi de choisir ce que
tu veux dire, et de t'assurer de ce que tu dis soit cohérent.



En conclusion: il faut définir les pré- et post-conditions de chaque
méthode (et les invariants de chaque classe, mais c'est une autre
question).



Ah, je n'avais pas vu que l'invariant de classe était évoqué ici.
Tous les définir, ça risque de prendre du temps...

Donc, j'ai l'impression qu'une hiérarchie de classe se valide par ses
pré/post conditions et invariant, qui en valide ainsi l'usage qu'on en
fait.



Oui. C'est comme la déclaration des types; Si tu écris:

{
int x;
x="hello";
}

c'est toi qui vois. Comme tu écris une incohérence, un compilateur ou
un vérificateur peut la détecter, et c'est à toi de choisir si tu change
le type ou si tu change la valeur.

--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
Pascal J. Bourguignon
Laurent Georget - slrn writes:

Oui, mais on change alors de paradigme de programmation puisque le lisp
s'appuie sur une vision fonctionnelle des choses.



Non, pas du tout. Lisp est multi-paradigme. Tu choisis pour chaque
bout de code avec quel paradigme tu l'écris.


Pour ma part, j'ai toujours appris qu'une relation d'héritage n'est valide
que si elle a un sens sémantiquement.



La sémantique formelle se mord la queue: elle est définie à partir de
modèles qui sont aussi des systèmes formels. Il n'y a donc pas de
résolution.


Par exemple, on peut faire hériter Voiture de Véhicule car une Voiture
est une spécialisation de Véhicule (un cas d'école longtemps
rabaché...:) ). Mais dans le cas des objets mathématiques ;
effectivement ça ne marche plus vraiment.



On peut aussi s'amuser à faire hériter Véhicule de Voiture. Après tout,
un Véhicule comme un Avion, c'est une sorte de voiture avec des ailes,
ou un Véhicule comme un Bateau, c'est une sorte de voiture qui flotte.



Je pense cependant que la relation "Carré hérite de Rectangle" a plus de
sens. Par exemple, si jamais on voulait étendre la classe pour
introduire le calcul d'aire. La relation valable pour le rectangle
A=hauteur * largeur resterait valide pour le carré (A=côté*côté),
tandis qu'une modélisation inverse rendrait nécessaire la redéfinition
de la méthode pour le rectangle. Et, évidemment, c'est le cas pour pas
mal de propriétés du rectangle. Donc, pour garantir la maintenabilité,
je privilégierai ce choix d'héritage.



Tout dépend des pré- et post-conditions que tu choisis. Si tu n'en
écris aucune, n'importe quoi est possible.


Avez-vous des exemples de méthodes qui fonctionnent dans l'autre sens ?
C'est-à-dire, en supposant que Carré hérite de Rectangle, des méthodes
qui sont établies pour le carré et reste valide pour le rectangle par
héritage, alors que dans la modélisation inverse, elle nécessiterait une
redéfinition ?



Par exemple: déplacer, faire_tourner, colorier, etc.

--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
David FLEURY
classe Rectangle
largeur
hauteur

méthode Rectangle::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Pc]
post-condition: self.largeur = α [Qc]


classe Carré, sous-classe de Rectangle

méthode Carré::changer_largeur (nouvelle_largeur)
pre-condition: nouvelle_largeur = α ∧ α> 0 [Ps]
post-condition: self.largeur = α ∧ self.hauteur = α [Qs]



Pour cet exemple, l'auteur de l'article considère (page 7) que la
post-condition sur Carré::changer_largeur (en supposant l'implicite qui
est donc hauteur inchangé) est plus faible que celui de la classe mère
(post-condition: self.largeur = α)
Donc le principe ne semble pas être respecté ici.


Si tu introduit des couples pré- et post-conditions qui sont incohérents
avec tes définitions, c'est ton problème. C'est à toi de choisir ce que
tu veux dire, et de t'assurer de ce que tu dis soit cohérent.



Le problème viendrait plutôt de quelqu'un qui introduit une nouvelle
classe dans l'arbre d'héritage. A priori, l'auteur sait (ou a su)
utiliser ces classes.

Les pré/post conditions étant implicite, et dans ce cas, on peut juste
supposer qu'elles proviennent du modèle fonctionnelle.

Dans mon exemple, je dirais que ni le carré ni le rectangle ne peuvent
hériter l'un de l'autre sans qu'on puisse trouver un cas d'utilisation
ne cassant pas le principe de substitution. Par contre, il existe des
domaines d'utilisation, ou il est raisonnable de le faire, dans ce cas,
il semble judicieux (sauf à avoir des problèmes qui arriveront avec
l'ajout de classe dans l'arbre) de définir clairement les pré/post
conditions.

C'est comme la déclaration des types; Si tu écris:

{
int x;
x="hello";
}

c'est toi qui vois. Comme tu écris une incohérence, un compilateur ou
un vérificateur peut la détecter, et c'est à toi de choisir si tu
changes le type ou si tu change la valeur.



Pour changer de type, va falloir changer de langage.
De toute façon, je trouve un peu trop "contre nature" un changement de
type...

En tout cas, j'ai l'impression que c'est un sujet encore bien délicat,
ou les théoriciens ne semblent pas tous d'accord, et qu'en pratique on
arrive à bien vivre sans.
Avatar
Pascal J. Bourguignon
David FLEURY writes:

Si tu introduit des couples pré- et post-conditions qui sont incohérents
avec tes définitions, c'est ton problème. C'est à toi de choisir ce que
tu veux dire, et de t'assurer de ce que tu dis soit cohérent.



Le problème viendrait plutôt de quelqu'un qui introduit une nouvelle
classe dans l'arbre d'héritage. A priori, l'auteur sait (ou a su)
utiliser ces classes.

Les pré/post conditions étant implicite, et dans ce cas, on peut juste
supposer qu'elles proviennent du modèle fonctionnelle.



Il faut les rendre explicites.

Au pire:

class::method(int arg…){
assert(pre_condition(this,arg);
// do method.
assert(post_condition(this,arg);
}

Au mieux, utiliser un langage comme Eiffel ou lisp qui permet d'écrire
et de vérifier et/ou prouver les conditions.


Dans mon exemple, je dirais que ni le carré ni le rectangle ne peuvent
hériter l'un de l'autre sans qu'on puisse trouver un cas d'utilisation
ne cassant pas le principe de substitution. Par contre, il existe des
domaines d'utilisation, ou il est raisonnable de le faire, dans ce
cas, il semble judicieux (sauf à avoir des problèmes qui arriveront
avec l'ajout de classe dans l'arbre) de définir clairement les
pré/post conditions.



C'est toi qui vois.


C'est comme la déclaration des types; Si tu écris:

{
int x;
x="hello";
}

c'est toi qui vois. Comme tu écris une incohérence, un compilateur ou
un vérificateur peut la détecter, et c'est à toi de choisir si tu
changes le type ou si tu change la valeur.



Pour changer de type, va falloir changer de langage.
De toute façon, je trouve un peu trop "contre nature" un changement de
type...



Qu'est ce qu'il y a de contre nature à écrire:

{
char* x;
x="hello";
}

si c'est ce qu'on veut dire?


En tout cas, j'ai l'impression que c'est un sujet encore bien délicat,
ou les théoriciens ne semblent pas tous d'accord, et qu'en pratique on
arrive à bien vivre sans.



En pratique, on met pré-condition = Vrai, post-condition = Faux, et on
est tranquille. Mais on ne peut rien prouver sur son programme, et il
peut faire n'importe quoi.


--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
Laurent Georget
Le 16/04/2012 12:43, Pascal J. Bourguignon a écrit :
Laurent Georget - slrn writes:

Oui, mais on change alors de paradigme de programmation puisque le lisp
s'appuie sur une vision fonctionnelle des choses.



Non, pas du tout. Lisp est multi-paradigme. Tu choisis pour chaque
bout de code avec quel paradigme tu l'écris.




Ah, j'ignorais, c'est intéressant. Je n'ai jamais travaillé en Lisp.
Uniquement en Scheme, en OCaml et un peu en Haskell, qui ont un typage
assez rigide (surtout le OCaml).


Pour ma part, j'ai toujours appris qu'une relation d'héritage n'est valide
que si elle a un sens sémantiquement.



La sémantique formelle se mord la queue: elle est définie à partir de
modèles qui sont aussi des systèmes formels. Il n'y a donc pas de
résolution.


Par exemple, on peut faire hériter Voiture de Véhicule car une Voiture
est une spécialisation de Véhicule (un cas d'école longtemps
rabaché...:) ). Mais dans le cas des objets mathématiques ;
effectivement ça ne marche plus vraiment.



On peut aussi s'amuser à faire hériter Véhicule de Voiture. Après tout,
un Véhicule comme un Avion, c'est une sorte de voiture avec des ailes,
ou un Véhicule comme un Bateau, c'est une sorte de voiture qui flotte.




Euh... ça me semble un peu tiré par les cheveux, quand même. Je ne vois
pas trop comment un Bateau est censé implémenter toutes les méthodes
applicables de façon "naturelle" à une Voiture comme la méthode rouler.

Je pense cependant que la relation "Carré hérite de Rectangle" a plus de
sens. Par exemple, si jamais on voulait étendre la classe pour
introduire le calcul d'aire. La relation valable pour le rectangle
A=hauteur * largeur resterait valide pour le carré (A=côté*côté),
tandis qu'une modélisation inverse rendrait nécessaire la redéfinition
de la méthode pour le rectangle. Et, évidemment, c'est le cas pour pas
mal de propriétés du rectangle. Donc, pour garantir la maintenabilité,
je privilégierai ce choix d'héritage.



Tout dépend des pré- et post-conditions que tu choisis. Si tu n'en
écris aucune, n'importe quoi est possible.




Comme quoi, ces considérations théoriques ne sont pas forcément
pertinentes. Il vaut mieux s'appuyer sur un cas d'implémentation concret.


Avez-vous des exemples de méthodes qui fonctionnent dans l'autre sens ?
C'est-à-dire, en supposant que Carré hérite de Rectangle, des méthodes
qui sont établies pour le carré et reste valide pour le rectangle par
héritage, alors que dans la modélisation inverse, elle nécessiterait une
redéfinition ?



Par exemple: déplacer, faire_tourner, colorier, etc.



Ça ne répond pas vraiment à ma question. Si dans ma modélisation "Carré
hérite de rectangle", je définis déplacer, faire_tourner ou colorier
dans Rectangle je n'aurai pas à les redéfinir dans Carré ; les
transformations géométriques associées, la translation et la rotation,
s'appliqueront très bien.
1 2 3 4 5