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
David FLEURY
Il faut les rendre explicites.

Au pire:

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




J'imagine que c'est l'origine de la pratique (en c++) de mettre les
fonctions publiques non virtuelle, et celles protected virtuelles.


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



Oulà... ce n'est pas mon monde du tout.

[...]


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

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

si c'est ce qu'on veut dire?



Bah, je vois ne vois ni contre nature ni changement de type ici.
je parlerais plutôt de

{
auto i = 3; // i est in int
i = std::string( "HELLO" ); // i devient une chaine ???
}


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.



En théorie...
En pratique, ce n'est pas vraiment le cas.

Et puis, rien n'empêche de valider de manière externe un programme.
Avatar
Pascal J. Bourguignon
Laurent Georget writes:

Le 16/04/2012 12:43, Pascal J. Bourguignon a écrit :
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.



C'est justement la question! Qu'est ce que ça signifie que "rouler"?

La sémantique est donnée/spécifiée par les pre- et post-conditions.
Si on établi dans les pré- et post-conditions que rouler consiste à se
déplacer parallèlement à une surface à peu près plane et à peu près
horizontale, (pourquoi irait-on spécifier quoi que ce soit d'autre sur
cette surface, comme sa solidité ou sa matière?), alors un bateau
"roule" aussi sur l'eau. (Et un avion "roule" en l'air en général, je
n'ai rien spécifié sur la distance entre la "Voiture" et la surface).



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.



Mais c'est tout à fait concrêt. Je n'ai encore jamais vu de code avec
toutes ses pré- et post-conditions. En théorie à la fac, oui. J'ai
aussi ouï-dire que ça existait par exemple à la NASA, mais en pratique
je n'en ai jamais vu, donc en pratique les pré-conditions sont Vrai et
les post-conditions sont Faux.


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.



Exact. Je n'en trouve pas.

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

Le 16/04/2012 12:43, Pascal J. Bourguignon a écrit :
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.



C'est justement la question! Qu'est ce que ça signifie que "rouler"?

La sémantique est donnée/spécifiée par les pre- et post-conditions.
Si on établi dans les pré- et post-conditions que rouler consiste à se
déplacer parallèlement à une surface à peu près plane et à peu près
horizontale, (pourquoi irait-on spécifier quoi que ce soit d'autre sur
cette surface, comme sa solidité ou sa matière?), alors un bateau
"roule" aussi sur l'eau. (Et un avion "roule" en l'air en général, je
n'ai rien spécifié sur la distance entre la "Voiture" et la surface).





Oui, mais c'est là qu'apparaît la faille. Rouler ne nécessite clairement
pas que la route soit horizontale et peut être terrestre. Dans le cas
d'un Bateau, le chemin est horizontal et uniquement aquatique. Donc on
ne peut pas utiliser un Bateau dans les mêmes conditions qu'une Voiture
donc on a violé le LSP. Tandis qu'en restant plus abstrait, et en
faisant hériter et Voiture Bateau de Vehicule dans lequel on définit le
concept de "deplacer" et de "chemin" sans en dire davantage -- un peu
comme tu l'as fait pour rouler -- on est plus sûr de rester cohérent et
valide.

Dans la modélisation objet, je ne pense pas que les modélisations les
plus surprenantes soient les meilleures. Ce qui nous semble "naturel"
peut se révéler trompeur mais ce qui heurte de plein fouet le bon sens à
la première lecture est rarement approprié.

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.



Mais c'est tout à fait concrêt. Je n'ai encore jamais vu de code avec
toutes ses pré- et post-conditions. En théorie à la fac, oui. J'ai
aussi ouï-dire que ça existait par exemple à la NASA, mais en pratique
je n'en ai jamais vu, donc en pratique les pré-conditions sont Vrai et
les post-conditions sont Faux.




Je disais que ce n'était pas concret dans la mesure où l'on ne sait pas
à quoi vont servir les carrés et les rectangles que l'on cherche à
modéliser. À l'école, on passe son temps à définir des Polygone à partir
de Point (j'ai dû étudier cette exemple trois fois en cours de Java)
mais curieusement, une fois que l'on code "pour de vrai", on fait
beaucoup moins de maths.

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.



Exact. Je n'en trouve pas.

Avatar
Marc Boyer
Le 15-04-2012, Marc a écrit :
"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 rafiner un peu en disant qu'en C++, on n'utilise souvent
pas assez les classes abstraites, et en heritant de classes
concrètres, on récupère simultanément
1) un sous-type
2) des attributs et une implantation par défaut.

