Dlaczego setery łańcuchowe są niekonwencjonalne?

46

Zaimplementowanie tworzenia łańcuchów na fasoli jest bardzo przydatne: nie trzeba przeciążać konstruktorów, mega konstruktorów, fabryk i zapewnia większą czytelność. Nie mogę wymyślić żadnych wad, chyba że chcesz, aby twój obiekt był niezmienny , w którym to przypadku i tak nie miałby żadnych ustawień. Czy jest więc powód, dla którego nie jest to konwencja 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");
Ben
źródło
32
Ponieważ Java może być jedynym językiem, w którym setery nie są obrzydliwością dla człowieka ...
Telastyn
11
Jest to zły styl, ponieważ zwracana wartość nie ma znaczenia semantycznego. To wprowadza w błąd. Jedyną zaletą jest oszczędność bardzo niewielu naciśnięć klawiszy.
usr
58
Ten wzór wcale nie jest rzadki. Jest nawet na to nazwa. Nazywa się to płynnym interfejsem .
Philipp
9
Możesz także wyodrębnić to stworzenie w kreatorze, aby uzyskać bardziej czytelny wynik. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Zrobiłbym to, aby nie kolidować z ogólną ideą, że setery są puste.
9
@Filipp, choć technicznie masz rację, nie powiedziałbym, że new Foo().setBar('bar').setBaz('baz')wydaje się to bardzo „płynne”. Mam na myśli, że można go zaimplementować dokładnie w ten sam sposób, ale bardzo bym się spodziewał, że przeczytam coś więcejFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Odpowiedzi:

50

Czy istnieje więc powód, dla którego nie jest to konwencja OOP?

Moje najlepsze przypuszczenie: ponieważ narusza CQS

Masz polecenie (zmieniające stan obiektu) i zapytanie (zwracające kopię stanu - w tym przypadku samego obiektu) zmieszane w tę samą metodę. To niekoniecznie problem, ale narusza niektóre podstawowe wytyczne.

Na przykład w C ++ std :: stack :: pop () jest poleceniem, które zwraca void, a std :: stack :: top () jest zapytaniem, które zwraca odwołanie do górnego elementu na stosie. Klasycznie chciałbyś połączyć oba, ale nie możesz tego zrobić i być wyjątkowo bezpiecznym. (Nie jest to problem w Javie, ponieważ operator przypisania w Javie nie rzuca).

Gdyby DTO był typem wartości, możesz osiągnąć podobny koniec

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

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

Również łańcuchowe wartości zwrotne są kolosalnym bólem w czasie, gdy mamy do czynienia z dziedziczeniem. Zobacz „Ciekawie powtarzający się wzór szablonu”

Wreszcie jest problem, że domyślny konstruktor powinien pozostawić obiekt w prawidłowym stanie. Jeśli musisz uruchomić kilka poleceń, aby przywrócić prawidłowy stan obiektu, coś poszło bardzo źle.

VoiceOfUnreason
źródło
4
Wreszcie prawdziwy catain tutaj. Dziedziczenie to prawdziwy problem. Myślę, że tworzenie łańcuchów może być konwencjonalnym ustawieniem dla obiektów reprezentujących dane, które są używane w kompozycji.
Ben
6
„zwracanie kopii stanu z obiektu” - nie robi tego.
user253751
2
Twierdziłbym, że największym powodem przestrzegania tych wytycznych (z pewnymi istotnymi wyjątkami, takimi jak iteratory) jest to, że znacznie ułatwia to rozumowanie kodu .
jpmc26
1
@MatthieuM. nie. Mówię głównie w Javie, i jest tam powszechne w zaawansowanych „płynnych” API - jestem pewien, że istnieje również w innych językach. Zasadniczo czynisz swój typ typowym dla typu rozszerzającego się. Następnie dodajesz abstract T getSelf()metodę ze zwrotem typu ogólnego. Teraz, zamiast wracać thisz twoich seterów, ty, return getSelf()a następnie każda nadrzędna klasa po prostu sprawia, że ​​typ jest sam w sobie ogólny i wraca thisz getSelf. W ten sposób ustawiający zwracają typ rzeczywisty, a nie typ deklarujący.
Boris the Spider
1
@MatthieuM. naprawdę? Brzmi podobnie do CRTP dla C ++ ...
Bezużyteczne
33
  1. Zapisanie kilku naciśnięć klawiszy nie jest atrakcyjne. Może to być miłe, ale konwencje OOP bardziej troszczą się o koncepcje i struktury, a nie klawisze.

  2. Zwracana wartość nie ma znaczenia.

  3. Co więcej, nie ma znaczenia, wartość zwracana jest myląca, ponieważ użytkownicy mogą oczekiwać, że wartość zwrotna będzie miała znaczenie. Mogą oczekiwać, że jest to „niezmienny seter”

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

    W rzeczywistości Seter mutuje obiekt.

  4. Nie działa dobrze z dziedziczeniem.

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

    Umiem pisać

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

    ale nie

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

