Pourquoi les chaînes d'enchaînement ne sont-elles pas conventionnelles?


46

Le chaînage sur les haricots est très pratique: vous n'avez pas besoin de surcharger les constructeurs, les méga constructeurs, les usines et vous donne une lisibilité accrue. Je ne peux penser à aucun inconvénient, à moins que vous ne souhaitiez que votre objet soit immuable , auquel cas il n'aurait aucun décideur de toute façon. Donc, y a-t-il une raison pour laquelle ce n'est pas une convention POO?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
Parce que Java peut être la seule langue où setters ne sont pas en abomination à l' homme ...
Telastyn

11
C'est un mauvais style car la valeur de retour n'est pas sémantiquement significative. C'est trompeur. Le seul avantage est d'économiser très peu de frappes.
USR le

58
Ce modèle n'est pas rare du tout. Il y a même un nom pour cela. C'est ce qu'on appelle l' interface fluide .
Philipp

9
Vous pouvez également résumer cette création dans un générateur pour un résultat plus lisible. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Je le ferais pour ne pas contredire l’idée générale selon laquelle les setters sont vides.

9
@Philipp, bien que techniquement vous ayez raison, ne diriez pas que c'est new Foo().setBar('bar').setBaz('baz')très "fluide". Je veux dire, bien sûr, cela pourrait être mis en œuvre exactement de la même manière, mais je m'attendrais beaucoup à lire quelque chose qui ressemble davantage àFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner, le

Réponses:


50

Alors, y a-t-il une raison pour laquelle ce n'est pas une convention de POO?

Ma meilleure supposition: parce que c'est une violation de CQS

Vous avez une commande (modification de l'état de l'objet) et une requête (renvoyant une copie de l'état - dans ce cas, l'objet lui-même) mélangées dans la même méthode. Ce n'est pas nécessairement un problème, mais cela enfreint certaines des directives de base.

Par exemple, en C ++, std :: stack :: pop () est une commande qui renvoie void et std :: stack :: top () est une requête qui renvoie une référence à l'élément supérieur de la pile. Classiquement, vous voudriez combiner les deux, mais vous ne pouvez pas le faire et être exceptionnellement sûr. (Pas de problème en Java, car l’opérateur d’affectation en Java ne jette pas).

Si DTO était un type de valeur, vous pourriez obtenir une fin similaire avec

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

En outre, l’enchaînement des valeurs de retour est une douleur colossale lorsqu’il s’agit d’un héritage. Voir "Modèle de modèle curieusement récurrent"

Enfin, le constructeur par défaut devrait vous laisser un objet dans un état valide. Si vous devez exécuter plusieurs commandes pour restaurer l'objet dans un état valide, quelque chose s'est très mal passé.


4
Enfin, une vraie catain ici. L'héritage est le vrai problème. Je pense que l'enchaînement pourrait être un outil de définition conventionnel pour les objets de représentation de données utilisés dans la composition.
Ben

6
"renvoyer une copie de l'état d'un objet" - cela ne fait pas cela.
user253751

2
Je dirais que la principale raison de suivre ces directives (à quelques exceptions notables comme les itérateurs) est que cela rend le code beaucoup plus facile à raisonner.
jpmc26

1
@MatthieuM. Nan. Je parle principalement Java et il est répandu dans les API "fluides" avancées - je suis sûr que cela existe également dans d'autres langues. Essentiellement, vous faites votre type générique sur un type s'étendant lui-même. Vous ajoutez ensuite une abstract T getSelf()méthode avec renvoie le type générique. Maintenant, au lieu de revenir thisde vos setters, vous return getSelf()et toute classe dominante rendent simplement le type générique en lui-même et reviennent thisde getSelf. De cette façon, les setters renvoient le type réel et non le type déclarant.
Boris l'araignée

1
@MatthieuM. vraiment? Cela ressemble au CRTP pour C ++ ...
Inutile

