web-dev-qa-db-fra.com

Pourquoi l'enchaînement des colons n'est-il pas conventionnel?

Avoir le chaînage implémenté sur les beans est très pratique: 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, sauf si vous voulez que votre objet soit immuable, auquel cas il n'aurait de toute façon aucun paramètre. Y a-t-il une raison pour laquelle ce n'est pas une convention OOP?

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");
46
Ben

Y a-t-il une raison pour laquelle ce n'est pas une convention OOP?

Ma meilleure supposition: car il viole CQS

Vous avez une commande (changer l'état de l'objet) et une requête (renvoyer 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 viole 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 aimeriez combiner les deux, mais vous ne pouvez pas faire cela et pour être protégé contre les exceptions. (Pas de problème en Java, car l'opérateur d'affectation en Java ne lance 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 lorsque vous traitez avec l'héritage. Voir le "Modèle de modèle curieusement récurrent"

Enfin, il y a le problème que le constructeur par défaut devrait vous laisser avec un objet qui est dans un état valide. Si vous devez exécuter un tas de commandes pour restaurer l'objet à un état valide, quelque chose s'est mal passé.

50
VoiceOfUnreason
  1. L'enregistrement de quelques frappes n'est pas convaincant. C'est peut-être bien, mais OOP se soucient plus des concepts et des structures, pas des frappes.

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

  3. Encore plus qu'être sans signification, la valeur de retour est trompeuse, puisque les utilisateurs peuvent s'attendre à ce que la valeur de retour ait un sens. Ils peuvent s'attendre à ce que ce soit 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.

33
Paul Draper

Je ne pense pas que ce soit une convention OOP, elle est plus liée à la conception du langage et à ses conventions.

Il semble que vous aimiez utiliser Java. Java a une spécification JavaBeans qui spécifie le type de retour du setter à annuler, c'est-à-dire qu'il est en conflit avec le chaînage 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 ne pas enchaîner une partie de la spécification. Je ne connais pas la réponse, peut-être que ce modèle n'était tout simplement pas connu/populaire à l'époque.

12
qbd

Comme d'autres l'ont dit, cela est souvent appelé interface fluide .

Normalement, les passeurs appellent en passant variables en réponse au code logique dans une application; votre classe DTO en est un exemple. Le code conventionnel lorsque les utilisateurs ne renvoient rien est normal pour cela. D'autres réponses ont expliqué le chemin.

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

  • Les constantes sont pour la plupart transmises aux colons
  • La logique du programme ne change pas ce qui est transmis aux setters.

Mise en place 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, en utilisant spécial TestDataBulderClasses (Object Mothers)

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 la peine que lorsque vous avez beaucoup de configuration "statique". Une interface fluide ne doit pas non plus être mélangée à des classes "normales"; par conséquent, le modèle de générateur est souvent utilisé.

7
Ian

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

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");

setFoo et setBar ne sont alors plus nécessaires pour l'initialisation et peuvent être utilisés pour la mutation ultérieurement.

Bien que l'enchaînement soit utile dans certaines circonstances, il est important de ne pas essayer de tout mettre sur une seule ligne juste pour réduire les caractères de nouvelle ligne.

Par exemple

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

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

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

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

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

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

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

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

5
Cort Ammon

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

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

Par exemple, c'est ainsi que la doctrine de Symfony: Génère: la commande entités génère automatiquement toussetters, par défaut.

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

2
xDaizu