Dla mnie najważniejsze są numery od 1 do 3. Dobrze napisany kod nie dotyczy przyjemnie ułożonego tekstu. Dobrze napisany kod dotyczy podstawowych pojęć, relacji i struktury. Tekst jest jedynie zewnętrznym odzwierciedleniem prawdziwego znaczenia kodu.

Paul Draper
źródło
2
Większość tych argumentów dotyczy jednak dowolnego płynnego / łańcuchowego interfejsu.
Casey
@Casey, tak. Konstruktorzy (tzn. Z seterami) to najczęstszy przypadek łączenia.
Paul Draper,
10
Jeśli znasz pomysł płynnego interfejsu, nr 3 nie ma zastosowania, ponieważ możesz podejrzewać płynny interfejs na podstawie typu zwrotu. Muszę też się nie zgodzić z „Dobrze napisany kod nie dotyczy przyjemnie ułożonego tekstu”. Nie o to chodzi, ale ładnie ułożony tekst jest raczej ważny, ponieważ pozwala czytelnikowi programu zrozumieć go przy mniejszym wysiłku.
Michael Shaw
1
Łańcuch seterów nie polega na przyjemnym układaniu tekstu ani na oszczędzaniu naciśnięć klawiszy. Chodzi o zmniejszenie liczby identyfikatorów. Za pomocą łańcucha seterów możesz utworzyć i ustawić obiekt w jednym wyrażeniu, co oznacza, że ​​nie będziesz musiał zapisywać go w zmiennej - zmiennej, którą będziesz musiał nazwać , i która pozostanie tam do końca zakresu.
Idan Arye
3
Jeśli chodzi o punkt # 4, jest to coś, co można rozwiązać w Javie w następujący sposób: public class Foo<T extends Foo> {...}po powrocie setera Foo<T>. Także te metody „ustawiające”, wolę nazywać je „metodami”. Jeśli przeciążenie działa dobrze, to po prostu Foo<T> with(Bar b) {...}w przeciwnym razie Foo<T> withBar(Bar b).
YoYo
12

Nie sądzę, że jest to konwencja OOP, jest bardziej związana z projektowaniem języka i jego konwencjami.

Wygląda na to, że lubisz używać Java. Java ma specyfikację JavaBeans, która określa typ zwracany przez setter, który ma być nieważny, tzn. Jest w konflikcie z łańcuchem seterów. Ta specyfikacja jest powszechnie akceptowana i wdrażana w różnych narzędziach.

Oczywiście możesz zapytać, dlaczego nie jest częścią łańcucha specyfikacji. Nie znam odpowiedzi, może ten wzór nie był wtedy znany / popularny.

qbd
źródło
Tak, JavaBeans jest dokładnie tym, co miałem na myśli.
Ben
Nie sądzę, że większość aplikacji korzystających z JavaBeans dba o to, ale mogą one (używając refleksji, aby pobrać metody o nazwie „set ...”, mają jeden parametr i są nieważne ). Wiele programów do analizy statycznej narzeka na to, że nie sprawdza wartości zwracanej przez metodę, która zwraca coś, co może być bardzo denerwujące.
Wywołania odblaskowe @MichaelT w celu uzyskania metod w Javie nie określają typu zwrotu. Wywołanie metody w ten sposób zwraca Object, co może spowodować zwrócenie singletonu typu Void, jeśli metoda określa voidjako typ zwracany. W innych wiadomościach najwyraźniej „pozostawienie komentarza, ale nie głosowanie” jest podstawą do niepowodzenia audytu „pierwszego postu”, nawet jeśli jest to dobry komentarz. Winię za to shoga.
7

Jak powiedzieli inni ludzie, jest to często nazywane płynnym interfejsem .

Zwykle ustawiacze są zmiennymi przekazującymi wywołania w odpowiedzi na kod logiczny w aplikacji; twoja klasa DTO jest tego przykładem. Konwencjonalny kod, gdy setery nie zwracają niczego, jest w tym przypadku normalny. Inne odpowiedzi wyjaśniły sposób.

