Warum ist es unkonventionell, Setter zu verketten?

46

Die Implementierung der Verkettung in Beans ist sehr praktisch: Konstruktoren, Megakonstruktoren und Fabriken müssen nicht überladen werden, und die Lesbarkeit wird erhöht. Ich kann mir keine Nachteile vorstellen, es sei denn, Sie möchten, dass Ihr Objekt unveränderlich ist. In diesem Fall würde es sowieso keine Setter haben. Gibt es einen Grund, warum dies keine OOP-Konvention ist?

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");
Ben
quelle
32
Weil Java möglicherweise die einzige Sprache ist, in der Setter keine Greuel für den Menschen sind ...
Telastyn
11
Es ist ein schlechter Stil, weil der Rückgabewert semantisch nicht aussagekräftig ist. Es ist irreführend. Der einzige Vorteil ist, dass nur wenige Tastenanschläge eingespart werden müssen.
usr
58
Dieses Muster ist überhaupt nicht ungewöhnlich. Es gibt sogar einen Namen dafür. Es heißt flüssige Schnittstelle .
Philipp
9
Sie können diese Erstellung auch in einem Builder abstrahieren, um ein besser lesbares Ergebnis zu erzielen. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Ich würde das tun, um nicht der allgemeinen Vorstellung zu widersprechen, dass Setter leere Stellen sind.
9
@Philipp, obwohl du technisch gesehen recht hast, würde ich nicht sagen, dass new Foo().setBar('bar').setBaz('baz')sich das sehr "flüssig" anfühlt. Ich meine, sicher, es könnte genauso implementiert werden, aber ich würde sehr erwarten, dass ich etwas Ähnliches wieFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Antworten:

50

Gibt es einen Grund, warum dies keine OOP-Konvention ist?

Meine beste Vermutung: weil es CQS verletzt

Sie haben einen Befehl (Ändern des Status des Objekts) und eine Abfrage (Zurückgeben einer Kopie des Status - in diesem Fall des Objekts selbst) in derselben Methode gemischt. Das ist nicht unbedingt ein Problem, verstößt aber gegen einige der grundlegenden Richtlinien.

In C ++ ist std :: stack :: pop () beispielsweise ein Befehl, der void zurückgibt, und std :: stack :: top () ist eine Abfrage, die einen Verweis auf das oberste Element im Stapel zurückgibt. Klassischerweise möchten Sie beide kombinieren, aber Sie können das nicht und sind ausnahmesicher. (Kein Problem in Java, da der Zuweisungsoperator in Java nicht auslöst).

Wenn DTO ein Wertetyp wäre, könnten Sie mit ein ähnliches Ziel erreichen

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

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

Außerdem ist die Verkettung von Rückgabewerten ein gewaltiger Schmerz, wenn Sie sich mit Vererbung befassen. Siehe "Seltsamerweise wiederkehrendes Vorlagenmuster"

Schließlich gibt es das Problem, dass der Standardkonstruktor Sie mit einem Objekt belassen sollte, das sich in einem gültigen Zustand befindet. Wenn Sie müssen eine Reihe von Befehlen führen Sie das Objekt in einen gültigen Zustand wieder herzustellen, hat etwas sehr falsch gegangen.

VoiceOfUnreason
quelle
4
Endlich ein echter Hingucker. Vererbung ist das eigentliche Problem. Ich denke, Verkettung könnte ein konventioneller Setter für Datenrepräsentationsobjekte sein, die in der Komposition verwendet werden.
Ben
6
"eine Kopie des Zustands von einem Objekt zurückgeben" - das macht es nicht.
user253751
2
Ich würde argumentieren, dass der Hauptgrund für das Befolgen dieser Richtlinien (mit einigen bemerkenswerten Ausnahmen wie Iteratoren) darin besteht, dass es den Code erheblich einfacher macht, darüber nachzudenken .
jpmc26
1
@MatthieuM. Nee. Ich spreche hauptsächlich Java und es ist dort in fortgeschrittenen "flüssigen" APIs weit verbreitet - ich bin sicher, es gibt es auch in anderen Sprachen. Im Wesentlichen definieren Sie Ihren Typ als generisch für einen Typ, der sich selbst erweitert. Anschließend fügen Sie eine abstract T getSelf()Methode hinzu, die den generischen Typ zurückgibt. Anstatt thisvon Ihren Setzern zurückzukehren, machen Sie return getSelf()und dann jede übergeordnete Klasse den Typ einfach generisch und kehren thisvon zurück getSelf. Auf diese Weise geben die Setter den tatsächlichen Typ und nicht den deklarierenden Typ zurück.
Boris die Spinne
1
@MatthieuM. Ja wirklich? Sounds ähnlich wie CRTP für C ++ ...
Useless
33
  1. Das Speichern einiger Tastenanschläge ist nicht zwingend. Es mag nett sein, aber bei OOP-Konventionen geht es mehr um Konzepte und Strukturen als um Tastenanschläge.

  2. Der Rückgabewert ist bedeutungslos.

  3. Der Rückgabewert ist nicht nur bedeutungslos, sondern auch irreführend, da Benutzer möglicherweise erwarten, dass der Rückgabewert eine Bedeutung hat. Sie können erwarten, dass es ein "unveränderlicher Setter" ist

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

    In Wirklichkeit mutiert Ihr Setter das Objekt.

  4. Es funktioniert nicht gut mit Vererbung.

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

    ich kann schreiben

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

    aber nicht

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