Cela correspond aux besoins courants, mais pas à tous les besoins.

Il manque, à ma connaissance, la possibilité de n'hériter que de
l'interface d'une classe concrète. Il faut redécouper la classe
dont on veut hériter en classe abstraite/concrète, et aller
modifier le code utilisateur déjà fait.

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Marc Boyer
Le 16-04-2012, Laurent Georget - slrn a écrit :
On Mon, 16 Apr 2012 00:45:33 +0200,
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 la relation Véhicule/Voiture ne correspond pas ien à votre
souci de nombre d'attributs, car Véhicule sera sûrement une
classe complètement abstraite, et Voiture concrète, ou partiellement
concrète.

Mais dans le cas des objets
mathématiques ; effectivement ça ne marche plus vraiment.



Parce que le monde mathématique a une sémantique de valeur
et le onde réel a plus souvent une sémantique de référénce.

Si je translate un carré, j'obtiens un autre carré. Alors que
si je transalate une voiture, c'est la même voiture.

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.



"Carré herite de Rectangle" n'a de sens que si on garde la sémantique
mathématique de valeur. Dans cette sémantique, une fonction
qui permet d'élargir un Rectangle ne modifie pas le Retangle mais
en retourne un nouveau.

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 ?



La question de la redéfinition est, à mon sens, secondaire. C'est celle
de la validité du code qui est importante. Si je prends le cas d'un
éditeur graphique qui permet de faire une déformation sur une forme,
est-ce que je peux lui passer un carré quand elle attend un rectangle ?

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Marc Boyer
Le 16-04-2012, David FLEURY a écrit :
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).



Mais les invariants sont plus souvent liés à la représentation
interne. Ce sont les pré/post qui donnent généralement le
comportement visible.

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.



C'est souvent l'usage qui en est fait, mais ce n'est pas uniquement
come ça qu'l faut penser quand on conçoit une hiérarchie de type.

Si je pense à une pile finie, je vais faire un truc du genre
Pile::put(int i)
pre-condition: size() < capacite()
post-condition: size()= old(size()) + 1
top() == i

Mais dans le code, autant je vais ajouter
assert( size() < capacite() )
quant on teste rarement que le nouveau sommet de pile
correspond à l'argument. Pourtant, c'est la sémantique
de la pile.

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.



Ce qui montre qu'un Carré modifiable n'est
pas un Rectangle modifiable.

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.



Exactement. Donc un Carré modifiable n'est pas un Rectangle modifiable.

Mais si vous pensez qu'une méthoe Elargir retourne un nouveau Rectangle,
ça marche.

Rectangle Rectangle::Elargir(int l)
pre-cond: l >= 0
post-cond: res.hauteur() == self.hauteur() and res.largeur() == l

Rectangle Carre::Elargir(int l)
pre-cond: l >= 0
post-cond: res.hauteur() == self.hauteur() and res.largeur() == l

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...



De même qu'on ne prouve pas tous les algos qu'on code. Mais si
on essaye de raisonner sur une hiérarchie de classe, en considérant
le principe de substitution comme paradigme, c'est par les
pre/post qu'il faut raisonner.

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Pascal J. Bourguignon
Marc Boyer writes:

La question de la redéfinition est, à mon sens, secondaire. C'est celle
de la validité du code qui est importante. Si je prends le cas d'un
éditeur graphique qui permet de faire une déformation sur une forme,
est-ce que je peux lui passer un carré quand elle attend un rectangle ?



Cette question est un bon exemple.

La réponse dépend complètement des préconditions de l'éditeur graphique.

On peut écrire un éditeur graphique qui plantera, ou qui aura pour
l'utilisateur un comportement ératique, quand on lui passe un carré au
lieu d'un rectangle (même si c'est une sous-classe de rectangle, si
changer_largeur change aussi la hauteur).

Ou on peut écrire un éditeur graphique qui se comportera correctement
dans ce cas, reflétant à l'utilisateur le fait que changer_largeur
change aussi la hauteur.


--
__Pascal Bourguignon__ http://www.informatimago.com/
A bad day in () is better than a good day in {}.
Avatar
espie
In article <jn35ti$98k$,
Marc Boyer wrote:
Le 15-04-2012, Marc a écrit :
"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 rafiner un peu en disant qu'en C++, on n'utilise souvent
pas assez les classes abstraites, et en heritant de classes
concrètres, on récupère simultanément
1) un sous-type
2) des attributs et une implantation par défaut.

Cela correspond aux besoins courants, mais pas à tous les besoins.