Jednak istnieje kilka przypadków, w których płynny interfejs może być dobrym rozwiązaniem, mają one wspólne.

  • Stałe są w większości przekazywane do seterów
  • Logika programu nie zmienia tego, co przekazywane jest ustawiającym.

Konfigurowanie konfiguracji, na przykład płynnie-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");

Konfigurowanie danych testowych w testach jednostkowych przy użyciu specjalnych TestDataBulderClasses (Mother Object)

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

Jednak stworzenie dobrego płynnego interfejsu jest bardzo trudne , więc warto go tylko wtedy, gdy masz wiele „statycznych” ustawień. Również płynnego interfejsu nie należy mieszać z „normalnymi” klasami; stąd często stosuje się wzorzec konstruktora.

Łan
źródło
5

Myślę, że wiele powodów, dla których nie jest konwencją łączenia setera za drugim, jest takie, że w takich przypadkach bardziej typowe jest widzenie obiektu opcji lub parametrów w konstruktorze. C # ma również składnię inicjalizacyjną.

Zamiast:

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

Można napisać:

(w JS)

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

(w C #)

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

(w Javie)

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

setFooi setBarwtedy nie są już potrzebne do inicjalizacji i mogą być później wykorzystane do mutacji.

Chociaż w niektórych przypadkach przydatność łańcuchowa jest przydatna, ważne jest, aby nie próbować umieszczać wszystkiego w jednym wierszu tylko ze względu na redukcję znaków nowego wiersza.

Na przykład

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

utrudnia czytanie i rozumienie, co się dzieje. Przeformatowanie do:

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

Jest znacznie łatwiejszy do zrozumienia i sprawia, że ​​„błąd” w pierwszej wersji jest bardziej oczywisty. Po przekształceniu kodu do tego formatu nie ma żadnej rzeczywistej przewagi nad:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
zzzzBov
źródło
3
1. Naprawdę nie mogę się zgodzić z kwestią czytelności, dodawanie instancji przed każdym wywołaniem wcale nie czyni go bardziej zrozumiałym. 2. Wzorce inicjalizacji, które pokazałeś za pomocą js i C #, nie mają nic wspólnego z konstruktorami: to, co zrobiłeś w js, przechodzi jeden argument, a to, co zrobiłeś w C #, to shugar składni, który wywołuje getery i setery za scenami i java nie ma shugar ustawiającego geter jak C #.
Ben
1
@Benedictus, pokazałem różne sposoby, w jakie języki OOP radzą sobie z tym problemem bez tworzenia łańcuchów. Nie chodziło o podanie identycznego kodu, chodziło o pokazanie alternatyw, które sprawiają, że tworzenie łańcuchów jest niepotrzebne.
zzzzBov
2
„Dodanie instancji przed każdym połączeniem wcale nie czyni go bardziej zrozumiałym” Nigdy nie twierdziłem, że dodanie instancji przed każdym wywołaniem uczyniło coś bardziej zrozumiałym, po prostu powiedziałem, że jest względnie równoważne.
zzzzBov
1
VB.NET ma również użyteczne słowo kluczowe „With”, które tworzy tymczasowe odwołanie kompilatora, dzięki czemu np. [Użycie /do reprezentowania podziału linii] With Foo(1234) / .x = 23 / .y = 47byłoby równoważne Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Taka składnia nie powoduje dwuznaczności, ponieważ .xsama w sobie nie może mieć innego znaczenia niż wiązanie się z bezpośrednio otaczającą instrukcją „With” [jeśli nie ma, lub obiekt nie ma elementu x, to .xjest bez znaczenia]. Oracle nie umieściło czegoś takiego w Javie, ale taka konstrukcja idealnie pasowałaby do języka.
supercat
5

Ta technika jest faktycznie używana we wzorze Konstruktora.

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

Ogólnie jednak unika się go, ponieważ jest niejednoznaczny. Nie jest oczywiste, czy zwracana wartość to obiekt (aby można było wywoływać inne obiekty ustawiające), czy też zwracany obiekt to właśnie przypisana wartość (również wspólny wzorzec). W związku z tym Zasada najmniejszej niespodzianki sugeruje, że nie powinieneś próbować zakładać, że użytkownik chce zobaczyć jedno lub drugie rozwiązanie, chyba że jest to fundamentalne dla projektu obiektu.

Cort Ammon
źródło
6
Różnica polega na tym, że podczas korzystania z wzorca konstruktora zwykle piszesz obiekt tylko do zapisu (konstruktor), który ostatecznie konstruuje obiekt tylko do odczytu (niezmienny) (niezależnie od budowanej klasy). W związku z tym pożądany jest długi łańcuch wywołań metod , ponieważ można go użyć jako pojedynczego wyrażenia.
Darkhogg
„lub jeśli zwracany obiekt jest właśnie przypisaną wartością” Nienawidzę tego, jeśli chcę zachować wartość, którą właśnie przekazałem, najpierw wstawię ją do zmiennej. Jednak przydatne może być uzyskanie wartości, która została wcześniej przypisana (wydaje mi się, że niektóre interfejsy kontenerowe to robią).
JAB
@JAB Zgadzam się, nie jestem fanem tego zapisu, ale ma swoje miejsca. Przypomina Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); mi się, że zyskał także popularność, ponieważ odzwierciedla a = b = c = d, choć nie jestem przekonany, czy popularność była dobrze uzasadniona. Widziałem ten, o którym wspomniałeś, zwracający poprzednią wartość, w niektórych bibliotekach operacji atomowych. Morał historii? Jest to nawet bardziej mylące niż mi się wydawało =)
Cort Ammon
Uważam, że naukowcy są trochę pedantyczni: z idiomem nie ma nic złego. Jest niezwykle przydatny w dodawaniu przejrzystości w niektórych przypadkach. Weźmy na przykład następującą klasę formatowania tekstu, która wcina każdy wiersz ciągu i rysuje wokół niego pole ascii-art new BetterText(string).indent(4).box().print();. W takim przypadku ominąłem garść gobbledygooka i wziąłem ciąg znaków, wyciąłem go, zapakowałem i wysłałem. Możesz nawet chcieć metody duplikacji w kaskadzie (takiej jak, powiedzmy, .copy()), aby umożliwić wszystkim kolejnym działaniom nie modyfikowanie oryginału.
tgm1024,
2