33
  1. Enregistrer quelques frappes n’est pas convaincant. Ce serait peut-être bien, mais les conventions POO accordent plus d'importance aux concepts et aux structures qu'à la frappe au clavier.

  2. La valeur de retour n'a pas de sens.

  3. Plus encore que ne voulant rien dire, la valeur de retour est trompeuse, car les utilisateurs peuvent s'attendre à ce que la valeur de retour ait une signification. Ils peuvent s’attendre à ce qu’il s’agisse d’un "passeur immuable"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }

    En réalité, votre setter mute l'objet.

  4. Cela ne fonctionne pas bien avec l'héritage.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 

    je peux écrire

    new BarHolder().setBar(2).setFoo(1)

    mais non

    new BarHolder().setFoo(1).setBar(2)

Pour moi, les numéros 1 à 3 sont les plus importants. Un code bien écrit ne concerne pas un texte arrangé agréablement. Un code bien écrit concerne les concepts fondamentaux, les relations et la structure. Le texte n'est qu'un reflet extérieur de la véritable signification du code.


2
La plupart de ces arguments s’appliquent toutefois à toutes les interfaces fluides / pouvant être chaînées.
Casey

@ Casey, oui. Les bâtisseurs (c'est-à-dire avec les setters) est le cas le plus fréquent de chaînage.
Paul Draper

10
Si vous connaissez bien l’idée d’une interface fluide, le n ° 3 ne s’appliquera pas, car vous soupçonnerez probablement une interface fluide du type retour. Je ne suis pas d'accord non plus avec "Un code bien écrit n'est pas un texte arrangé agréablement". Ce n’est pas tout, c’est un texte bien arrangé qui est assez important car il permet au lecteur humain du programme de le comprendre avec moins d’effort.
Michael Shaw

1
Le chaînage par sélecteur ne consiste pas à arranger agréablement le texte ni à enregistrer les frappes au clavier. Il s'agit de réduire le nombre d'identifiants. Avec le chaînage, vous pouvez créer et configurer l'objet dans une seule expression, ce qui signifie que vous n'avez peut-être pas à l'enregistrer dans une variable - une variable que vous devrez nommer et qui y restera jusqu'à la fin de la portée.
Idan Arye

3
À propos du point 4, c’est quelque chose qui peut être résolu en Java de la manière suivante: public class Foo<T extends Foo> {...}avec le paramètre retourneur Foo<T>. Aussi ces méthodes "setter", je préfère les appeler "avec" méthodes. Si la surcharge fonctionne bien, alors Foo<T> with(Bar b) {...}, sinon Foo<T> withBar(Bar b).
YoYo

12

Je ne pense pas que ce soit une convention POO, mais plutôt liée à la conception du langage et à ses conventions.

Il semble que vous aimez utiliser Java. Java a une spécification JavaBeans qui spécifie le type de retour du setter à vide, c'est-à-dire qu'il entre en conflit avec l'enchaînement des setters. Cette spécification est largement acceptée et implémentée dans une variété d’outils.

Bien sûr, vous pourriez vous demander pourquoi la chaîne ne fait pas partie des spécifications. Je ne connais pas la réponse, peut-être que ce motif n'était tout simplement pas connu / populaire à cette époque.


Oui, JavaBeans est exactement ce que j'avais en tête.
Ben

Je ne pense pas que la plupart des applications qui utilisent JavaBeans s’inquiètent, mais elles le pourraient (en utilisant la réflexion pour saisir les méthodes nommées "set ...", un seul paramètre et un retour nul ). De nombreux programmes d'analyse statique se plaignent de ne pas vérifier la valeur renvoyée par une méthode qui renvoie quelque chose, ce qui peut être très gênant.

Les appels @MichaelT réflexifs pour obtenir des méthodes en Java ne spécifient pas le type de retour. L'appel d'une méthode de cette façon renvoie Object, ce qui peut entraîner le renvoi du singleton de type Voidsi la méthode spécifie voidcomme type de retour. Par ailleurs, apparemment, "laisser un commentaire mais pas voter" est un motif d'échec pour un audit de "premier post", même s'il s'agit d'un bon commentaire. Je blâme shog pour cela.

7

Comme d’autres l’ont dit, on parle souvent d’ interface fluide .

Normalement, les setters sont des appels qui transmettent des variables en réponse au code logique dans une application; votre classe DTO en est un exemple. Le code conventionnel lorsque les setters ne renvoient rien est normal pour cela. D'autres réponses ont expliqué de manière.

Cependant, il existe quelques cas où une interface fluide peut être une bonne solution, ils ont en commun.

  • Les constantes sont surtout transmises aux setters
  • La logique du programme ne change pas ce qui est transmis aux setters.

Configuration de la configuration, par exemple fluent-nhibernate

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Configuration des données de test dans les tests unitaires, à l'aide de TestDataBulderClasses (Object Mothers) spécial

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Cependant, créer une bonne interface fluide est très difficile , donc cela ne vaut que si vous avez beaucoup de configurations «statiques». De plus, l'interface fluide ne doit pas être mélangée avec des classes «normales»; par conséquent, le modèle de construction est souvent utilisé.


5

Je pense qu'une grande partie de la raison pour laquelle ce n'est pas une convention pour enchaîner un setter après un autre c'est parce que dans ces cas, il est plus typique de voir un objet options ou des paramètres dans un constructeur. C # a également une syntaxe d'initialiseur.

Au lieu de:

DTO dto = new DTO().setFoo("foo").setBar("bar");

On pourrait écrire:

(en JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(en C #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(en Java)

DTO dto = new DTO("foo", "bar");

setFooet ne setBarsont alors plus nécessaires pour l’initialisation et peuvent être utilisés pour une mutation ultérieure.

Bien que la chaînage soit utile dans certaines circonstances, il est important de ne pas essayer de tout mettre sur une seule ligne uniquement pour réduire les caractères de nouvelle ligne.

Par exemple

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

rend plus difficile la lecture et la compréhension de ce qui se passe. Reformatage à:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

Est beaucoup plus facile à comprendre, et rend "l'erreur" dans la première version plus évidente. Une fois que vous avez restructuré le code dans ce format, vous n’avez plus aucun avantage réel à:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1. Je ne peux vraiment pas accepter le problème de la lisibilité, ajouter une instance avant chaque appel ne le rend pas plus clair du tout. 2. Les modèles d’initialisation que vous avez montrés avec js et C # n’ont rien à voir avec les constructeurs: ce que vous avez fait dans js est transmis à un seul argument, et ce que vous avez fait en C # est une syntaxe shugar qui appelle les geters et les setters en coulisse, et java n'a pas le shugar geter-setter comme C #.
Ben

1
@Benedictus, je montrais différentes manières pour les langages POO de gérer ce problème sans chaînage. Le but n'était pas de fournir un code identique, mais de montrer des alternatives rendant inutile l'enchaînement.
ZzzzBov

2
"l'ajout d'instance avant chaque appel ne le rend pas plus clair du tout" Je n'ai jamais prétendu que l'ajout d'instance avant chaque appel ait apporté quelque chose de plus clair, j'ai juste dit qu'il était relativement équivalent.
ZzzzBov

1
VB.NET a aussi un mot-clé utilisable «Avec» qui crée une référence temporaire au compilateur, de sorte que, par exemple, [using /to représente un saut de ligne] With Foo(1234) / .x = 23 / .y = 47serait équivalent à Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Une telle syntaxe ne crée pas d'ambiguïté car .xelle ne peut par elle-même signifier que de se lier à l'instruction "With" qui entoure immédiatement [s'il n'y en a pas, ou si l'objet n'a pas de membre x, alors .xn'a pas de sens]. Oracle n'a rien inclus de la sorte en Java, mais une telle construction s'intégrerait parfaitement dans le langage.
Supercat

5

Cette technique est en fait utilisée dans le motif Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

Cependant, en général, il est évité car il est ambigu. Il n'est pas évident de savoir si la valeur de retour est l'objet (vous pouvez donc appeler d'autres paramètres), ou si l'objet de retour est la valeur qui vient d'être affectée (également un modèle commun). En conséquence, le principe de la moindre surprise suggère de ne pas essayer de supposer que l'utilisateur souhaite voir l'une ou l'autre solution, à moins que cela ne soit fondamental pour la conception de l'objet.


6
La différence est que, lorsque vous utilisez le modèle de générateur, vous écrivez généralement un objet en écriture seule (le générateur) qui construit éventuellement un objet en lecture seule (immuable) (quelle que soit la classe que vous construisez). À cet égard, il est souhaitable d’ avoir une longue chaîne d’appels de méthode, car elle peut être utilisée comme une expression unique.
Darkhogg

"ou si l'objet de retour est la valeur qui vient d'être assignée" Je déteste ça, si je veux conserver la valeur que je viens de transmettre, je le mettrai d'abord dans une variable. Cependant, il peut être utile d’obtenir la valeur précédemment attribuée (certaines interfaces de conteneur le font, je crois).
JAB

@JAB Je suis d'accord, je ne suis pas un fan de cette notation, mais elle a sa place. Ce qui Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); me vient à l’esprit, c’est que je pense qu’il a également gagné en popularité parce que cela reflète a = b = c = dbien, même si je ne suis pas convaincu que la popularité était bien fondée. J'ai vu celui que vous avez mentionné, renvoyant la valeur précédente, dans certaines bibliothèques d'opérations atomiques. Morale de l'histoire? C'est encore plus déroutant que je ne le pensais =)
Cort Ammon

Je crois que les universitaires deviennent un peu pédants: il n'y a rien de mal en soi à l'idiome. C'est extrêmement utile pour ajouter de la clarté dans certains cas. Prenez la classe de mise en forme de texte suivante, par exemple, qui indente chaque ligne d'une chaîne et trace un cadre ascii-art autour de celle-ci new BetterText(string).indent(4).box().print();. Dans ce cas, j'ai contourné un tas de gobbledygook pour prendre une chaîne, la mettre en retrait, la mettre en boîte et la sortir. Vous pouvez même souhaiter une méthode de duplication au sein de la cascade (telle que, par exemple .copy()) pour permettre à toutes les actions suivantes de ne plus modifier l'original.
tgm1024

2

C'est plus un commentaire qu'une réponse, mais je ne peux pas en parler, alors ...

je voulais juste mentionner que cette question m'a surpris parce que je ne considère pas cela comme inhabituel du tout. En fait, dans mon environnement de travail (développeur web) est très commun.

Par exemple, voici comment la commande doctrine: generate: entity de Symfony génère tout automatiquement setters, par défaut .

jQuery enchaîne un peu la plupart de ses méthodes de manière très similaire.


Js est une abomination
Ben

@ Benedictus Je dirais que PHP est l'abomination la plus grande. JavaScript est un langage parfaitement fin et est devenu assez agréable avec l'inclusion de fonctionnalités ES6 (bien que je sois sûr que certaines personnes préfèrent toujours CoffeeScript ou ses variantes; personnellement, je ne suis pas fan de la façon dont CoffeeScript gère la portée des variables lors de la vérification de l'étendue externe d’abord plutôt que de traiter les variables affectées dans la portée locale comme locales, sauf indication explicite comme non locale / globale comme le fait Python).
JAB

@ Benedictus, je suis d'accord avec JAB. Au cours de mes années de collège , je l' habitude de regarder vers le bas à JS comme une abomination wanna-be langage de script, mais après avoir travaillé quelques années avec elle, et l' apprentissage de la droite façon de l' utiliser, je suis venu ... en fait aimer ce . Et je pense qu'avec ES6, cela va devenir un langage vraiment mature . Mais ce qui me fait tourner la tête, c’est… qu’est-ce que ma réponse a à voir avec JS en premier lieu? ^^ U
xDaizu

En fait, j'ai travaillé quelques années avec JS et je peux dire que j'ai appris comment s'y prendre. Néanmoins, je ne vois plus ce langage comme une erreur. Mais je suis d'accord, Php est bien pire.
Ben

@Benedictus Je comprends votre position et je la respecte. Je l'aime encore malgré çà. Bien sûr, il a ses bizarreries et ses avantages (quelle langue n’a pas?), Mais il avance régulièrement dans la bonne direction. Mais ... je n'aime pas tellement ça que j'ai besoin de le défendre. Je ne gagne rien des gens qui l'aiment ou le détestent ... hahaha En outre, ce n'est pas un endroit pour ça, ma réponse n'était même pas à propos de JS.
xDaizu
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.