J'ai un objet a de classe A, avec une fonction membre b qui fabrique un B, qui a une fonction c qui fabrique un C qui a une fonction foo().
A a;
a.b().c().foo(); // please don't feed the Demeter troll
Dans la classe A, la fonction b() appelle un constructeur de B qui a comme paramètre un pointeur sur le A lui-même
class A {
public:
B b() {
return { this };
}
};
Les instances de B notent ce pointeur. Leur fonction c fabrique un C de la même manière
class B {
A* m_ptr_a;
public:
B (A * ptr_a) : m_ptr_a{ptr_a} {}
C c() {
return { this };
}
};
et pareil dans C, dont la fonction foo() fait quelque chose
class C {
B * m_ptr_b;
public:
C(B *ptr_b) : m_ptr_b{ptr_b} {}
void foo() {
...
}
};
En fait, foo lance une fonction bar de B (non décrite ici) à travers son pointeur m_ptr_b; et la fonction B::bar elle même lance une fonction de son A.
J'ai un truc de ce genre dans un programme, et ça plante.
Je me demandais si on avait une garantie sur la durée de vie du B produit par a.b()
En détail
l'objet a (qui a une durée de vie assez longue, dans son bloc) fabrique une instance de B
l'instance de B fabrique une instance de C
l'instance de C fait une action (foo).
A la dernière étape, est-ce que je suis sûr que l'instance de B, qui est une donnée intermédiaire, existe encore quand foo est lancée ?
- Edité par michelbillaud 14 janvier 2019 à 18:37:02
Le "lifetime extension", c'est lorsqu'un temporaire est assignée a une variable et a sa durée de vie prolongée en dehors de l'expression qui crée ce temporaire.
Ici, le temporaire existe durant tout la "vie" de l'expression, donc je ne pense pas que ce soit ca le probleme.
Bon il faudra que je redéveloppe un exemple minimum quand j'aurais le temps. En plus il s'y mêlait des histoires de templates, donc c'est difficile de savoir.
- Edité par michelbillaud 14 janvier 2019 à 21:53:58
Ceci étant dit, cette situation pose, dés le départ, un problème conceptuel
Car, il faut se rappeler la loi de Déméter, qui nous dit
Si un objet a de type A utilise en interne un objet b de type B, l'utilisateur de l'objet a ne devrait pas avoir à connaître le type B pour pouvoir l'utiliser.
Si bien que, *** dans l'idéal ***, tu devrait n'avoir qu'un service -- au niveau de ta classe A -- qui décrit le travail qui sera exécuté, mais qui ne nécessite la récupération ni de B, ni de C.
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Il s'appuie sur la règle 4 d'une définition précise de la loi de Demeter
For all classes C, and for all methods M attached to C, all objects to which M sends a message must be:
self (this in Java)
M’s argument objects
Instance variable objects of C
Objects created by M, or by functions or methods which M calls
Objects in global variables (static fields in Java)
Rule #4 states, that all objects created during the call to M, either directly or indirectly, are allowed. So the prohibition must be among objects that already existed when the call began.
Pour le raccorder à l'objection de @Koala01 : l'objet a n'utilise pas en interne un objet de type B. Il le _produit_ et le retourne.
C'est discuté aussi dans Clean Code, de Robert "uncle bob" Martin.
- Edité par michelbillaud 15 janvier 2019 à 13:56:42
Accessoirement, j'imagine volontiers des problèmes qui au fond ne violent pas tant que ça la loi de Déméter. Genre une fonction (*) qui renvoie un iterateur vers un type proxy, des chainages à coup de pipes avec les ranges v3, etc.
(*) je n'ai pas dit si la fonction était liée à un conteneur ou à un range produit à la volé façon range v3 justement.
Bref. Déméter, j'essaie de m'y tenir dans l'esprit (tell, don't ask), mais parfois cela introduit tellement de complexité que l'on a vite fait de finir avec un fizzbufzz-enterprise. Tout est dans la modération.
Prise à la lettre, la loi de Démeter interdirait d'écrire
std::cout << "a=" << a << " b=" << b << std::endl;
La loi de Déméter n'a rien contre le chaînage de fonctions (ce que tu fais dans cet exemple précis), mais contre l'obligation de connaître un "détail d'implémentation" que tu donnes à l'utilisateur de tes objets a (en sachant qu'il manipule un objet b) et b (en sachant qu'il manipule un objet c).
Dans l'exemple présent, tu manipules toujours ... le même std::ostream (std::cout, qui est renvoyé à chaque fois sous forme de référence pour permettre le chaînage d'appels)
Dans l'exemple précédent, tu manipule un a pour obtenir un b, que tu vas manipuler pour obtenir un c et -- enfin -- pouvoir lui donner l'ordre qui t'intéresse.
Et ca, c'est effectivement contre la loi de déméter, qui nous dit, en gros, que b devrait fournir "un service" qui permette de donner l'ordre à c "sans que l'utilisateur de b n'aie conscience de manipuler un c", et que a devrait fournir un service (potentiellement le même que b, d'ailleurs) qui permettra de faire appel au service en question de b, toujours, sans que l'utilisateur de a n'aie conscience que l'on est en train de manipuler b (et, de manière indirecte, c).
Je sais que les deux situations semblent particulièrement identiques, mais, si tu y réfléchis trente secondes, tu te rendras compte qu'elles sont tout à fait différentes
michelbillaud a écrit:
Pour le raccorder à l'objection de @Koala01 : l'objet a n'utilise pas en interne un objet de type B. Il le _produit_ et le retourne.
Et donc, tu te retrouve à utiliser une fabrique (a) dont le seul but est de créer "sa petite soeur" (une autre fabrique : b) pour pouvoir produire l'objet qui t'intéresse et pouvoir l'utiliser!
Voilà qui semble un peu tiré par les cheveux, tu ne crois pas ???
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
A priori, le method chaining n'impose pas cela, mais fluent interface oui.
Bon, peu importe. Ca serait amusant de voir l'évolution de ces concepts, mais c'est pas forcement pertinent.
Il est vrai que quand on parle de method chaining, je pense surtout aux flux ou a QString::arg, mais en soi, ça me choque pas d'avoir une fonction qui retourne un nouvel objet. Un exemple classique, c'est les template expression. On utilise des opérateurs qui retourne des types "internes" (dans le sens où ils ne sont pas destinés a etre utilisé directement, mais a etre resolu lors de l'affectation =), que l'on va chainer pour contruire une expression.
Dans un ET, quand on ecrit "a + b + c", le premier operateur + retourne un nouveau type, different du type de a et b. Et sur cet objet, on appelle un second operateur +, qui retourne encore une fois un nouveau type, différent de a, b, c, et (a+b).
Au final, quand on écrit :
a.b().c().foo();
Que b() retourne une reference sur a (fluent interface) ou un nouvel objet (method chaining), Demeter est respecté. Et on le voit bien dans le premier code : a aucun moment, A ne connait C et reciproquement.
Il faudrait peut être aussi faire l'effort de retrouver les raisons qui ont conduit à faire cette recommandation (glorifiee par un titre pompeux pour faire serieux).
Les exemples et les problèmes qu'on veut éviter, quoi.
Il ne manque pas de cas où des recommandations sont des sur-réactions. Genre : ne mettez JAMAIS de commentaires parce que si on changeait le code on risquerait d'avoir des commentaires qui ne correspondent pas au code.
Il ne manque pas de cas où des recommandations sont des sur-réactions. Genre : ne mettez JAMAIS de commentaires parce que si on changeait le code on risquerait d'avoir des commentaires qui ne correspondent pas au code.
Je dirais surtout que c'est parce que c'est au noms (variables, fonctions, classes, namespace) d'etre suffisament expressif. Cela oblige a penser correctement au nommage, plutot que de se reposer sur les commentaires.
Mais oui, bien sur, c'est le genre de règle pour laquelle il y a des sur-réactions. (Au point où certains disent "pas de commentaires", voire "pas de doc").
michelbillaud a écrit:
Il faudrait peut être aussi faire l'effort de retrouver les raisons qui ont conduit à faire cette recommandation (glorifiee par un titre pompeux pour faire serieux).
Pour Demeter, c'est simplement pour réduire les dépendences dans les codes et donc facilité la maintenabilité. Si la classe A connait la classe B uniquement, il y aura moins de couplage que si A connait B et C.
Un exemple concret. Si tu veux implémenter la fonction "conduire" de la classe "voiture", tu auras besoin d'utiliser l'attribut "volant". Mais si pour utiliser "volant", tu as besoin aussi d'utiliser le composant "piston" de "volant", on comprend bien qu'il y a un risque de devoir modifier la fonction "conduire" pour chaque type de moteur. Ce qui est un travail supplémentaire inutile.
(Bien sur, si on veut remplacer "volant" par "manche a balais", il faudra dans ce cas réecrire la fonction "conduire". Mais c'est un problème de respect de l'OCP dans ce cas)
Dans ton code initial, Demeter est respecté : si par exemple, on change le type retourné par B::c(), cela ne change pas le code ni de A, ni de C. A et C sont bien indépendants.
Je me méfie comme de la peste des "exemples concrets" transposés à la programmation :-) Et plus généralement les analogies qu'on introduit sournoisement qui peuvent conduire à des choses très bizarres.
D'habitude, quand on fait une analogie avec les voitures, je coupe court avec "si c'est comme les voitures, comment tu fais klaxonner ?", et on essaie de revenir dans la réalité.
Dans le cas que je regardais, c'est l'interface des Streams, où on écrit (en java pour changer) des trucs comme
Qui va faire un display sur toutes les plaques d'immatriculation des voitures de 2000.
La bibliothèque des streams SPECIFIE que filter et map sont applicables à la classe abstraite Stream dont filter, map et forEach sont des méthodes. Et qu'elles retournent elles-mêmes des Streams.
Donc les considérations (fort respectable dans d'autres contextes) sur le "type retourné qui pourrait changer si on décide de changer l'implémentation" sont tout simplement hors sujet. Les méthodes applicables à l'objet retourné sont parfaitement connues.
- Edité par michelbillaud 16 janvier 2019 à 10:54:39
Les analogies sont faites pour expliquer ou illustrer un concept, pas raisonner dessus. Le but est justement d'ancrer un concept abstrait dans la "réalité" (c'est a dire des choses qu'on connait déjà), pour faciliter la mémorisation.
Si c'est les voitures que tu n'aimes pas, tu peux utiliser des camions.
Les analogies sont faites pour expliquer ou illustrer un concept, pas raisonner dessus. Le but est justement d'ancrer un concept abstrait dans la "réalité" (c'est a dire des choses qu'on connait déjà), pour faciliter la mémorisation.
Si c'est les voitures que tu n'aimes pas, tu peux utiliser des camions.
C'est le cas des fluent interfaces. Mais c'est un cas différent de ce que tu présentes dans ton premier code.
J'ai l'impression qu'on s'égare avec Demeter...
Pas tout à fait pareil, parce que l'operateur << retourne l'objet (std::cout) après avoir agi dessus, ce qui n'est pas le cas des operateurs filter et map, qui retournent d'autres streams que celui auquel ils sont appliqués.
Une classe "métier" qui fournit un Stream de nombres
export class RangeStream extends AbstractStream<number> {
private current : number
private last : number
constructor(first: number, last:number) {
super()
this.current = first
this.last = last
}
hasNext() : boolean {
return this.current <= this.last
}
next() : number {
return this.current++
}
A partir de quoi on peut faire
var str = new rs.RangeStream(1, 5)
str.
filter( n -> n > 3) .
map( n -> n * 100) .
forEach(console.log)
et là on voit bien qu'à a partir d'un RangeStream la méthode filter construit un FilterStream à qui on applique map qui fabrique un MapStream à qui s'applique le forEach.
Si ça fonctionnait comme on a l'habitude (production d'une liste de résultats), on se ficherait de perdre le resultat de filter une fois qu'on a calculé map dessus.
Mais là c'est de l'évaluation à la demande : c'est foreach qui demande au mapstream de lui fournir des valeurs, qui lui même les demande au FilterStream, qui les réclame au RangeStream. Je ne peux pas me permettre de perdre les intermédiaires.
- Edité par michelbillaud 16 janvier 2019 à 17:31:16
Pas tout à fait pareil, parce que l'operateur << retourne l'objet (std::cout) après lavoir agi dessus, ce qui n'est pas le cas des operateurs filter et map
Ah ok, la meme classe, mais pas le meme objet.
Donc on est plus dans un cas similaire aux ranges de Niebler et bien d'autres libs.
Pas tout à fait pareil, parce que l'operateur << retourne l'objet (std::cout) après lavoir agi dessus, ce qui n'est pas le cas des operateurs filter et map
Ah ok, la meme classe, mais pas le meme objet.
Donc on est plus dans un cas similaire aux ranges de Niebler et bien d'autres libs.
Cela reste valide pour Demeter.
Même pas la même classe : des objets de la même famille (implémentent l'interface Stream / dérivent de la classe abstraite kivabien en C++)
Pas une mauvaise idée, ça laisse la liberté d'implémenter soi même d'autres opérateurs sur les Streams, et d'autres "combinateurs", alors que par chainage, on est coincé par ce qui est défini dans l'interface Stream.
EDIT: d'un autre coté, en Java on est un coincé côté "sucre syntaxique", parce qu'on voudrait avoir des trucs qui ressemblent à des enchainements de commandes (pipes), mais qu'on ne peut pas redéfinir les opérateurs binaires. Et forcer à écrire
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
Discord NaN. Mon site.
git is great because Linus did it, mercurial is better because he didn't.
Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C
Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C
Discord NaN. Mon site.
Discord NaN. Mon site.
Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C
Discord NaN. Mon site.
Discord NaN. Mon site.
Discord NaN. Mon site.
Discord NaN. Mon site.