Für mich sind # 1 bis # 3 die wichtigsten. Bei gut geschriebenem Code geht es nicht um angenehm angeordneten Text. In gut geschriebenem Code geht es um die grundlegenden Konzepte, Beziehungen und Strukturen. Text spiegelt nur die wahre Bedeutung des Codes nach außen wider.

Paul Draper
quelle
2
Die meisten dieser Argumente gelten jedoch für jede flüssige / verkettbare Schnittstelle.
Casey
@Casey, ja. Bauherren (dh mit Setzern) ist der häufigste Fall, den ich beim Verketten sehe.
Paul Draper
10
Wenn Sie mit der Idee einer fließenden Schnittstelle vertraut sind, trifft Nummer 3 nicht zu, da Sie wahrscheinlich eine fließende Schnittstelle vom Rückgabetyp vermuten. Ich muss auch widersprechen "Gut geschriebener Code handelt nicht von angenehm angeordnetem Text". Das ist nicht alles, worum es geht, aber schön angeordneter Text ist ziemlich wichtig, da er dem menschlichen Leser des Programms ermöglicht, ihn mit weniger Aufwand zu verstehen.
Michael Shaw
1
Bei der Setter-Verkettung geht es nicht darum, den Text angenehm anzuordnen oder Tastatureingaben zu speichern. Es geht darum, die Anzahl der Bezeichner zu verringern. Mit der Setter-Verkettung können Sie das Objekt in einem einzelnen Ausdruck erstellen und einrichten. Dies bedeutet, dass Sie es möglicherweise nicht in einer Variablen speichern müssen - eine Variable, die Sie benennen müssen , und die dort bis zum Ende des Gültigkeitsbereichs verbleibt.
Idan Arye
3
In Bezug auf Punkt 4 kann dies in Java folgendermaßen gelöst werden: public class Foo<T extends Foo> {...}Wenn der Setter zurückkehrt Foo<T>. Auch diese "Setter" -Methoden, ich ziehe es vor, sie "mit" -Methoden aufzurufen. Wenn Überladung gut funktioniert, dann eben Foo<T> with(Bar b) {...}, sonst Foo<T> withBar(Bar b).
YoYo
12

Ich denke nicht, dass dies eine OOP-Konvention ist, sie hängt mehr mit dem Sprachdesign und seinen Konventionen zusammen.

Anscheinend möchten Sie Java verwenden. Java hat eine JavaBeans- Spezifikation, die den Rückgabetyp des Setters angibt, der ungültig sein soll, dh er steht in Konflikt mit der Verkettung von Settern. Diese Spezifikation wird allgemein akzeptiert und in einer Vielzahl von Tools implementiert.

Natürlich könnte man fragen, warum nicht ein Teil der Spezifikation verkettet wird. Ich kenne die Antwort nicht, vielleicht war dieses Muster zu dieser Zeit einfach nicht bekannt / beliebt.

qbd
quelle
Ja, JavaBeans ist genau das, was ich mir vorgestellt habe.
Ben
Ich glaube nicht, dass die meisten Anwendungen, die JavaBeans verwenden, sich darum kümmern, aber es könnte sein, dass sie (mithilfe von Reflection die Methoden mit dem Namen 'set ...' abrufen, einen einzelnen Parameter haben und die Rückgabe ungültig ist ). Viele statische Analyseprogramme bemängeln, dass ein Rückgabewert einer Methode, die etwas zurückgibt, nicht überprüft wird, was sehr ärgerlich sein kann.
Reflektierende @ MichaelT-Aufrufe zum Abrufen von Methoden in Java geben keinen Rückgabetyp an. Wenn Sie auf diese Weise eine Methode aufrufen Object, wird möglicherweise das Singleton des Typs Voidzurückgegeben, wenn die Methode voidden Rückgabetyp angibt. In anderen Nachrichten ist das "Hinterlassen eines Kommentars, aber nicht das Abstimmen" anscheinend ein Grund dafür, dass eine Prüfung nach dem ersten Post nicht bestanden wird, auch wenn es sich um einen guten Kommentar handelt. Ich beschuldige Shog dafür.
7