To bardziej komentarz niż odpowiedź, ale nie mogę komentować, więc ...

chciałem tylko wspomnieć, że to pytanie mnie zaskoczyło, ponieważ nie uważam tego za rzadkie . Właściwie w moim środowisku pracy (web developer) jest bardzo powszechne.

Na przykład, to jak symfony doctrine: generate: podmioty polecenia auto generuje wszystko setters , domyślnie .

jQuery łączy większość swoich metod w bardzo podobny sposób.

xDaizu
źródło
Js to obrzydliwość
Ben
@Benedictus Powiedziałbym, że PHP jest większą obrzydliwością. JavaScript jest całkowicie dobrym językiem i stał się całkiem przyjemny dzięki włączeniu funkcji ES6 (chociaż jestem pewien, że niektórzy ludzie nadal wolą CoffeeScript lub jego warianty; osobiście nie jestem fanem tego, jak CoffeeScript obsługuje zakres zmiennych, gdy sprawdza zakres zewnętrzny po pierwsze zamiast traktować zmienne przypisane w zasięgu lokalnym jako lokalne, chyba że jawnie wskazane jako nielokalne / globalne, tak jak robi to Python).
JAB
@Benedictus Zgadzam się JAB. W latach college'u patrzyłem na JS jako na obrzydliwy, niedoszły język skryptowy, ale po kilku latach pracy z nim i nauce WŁAŚCIWEGO sposobu korzystania z niego, zacząłem ... naprawdę go kocham . I myślę, że dzięki ES6 stanie się naprawdę dojrzałym językiem. Ale co skłoniło mnie do odwrócenia głowy ... co moja odpowiedź ma wspólnego z JS? ^^ U
xDaizu
Pracowałem kilka lat z js i mogę powiedzieć, że nauczyłem się tego. Niemniej jednak uważam ten język za zwykły błąd. Ale zgadzam się, Php jest znacznie gorszy.
Ben
@Benedictus Rozumiem twoje stanowisko i szanuję je. Jednak nadal mi się podoba. Jasne, ma swoje dziwactwa i zalety (który język nie ma?), Ale stale kroczy we właściwym kierunku. Ale ... Właściwie to nie lubię tego tak bardzo, że muszę go bronić. Nie czerpię korzyści z ludzi, którzy to kochają lub nienawidzą ... hahaha Również nie ma na to miejsca, moja odpowiedź wcale nie dotyczyła JS.
xDaizu