Il manque, à ma connaissance, la possibilité de n'hériter que de
l'interface d'une classe concrète. Il faut redécouper la classe
dont on veut hériter en classe abstraite/concrète, et aller
modifier le code utilisateur déjà fait.



C'est peut-etre les pratiques anciennes, mais il y a quand meme pas
mal de gens qui evangelisent les bonnes pratiques, comme Sutter ou Meyers.

Il faut decouper les classes en trucs gerables. Le principe 1 classe,
1 responsabilite, s'applique particulierement bien en C++.

Le gros interet d'avoir un langage avec une abstraction penalty tres faible
(voire inexistante si on sort l'artillerie lourde et qu'on inline
judicieusement) c'est qu'on peut ecrire les choses de maniere tres
detaillee sans avoir a payer de prix au runtime...

Les gens qui codent des grosses classes viennent souvent d'univers ou
ecrire de toutes petites classes coutent tres cher, parce qu'on empile 15
abstractions l'une sur l'autre...

Ne pas hesiter a leur faire lire a peu pres tout Sutter... ;-)
Avatar
Marc Boyer
Le 23-04-2012, Marc Espie a écrit :
In article <jn35ti$98k$,
Marc Boyer wrote:
On peut rafiner un peu en disant qu'en C++, on n'utilise souvent
pas assez les classes abstraites, et en heritant de classes
concrètres, on récupère simultanément
1) un sous-type
2) des attributs et une implantation par défaut.

Cela correspond aux besoins courants, mais pas à tous les besoins.

Il manque, à ma connaissance, la possibilité de n'hériter que de
l'interface d'une classe concrète. Il faut redécouper la classe
dont on veut hériter en classe abstraite/concrète, et aller
modifier le code utilisateur déjà fait.



C'est peut-etre les pratiques anciennes, mais il y a quand meme pas
mal de gens qui evangelisent les bonnes pratiques, comme Sutter ou Meyers.



Oui, on va arriver à une situation en C++ similaire à celle du C,
où les bonnes pratiques excluent certaines constructions du langage.

Il faut decouper les classes en trucs gerables. Le principe 1 classe,
1 responsabilite, s'applique particulierement bien en C++.

Le gros interet d'avoir un langage avec une abstraction penalty tres faible
(voire inexistante si on sort l'artillerie lourde et qu'on inline
judicieusement) c'est qu'on peut ecrire les choses de maniere tres
detaillee sans avoir a payer de prix au runtime...



Voui, mais il y a la question du choix par défaut.

Les méthodes ne sont pas virtuelles par défaut. Ca m'a été
expliqué comme un principe "don't pay for what you don't use".
Mais il me semble qu'il aurait été équivalent d'utiliser un
mot clef (static ?) pour faire les méthodes non virtuelles,
et aucun mot clef pour les methodes virtuelles (comme Java).

Mais en effet, ce que je demande (pouvoir n'hériter que de
l'interface d'une classe sans ses membres) pourrait avoir
un sur-coût que j'ai du mal à évaluer (je réalise que j'ai
oublié plein de truc sur l'implantation de l'héritage en C++).

Les gens qui codent des grosses classes viennent souvent d'univers ou
ecrire de toutes petites classes coutent tres cher, parce qu'on empile 15
abstractions l'une sur l'autre...

Ne pas hesiter a leur faire lire a peu pres tout Sutter... ;-)



Quand je vois le mal qu'on a encore à imposer des choses
simples du C, je pense que certains usages déplorables du
C++ dureront "un certain temps".

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Marc Boyer
Le 23-04-2012, Pascal J. Bourguignon a écrit :
Marc Boyer writes:

La question de la redéfinition est, à mon sens, secondaire. C'est celle
de la validité du code qui est importante. Si je prends le cas d'un
éditeur graphique qui permet de faire une déformation sur une forme,
est-ce que je peux lui passer un carré quand elle attend un rectangle ?



Cette question est un bon exemple.

La réponse dépend complètement des préconditions de l'éditeur graphique.



Oui et non.

Le principe de substitution de Liskov, dans sa version courante,
c'est qu'on peut utiliser un dérivé partout où l'on utilisait
une classe de base. Cette notion de "partout", c'est bien sûr,
partout dans le code utilisateur, et ça dépend donc du contexte.

Mais on peut aussi prendre une definition de la classe
par pre/post-condition et invariant, et l'héritage consiste
à autoriser affaiblissement/renforcement des pre/post conditions.

Mais à cette version très formelle, en pratique, il me semble
qu'on utilise beaucoup d'implicite.

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
1 2 3 4 5