Wie andere Leute gesagt haben, wird dies oft als flüssige Schnittstelle bezeichnet .

Normalerweise sind Setter Call-Passing- Variablen als Antwort auf den Logikcode in einer Anwendung. Ihre DTO-Klasse ist ein Beispiel dafür. Herkömmlicher Code, wenn Setter nichts zurückgeben, ist hierfür am besten geeignet. Andere Antworten haben übrigens erklärt.

Es gibt jedoch einige Fälle, in denen eine flüssige Benutzeroberfläche eine gute Lösung sein kann, die alle gemeinsam haben.

  • Konstanten werden meist an die Setter übergeben
  • Die Programmlogik ändert nicht, was an die Setter übergeben wird.

Einrichten der Konfiguration, z. B. 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");

Einrichten von Testdaten in Komponententests mit speziellen 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

Das Erstellen einer guten, fließenden Benutzeroberfläche ist jedoch sehr schwierig . Daher lohnt es sich nur, wenn Sie über viele „statische“ Einstellungen verfügen. Auch fließende Benutzeroberflächen sollten nicht mit „normalen“ Klassen gemischt werden. Daher wird häufig das Builder-Muster verwendet.

Ian
quelle
5

Ich denke, der Hauptgrund, warum es keine Konvention ist, einen Setter nach dem anderen zu verketten, liegt darin, dass in diesen Fällen ein Optionsobjekt oder Parameter in einem Konstruktor typischer ist. C # hat auch eine Initialisierungssyntax.

Anstatt von:

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

Man könnte schreiben:

(in JS)

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

(in C #)

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

(in Java)

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

setFoound setBarwerden dann nicht mehr zur Initialisierung benötigt und können später zur Mutation verwendet werden.

Die Verkettbarkeit ist unter bestimmten Umständen hilfreich, es ist jedoch wichtig, nicht zu versuchen, alles in eine einzelne Zeile zu packen, nur um die Anzahl der Zeilenumbrüche zu verringern.

Zum Beispiel

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

erschwert das Lesen und Verstehen des Geschehens. Neuformatierung zu:

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

Ist viel einfacher zu verstehen und macht den "Fehler" in der ersten Version offensichtlicher. Sobald Sie den Code in dieses Format überarbeitet haben, gibt es keinen wirklichen Vorteil mehr gegenüber:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
zzzzBov
quelle
3
1. Ich kann dem Lesbarkeitsproblem wirklich nicht zustimmen, da das Hinzufügen einer Instanz vor jedem Aufruf die Übersichtlichkeit nicht verbessert. 2. Die Initialisierungsmuster, die Sie mit js und C # gezeigt haben, haben nichts mit Konstruktoren zu tun: Was Sie in js getan haben, wird als einzelnes Argument übergeben, und was Sie in C # getan haben, ist eine Syntax-Shugar, die Geter und Setter hinter den Kulissen und Java aufruft hat nicht den Geter-Setter-Shugar wie C #.
Ben
1
@Benedictus, ich habe verschiedene Möglichkeiten aufgezeigt, wie OOP-Sprachen dieses Problem behandeln, ohne es zu verketten. Es ging nicht darum, identischen Code bereitzustellen, sondern Alternativen aufzuzeigen, die das Verketten unnötig machen.
zzzzBov
2
"Das Hinzufügen einer Instanz vor jedem Aufruf macht es überhaupt nicht klarer." Ich habe nie behauptet, dass das Hinzufügen einer Instanz vor jedem Aufruf etwas klarer macht. Ich habe nur gesagt, dass es relativ äquivalent ist.
zzzzBov
1
VB.NET hat auch eine brauchbaren „Mit“ Schlüsselwort , das eine Compiler-temporärer Referenz erzeugt , so dass zB [mit /zu Zeilenumbruch darstellt] With Foo(1234) / .x = 23 / .y = 47wäre gleichbedeutend mit Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Eine solche Syntax Erstellen von nicht Mehrdeutigkeit da .xvon selbst umgibt „Mit“ Aussage keine andere Bedeutung als zu binden an die sofort haben können [wenn es keinen gibt, oder das Objekt dort kein Mitglied hat x, dann .xist bedeutungslos]. Oracle hat so etwas nicht in Java aufgenommen, aber ein solches Konstrukt würde problemlos in die Sprache passen.
Supercat
5

Diese Technik wird tatsächlich im Builder-Muster verwendet.

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

Im Allgemeinen wird dies jedoch vermieden, da es nicht eindeutig ist. Es ist nicht offensichtlich, ob der Rückgabewert das Objekt ist (so dass Sie andere Setter aufrufen können) oder ob das Rückgabeobjekt der Wert ist, der gerade zugewiesen wurde (auch ein gemeinsames Muster). Dementsprechend schlägt das Prinzip der geringsten Überraschung vor, dass Sie nicht versuchen sollten, anzunehmen, dass der Benutzer die eine oder andere Lösung sehen möchte, es sei denn, dies ist grundlegend für das Design des Objekts.

Cort Ammon
quelle
6
Der Unterschied besteht darin, dass Sie bei Verwendung des Builder-Musters normalerweise ein schreibgeschütztes Objekt (den Builder) schreiben, das schließlich ein schreibgeschütztes (unveränderliches) Objekt erstellt (unabhängig von der Klasse, die Sie erstellen). In dieser Hinsicht ist eine lange Kette von Methodenaufrufen wünschenswert, da sie als einzelner Ausdruck verwendet werden kann.
Darkhogg
"oder wenn das Rückgabeobjekt der Wert ist, der gerade zugewiesen wurde" Ich hasse es, wenn ich den Wert, den ich gerade übergeben habe, beibehalten möchte, setze ich ihn zuerst in eine Variable. Es kann jedoch nützlich sein, den zuvor zugewiesenen Wert abzurufen (einige Containerschnittstellen tun dies, glaube ich).
JAB
@JAB Ich stimme zu, ich bin kein großer Fan dieser Notation, aber sie hat ihren Platz. Das, was Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); mir einfällt, ist, dass es meiner Meinung nach auch an Popularität gewonnen hat, weil es sich spiegelt a = b = c = d, obwohl ich nicht überzeugt bin, dass Popularität begründet war. Ich habe in einigen atomaren Operationsbibliotheken die von Ihnen erwähnte gesehen, die den vorherigen Wert zurückgibt. Moral der Geschichte? Es ist noch verwirrender, als ich es klingen ließ =)
Cort Ammon
Ich glaube, dass Akademiker ein wenig pedantisch werden: Es ist nichts an der Redewendung falsch. In bestimmten Fällen ist dies äußerst hilfreich, um Klarheit zu schaffen. Nehmen Sie zum Beispiel die folgende Textformatierungsklasse, die jede Zeile eines Strings einrückt und ein ASCII-Kunst-Kästchen um sie herum zeichnet new BetterText(string).indent(4).box().print();. In diesem Fall habe ich einen Haufen Gobbledygook umgangen und eine Zeichenfolge genommen, eingerückt, verpackt und ausgegeben. Möglicherweise möchten Sie sogar eine Duplizierungsmethode innerhalb der Kaskade (z. B. .copy()), damit alle folgenden Aktionen das Original nicht mehr ändern.
tgm1024
2

Dies ist eher ein Kommentar als eine Antwort, aber ich kann keinen Kommentar abgeben, also ...

Ich wollte nur erwähnen, dass mich diese Frage überrascht hat, weil ich das überhaupt nicht als ungewöhnlich betrachte . Eigentlich ist in meinem Arbeitsumfeld (Webentwickler) sehr sehr verbreitet.

Dies ist zum Beispiel die Doktrin von Symfony: generate: entity-Befehl , der standardmäßig alle automatisch generiert .setters

jQuery verkettet die meisten seiner Methoden auf ähnliche Weise.

xDaizu
quelle
Js ist ein Gräuel
Ben
@ Benedictus Ich würde sagen, PHP ist der größere Greuel. JavaScript ist eine sehr gute Sprache und wurde durch die Integration von ES6-Funktionen sehr ansprechend (obwohl ich sicher bin, dass einige Leute noch immer CoffeeScript oder Varianten bevorzugen) erstens, anstatt Variablen, die im lokalen Bereich zugewiesen sind, als lokal zu behandeln, es sei denn, dies wird ausdrücklich als nicht lokal / global angegeben (wie es Python tut).
JAB
@ Benedictus stimme ich zu JAB. In meiner Studienzeit habe ich bei JS nach unten zu schauen als ein Gräuel Möchtegern-Skriptsprache, aber nach ein paar Jahren mit ihm zu arbeiten, und das Erlernen der RECHTS Weg , es zu verwenden, habe ich komme zu ... wirklich lieben es . Und ich denke, dass es mit ES6 eine wirklich ausgereifte Sprache wird. Aber was mich dazu bringt, den Kopf zu drehen, ist ... was hat meine Antwort überhaupt mit JS zu tun? ^^ U
xDaizu
Ich habe tatsächlich ein paar Jahre mit js gearbeitet und kann sagen, dass ich gelernt habe, wie es geht. Trotzdem sehe ich in dieser Sprache nichts weiter als einen Fehler. Aber ich stimme zu, Php ist viel schlimmer.
Ben
@ Benedictus Ich verstehe Ihre Position und respektiere sie. Ich mag es aber immer noch. Sicher, es hat seine Macken und Vorteile (welche Sprache nicht?), Aber es geht stetig in die richtige Richtung. Aber ... ich mag es eigentlich nicht so sehr, dass ich es verteidigen muss. Ich verdiene
xDaizu