Łańcuch metod - dlaczego jest to dobra praktyka, czy nie?

151

Tworzenie łańcuchów metod to praktyka metod obiektowych zwracających sam obiekt w celu wywołania wyniku dla innej metody. Lubię to:

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save()

Wydaje się, że jest to dobra praktyka, ponieważ tworzy czytelny kod lub „płynny interfejs”. Jednak wydaje mi się, że zamiast tego wydaje mi się, że łamie notację wywoływania obiektów wynikającą z samej orientacji obiektu - wynikowy kod nie reprezentuje wykonywania działań w wyniku poprzedniej metody, co jest ogólnie oczekiwanym sposobem działania kodu obiektowego:

participant.getSchedule('monday').saveTo('monnday.file')

Ta różnica tworzy dwa różne znaczenia dla notacji z kropką „wywoływania wynikowego obiektu”: W kontekście łączenia w łańcuch powyższy przykład byłby odczytywany jako zapisanie obiektu uczestnika , nawet jeśli w rzeczywistości ma on na celu zapisanie harmonogramu obiekt otrzymany przez getSchedule.

Rozumiem, że różnica polega na tym, czy wywoływana metoda powinna coś zwrócić, czy nie (w takim przypadku zwróci ona sam wywołany obiekt w celu utworzenia łańcucha). Ale tych dwóch przypadków nie da się odróżnić od samej notacji, tylko od semantyki wywoływanych metod. Gdy nie używa się łączenia metod w łańcuch, zawsze mogę wiedzieć, że wywołanie metody operuje na czymś związanym z wynikiem poprzedniego wywołania - w przypadku łączenia w łańcuch to założenie zrywa się i muszę semantycznie przetworzyć cały łańcuch, aby zrozumieć, czym właściwie jest obiekt nazywa się naprawdę. Na przykład:

participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter'))

Tam ostatnie dwa wywołania metod odnoszą się do wyniku getSocialStream, podczas gdy poprzednie odnoszą się do uczestnika. Może to zła praktyka pisanie łańcuchów, w których zmienia się kontekst (prawda?), Ale nawet wtedy będziesz musiał ciągle sprawdzać, czy łańcuchy kropek, które wyglądają podobnie, są w rzeczywistości trzymane w tym samym kontekście, czy tylko działają .

Wydaje mi się, że podczas gdy tworzenie łańcuchów metod powierzchownie daje czytelny kod, przeładowanie znaczenia notacji z kropką powoduje tylko większe zamieszanie. Ponieważ nie uważam się za guru programowania, zakładam, że to moja wina. Więc: Czego mi brakuje? Czy rozumiem, że metoda łączenia jest w jakiś sposób niewłaściwa? Czy są przypadki, w których łączenie metod jest szczególnie dobre, czy takie, w których jest szczególnie złe?

Uwaga: Rozumiem, że to pytanie można odczytać jako wypowiedź zamaskowaną jako pytanie. Jednak tak nie jest - naprawdę chcę zrozumieć, dlaczego tworzenie łańcuchów jest uważane za dobrą praktykę i gdzie popełniam błąd, myśląc, że łamie nieodłączną notację obiektową.

Ilari Kajaste
źródło
Wygląda na to, że łańcuch metod jest przynajmniej jednym ze sposobów pisania inteligentnego kodu w Javie. Nawet jeśli nie wszyscy się
zgodzą
Nazywają te metody „płynnymi” lub „płynnym” interfejsem. Możesz zaktualizować swój tytuł, aby używał tego terminu.
S.Lott,
4
W innej dyskusji dotyczącej SO powiedziano, że płynny interfejs jest szerszą koncepcją związaną z czytelnością kodu, a łączenie metod jest tylko jednym ze sposobów osiągnięcia tego celu. Są jednak blisko spokrewnieni, więc dodałem tag i odwołałem się do płynnych interfejsów w tekście - myślę, że są wystarczające.
Ilari Kajaste
14
Sposób, w jaki o tym myślę, jest taki , że łączenie metod jest w rzeczywistości hakowaniem mającym na celu dodanie brakującej funkcji do składni języka. Naprawdę nie byłoby to potrzebne, gdyby istniała wbudowana notacja alternatywna ., która ignorowałaby wszelkie wartości zwracane przez mehtod i zawsze wywoływała łańcuchowe metody używające tego samego obiektu.
Ilari Kajaste
To świetny idiom kodowania, ale jak wszystkie świetne narzędzia jest nadużywany.
Martin Spamer

Odpowiedzi:

74

Zgadzam się, że jest to subiektywne. Przeważnie unikam łączenia metod, ale ostatnio znalazłem również przypadek, w którym było to właściwe - miałem metodę, która akceptowała około 10 parametrów i potrzebowała więcej, ale przez większość czasu wystarczyło określić mało. Z zastąpieniami bardzo szybko stało się to bardzo uciążliwe. Zamiast tego wybrałem podejście łańcuchowe:

MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();

Metoda łączenia metod była opcjonalna, ale ułatwiła pisanie kodu (szczególnie w przypadku technologii IntelliSense). Pamiętaj jednak, że jest to pojedynczy przypadek i nie jest to ogólna praktyka w moim kodzie.

Chodzi o to, że w 99% przypadków prawdopodobnie można to zrobić równie dobrze lub nawet lepiej bez łączenia metod. Ale jest 1%, gdzie jest to najlepsze podejście.

Vilx-
źródło
4
IMO, najlepszym sposobem użycia łączenia metod w tym scenariuszu jest utworzenie obiektu parametru, który ma być przekazany do funkcji, na przykład P = MyObject.GetParamsObj().SomeParameter(asdasd).SomeOtherParameter(asdasd); Obj = MyObject.Start(); MyObject.Execute(P);. Masz tę zaletę, że możesz ponownie wykorzystać ten obiekt parametru w innych wywołaniach, co jest plusem!
pedromanoel
20
Tylko mój procent wkładu w wzory, metoda fabryczna ma zwykle tylko jeden punkt tworzenia, a produkty są statycznym wyborem opartym na parametrach metody fabrycznej. Tworzenie łańcucha wygląda bardziej na wzorzec Builder, w którym wywołujesz różne metody w celu uzyskania wyniku, metody mogą być opcjonalne, jak w przypadku łączenia metod , możemy mieć tutajPizzaBuilder.AddSauce().AddDough().AddTopping() więcej odniesień
Marco Medrano
3
Łączenie metod w łańcuchy (jak chown w pierwotnym pytaniu) jest uważane za złe, gdy narusza prawo demeter . Zobacz: ifacethoughts.net/2006/03/07/… Odpowiedź udzielona tutaj w rzeczywistości jest zgodna z tym prawem, ponieważ jest to „wzorzec konstruktora”.
Angel O'Sphere
2
@Marco Medrano Przykład PizzaBuilder zawsze mi przeszkadzał, odkąd czytałem go w JavaWorld wieki temu. Czuję, że powinienem dodawać sos do mojej pizzy, a nie do szefa kuchni.
Breandán Dalton
1
Wiem, co masz na myśli, Vilx. Ale kiedy czytam list.add(someItem), czytam, że „ten kod jest dodawany someItemdo listobiektu”. więc kiedy czytam PizzaBuilder.AddSauce(), czytam to naturalnie jako „ten kod dodaje sos do PizzaBuilderobiektu”. Innymi słowy, widzę Orchestrator (jeden robi dodawanie) jako metodę, w której kod list.add(someItem)lub PizzaBuilder.addSauce()jest osadzony. Ale myślę, że przykład PizzaBuilder jest trochę sztuczny. Twój przykład MyObject.SpecifySomeParameter(asdasd)działa dobrze dla mnie.
Breandán Dalton
78

Tylko moje 2 centy;

Łańcuch metod utrudnia debugowanie: - Nie możesz umieścić punktu przerwania w zwięzłym punkcie, więc możesz wstrzymać program dokładnie tam, gdzie chcesz - Jeśli jedna z tych metod zgłasza wyjątek, a otrzymasz numer wiersza, nie masz pojęcia która metoda w „łańcuchu” spowodowała problem.

Myślę, że generalnie dobrą praktyką jest zawsze pisanie bardzo krótkich i zwięzłych linijek. Każda linia powinna wykonać tylko jedno wywołanie metody. Wolę więcej linii niż dłuższe linie.

EDYCJA: komentarz wspomina, że ​​łączenie metod i łamanie wierszy są oddzielne. To prawda. Jednak w zależności od debugera może być możliwe umieszczenie punktu przerwania w środku instrukcji lub nie. Nawet jeśli możesz, użycie oddzielnych wierszy ze zmiennymi pośrednimi zapewnia znacznie większą elastyczność i całą gamę wartości, które możesz sprawdzić w oknie Watch, które pomaga w procesie debugowania.

PROMIEŃ
źródło
4
Czy używanie nowych linii i łańcuchów metod nie jest niezależne? Możesz łączyć się z każdym wywołaniem nowej linii zgodnie z @ Vilx-odpowiedz i zazwyczaj możesz umieścić wiele oddzielnych instrukcji w tej samej linii (np. Używając średników w Javie).
brabster
2
Ta odpowiedź jest w pełni uzasadniona, ale pokazuje tylko słabość obecną we wszystkich znanych mi debuggerach i nie jest związana konkretnie z pytaniem.
masterxilo
1
@Brabster. Mogą być oddzielne dla niektórych debuggerów, jednak posiadanie oddzielnych wywołań ze zmiennymi pośrednimi nadal zapewnia znacznie większe bogactwo informacji podczas badania błędów.
RAY
4
+1, w żadnym momencie nie wiesz, co każda metoda zwróciła podczas debugowania krokowego łańcucha metod. Wzorzec łańcucha metody to hack. To najgorszy koszmar programisty.
Lee Kowalkowski
1
Nie jestem też wielkim fanem tworzenia łańcuchów, ale dlaczego po prostu nie umieścić punktu przerwania w definicjach metod?
Pankaj Sharma
39

Osobiście wolę łączyć metody, które działają tylko na oryginalnym obiekcie, np. Ustawiając wiele właściwości lub wywołując metody narzędziowe.

foo.setHeight(100).setWidth(50).setColor('#ffffff');
foo.moveTo(100,100).highlight();

Nie używam go, gdy jedna lub więcej połączonych metod zwróci w moim przykładzie obiekt inny niż foo. Chociaż składniowo możesz łączyć wszystko, o ile używasz prawidłowego interfejsu API dla tego obiektu w łańcuchu, zmiana obiektów IMHO sprawia, że ​​rzeczy są mniej czytelne i może być naprawdę mylące, jeśli interfejsy API dla różnych obiektów mają jakiekolwiek podobieństwa. Jeśli zrobić kilka naprawdę wspólny wywołanie metody na końcu ( .toString(), .print(), cokolwiek), które są przedmiotem ostatecznie działając na podstawie? Ktoś przypadkowo odczytujący kod może nie wychwycić, że byłby to niejawnie zwrócony obiekt w łańcuchu, a nie oryginalne odwołanie.

Łańcuch różnych obiektów może również prowadzić do nieoczekiwanych błędów zerowych. W moich przykładach, zakładając, że foo jest poprawne, wszystkie wywołania metod są „bezpieczne” (np. Poprawne dla foo). W przykładzie PO:

participant.getSchedule('monday').saveTo('monnday.file')

... nie ma gwarancji (jako zewnętrzny programista patrząc na kod), że getSchedule faktycznie zwróci prawidłowy, niezerowy obiekt harmonogramu. Ponadto debugowanie tego stylu kodu jest często dużo trudniejsze, ponieważ wiele środowisk IDE nie ocenia wywołania metody w czasie debugowania jako obiektu, który można sprawdzić. IMO, za każdym razem, gdy potrzebujesz obiektu do sprawdzenia w celu debugowania, wolę mieć go w jawnej zmiennej.

Brian Moeskau
źródło
Jeśli istnieje szansa, że Participantnie posiada ważnej Schedulewówczas getSchedulemetoda ma na celu zwrócić Maybe(of Schedule)rodzaj i saveTometoda przeznaczona jest do zaakceptowania Maybetypu.
Lightman,
24

Martin Fowler ma tutaj dobrą dyskusję:

Metoda łączenia

Kiedy go używać

Tworzenie łańcuchów metod może znacznie zwiększyć czytelność wewnętrznego DSL iw rezultacie w niektórych umysłach stało się niemal synonimem wewnętrznego DSL. Łączenie metod jest jednak najlepsze, gdy jest używane w połączeniu z innymi kombinacjami funkcji.

Łączenie metod jest szczególnie efektywne w przypadku gramatyki typu parent :: = (this | that) *. Użycie różnych metod zapewnia czytelny sposób sprawdzenia, który argument będzie następny. Podobnie opcjonalne argumenty można łatwo pominąć za pomocą łączenia metod. Lista obowiązkowych klauzul, takich jak parent :: = first second, nie działa tak dobrze w podstawowej formie, chociaż można ją dobrze obsługiwać przy użyciu progresywnych interfejsów. Przez większość czasu wolałbym funkcję zagnieżdżoną w tym przypadku.

Największym problemem związanym z łączeniem metod jest problem z wykończeniem. Chociaż istnieją obejścia, zwykle jeśli napotkasz to, lepiej jest użyć funkcji zagnieżdżonej. Funkcja zagnieżdżona jest również lepszym wyborem, jeśli wpadasz w bałagan ze zmiennymi kontekstowymi.

Dirk Vollmar
źródło
2
Link nie działa :(
Qw3ry
Co masz na myśli mówiąc o DSL? Język specyficzny dla domeny
Sören
@ Sören: Fowler odnosi się do języków specyficznych dla domeny.
Dirk Vollmar
21

Moim zdaniem łączenie metod jest trochę nowością. Jasne, wygląda fajnie, ale nie widzę w tym żadnych prawdziwych zalet.

Jak jest:

someList.addObject("str1").addObject("str2").addObject("str3")

lepiej niż:

someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")

Wyjątkiem może być sytuacja, gdy addObject () zwraca nowy obiekt, w którym to przypadku odblokowany kod może być nieco bardziej uciążliwy, na przykład:

someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")
Tom Dalling
źródło
9
Jest to bardziej zwięzłe, ponieważ unikasz dwóch części „someList” nawet w pierwszym przykładzie i otrzymujesz jedną linię zamiast trzech. To, czy jest to rzeczywiście dobre, czy złe, zależy od różnych rzeczy i być może jest to kwestia gustu.
Fabian Steeg,
26
Prawdziwą zaletą posiadania „someList” tylko raz jest to, że wtedy znacznie łatwiej jest nadać mu dłuższą, bardziej opisową nazwę. Za każdym razem, gdy nazwa musi pojawić się kilka razy w krótkich odstępach czasu, istnieje tendencja do skracania jej (w celu ograniczenia powtórzeń i poprawy czytelności), co sprawia, że ​​jest mniej opisowa, co szkodzi czytelności.
Chris Dodd
Dobra uwaga na temat zasad czytelności i DRY z zastrzeżeniem, że łańcuch metod zapobiega introspekcji wartości zwracanej przez daną metodę i / lub zakłada założenie pustych list / zbiorów i wartości innych niż null z każdej metody w łańcuchu (błąd powodujący wiele NPE w systemach, które muszę często debugować / naprawić).
Darrell Teague
@ChrisDodd Nigdy nie słyszałeś o automatycznym uzupełnianiu?
inf3rno
1
„ludzki mózg bardzo dobrze rozpoznaje powtórzenia tekstu, jeśli ma to samo wcięcie” - na tym właśnie polega problem z powtórzeniami: powoduje, że mózg skupia się na powtarzającym się wzorze. Aby więc przeczytać I ZROZUMIEĆ kod, musisz zmusić swój mózg do spojrzenia poza / za powtarzający się wzór, aby zobaczyć, co się naprawdę dzieje, czyli w różnicach między powtarzającym się wzorem, który twój mózg ma tendencję do tuszowania. To jest DLACZEGO powtarzanie jest tak złe dla czytelności kodu.
Chris Dodd,
7

Jest to niebezpieczne, ponieważ możesz polegać na większej liczbie obiektów niż oczekiwano, tak jak wtedy, gdy twoje wywołanie zwraca instancję innej klasy:

Podam przykład:

FoodStore to obiekt składający się z wielu posiadanych przez Ciebie sklepów spożywczych. foodstore.getLocalStore () zwraca obiekt, który przechowuje informacje o sklepie najbliższym parametrowi. getPriceforProduct (cokolwiek) jest metodą tego obiektu.

Więc kiedy wywołujesz foodStore.getLocalStore (parametry) .getPriceforProduct (cokolwiek)

polegasz nie tylko na FoodStore, jak Ty, ale także na LocalStore.

Jeśli getPriceforProduct (cokolwiek) kiedykolwiek się zmieni, musisz zmienić nie tylko FoodStore, ale także klasę, która wywołała metodę łańcuchową.

Zawsze powinieneś dążyć do luźnego łączenia między klasami.

Biorąc to pod uwagę, osobiście lubię łączyć je w łańcuch podczas programowania Rubiego.

rprandi
źródło
7

Wiele osób używa łączenia metod jako formy wygody, zamiast mieć na uwadze jakiekolwiek problemy z czytelnością. Tworzenie łańcuchów metod jest dopuszczalne, jeśli obejmuje wykonanie tej samej czynności na tym samym obiekcie - ale tylko wtedy, gdy faktycznie poprawia czytelność, a nie tylko w celu napisania mniejszej ilości kodu.

Niestety, wielu stosuje metodę łańcuchową, jak na przykładach podanych w pytaniu. Chociaż nadal można je uczynić czytelnymi, niestety powodują one duże sprzężenie między wieloma klasami, więc nie jest to pożądane.

aberrant80
źródło
6

Wydaje się to subiektywne.

Łączenie metod nie jest czymś, co jest z natury złe lub dobre imo.

Czytelność jest najważniejsza.

(Weź również pod uwagę, że posiadanie dużej liczby połączonych metod spowoduje, że rzeczy będą bardzo delikatne, jeśli coś się zmieni)

John Nicholas
źródło
To rzeczywiście może być subiektywne, stąd subiektywna etykieta. Mam nadzieję, że odpowiedzi podkreślą mnie, w jakich przypadkach łańcuchowanie metod byłoby dobrym pomysłem - w tej chwili nie widzę zbyt wiele, ale myślę, że jest to po prostu mój niezrozumienie zalet koncepcji, a nie coś z natury złego w samym łańcuchu.
Ilari Kajaste
Czy nie byłoby z natury źle, gdyby skutkowało to wysokim sprzężeniem? Rozbicie łańcucha na pojedyncze zdania nie obniża czytelności.
aberrant 80
1
zależy od tego, jak bardzo chcesz być dogmatyczny. Jeśli skutkuje to czymś bardziej czytelnym, może to być preferowane w wielu sytuacjach. Duży problem z tym podejściem polega na tym, że większość metod na obiekcie zwraca odwołania do samego obiektu, ale często metoda zwraca odwołanie do obiektu podrzędnego, na którym można łączyć więcej metod. Kiedy już zaczniesz to robić, innemu programistowi będzie bardzo trudno rozwikłać to, co się dzieje. Również każda zmiana funkcjonalności metody będzie trudna do debugowania w dużej instrukcji złożonej.
John Nicholas,
6

Korzyści z łączenia w łańcuch,
tj. Gdzie lubię go używać

Jedną z zalet łączenia łańcuchowego, o której nie wspomniałem, była możliwość użycia go podczas zmiennej inicjacji lub podczas przekazywania nowego obiektu do metody, nie mając pewności, czy jest to zła praktyka, czy nie.

Wiem, że to wymyślony przykład, ale powiedz, że masz następujące klasy

Public Class Location
   Private _x As Integer = 15
   Private _y As Integer = 421513

   Public Function X() As Integer
      Return _x
   End Function
   Public Function X(ByVal value As Integer) As Location
      _x = value
      Return Me
   End Function

   Public Function Y() As Integer
      Return _y
   End Function
   Public Function Y(ByVal value As Integer) As Location
      _y = value
      Return Me
   End Function

   Public Overrides Function toString() As String
      Return String.Format("{0},{1}", _x, _y)
   End Function
End Class

Public Class HomeLocation
   Inherits Location

   Public Overrides Function toString() As String
      Return String.Format("Home Is at: {0},{1}", X(), Y())
   End Function
End Class

I powiedz, że nie masz dostępu do klasy bazowej, lub Powiedz, że wartości domyślne są dynamiczne, oparte na czasie itp. Tak, możesz wtedy utworzyć instancję, a następnie zmienić wartości, ale może to stać się uciążliwe, zwłaszcza jeśli tylko zdasz wartości do metody:

  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)

Ale czy to nie jest po prostu dużo łatwiejsze do odczytania:

  PrintLocation(New HomeLocation().X(1337))

A co z członkiem klasy?

Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class

vs

Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class

W ten sposób używałem łańcuchów i zazwyczaj moje metody służą tylko do konfiguracji, więc mają tylko 2 linie, ustaw wartość Return Me. Dla nas to wyczyściło ogromne linie, bardzo trudne do odczytania i zrozumienia kodu w jedną linię, która brzmi jak zdanie. coś jak

New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats

Vs Coś jak

New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru
                   , Dealer.CarPicker.Models.WRX
                   , Dealer.CarPicker.Transmissions.SixSpeed
                   , Dealer.CarPicker.Engine.Options.TurboCharged
                   , Dealer.CarPicker.Exterior.Color.Blue
                   , Dealer.CarPicker.Interior.Color.Gray
                   , Dealer.CarPicker.Interior.Options.Leather
                   , Dealer.CarPicker.Interior.Seats.Heated)

Szkoda łańcuchów,
tj. Gdy nie lubię go używać

Nie używam tworzenia łańcuchów, gdy jest wiele parametrów do przekazania do procedur, głównie dlatego, że linie są bardzo długie i jak wspomniał OP, może to być mylące, gdy wywołujesz procedury do innych klas, aby przekazać je do jednej z metody łączenia.

Istnieje również obawa, że ​​procedura zwróci nieprawidłowe dane, dlatego do tej pory używałem łączenia łańcuchowego tylko wtedy, gdy zwracałem tę samą wywoływaną instancję. Jak zostało powiedziane, jeśli tworzysz łańcuch między klasami, utrudniasz debugowanie (która zwróciła wartość null?) I może zwiększyć sprzężenie zależności między klasami.

Wniosek

Jak wszystko w życiu i programowaniu, tworzenie łańcuchów nie jest ani dobre, ani złe, jeśli potrafisz uniknąć zła, wtedy łączenie łańcuchowe może być wielką korzyścią.

Staram się przestrzegać tych zasad.

  1. Staraj się nie łączyć między klasami
  2. Twórz procedury specjalnie do tworzenia łańcuchów
  3. Zrób tylko JEDNĄ rzecz w rutynowym łańcuchu
  4. Użyj go, gdy poprawi to czytelność
  5. Użyj go, gdy upraszcza kod
Apeiron
źródło
6

Łączenie metod może pozwolić na bezpośrednie projektowanie zaawansowanych DSL w Javie. Zasadniczo możesz modelować przynajmniej te typy reguł DSL:

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

Reguły te można zaimplementować za pomocą tych interfejsów

// Initial interface, entry point of the DSL
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow for
// repetitions. Repetitions can be ended any time because this interface
// extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

Dzięki tym prostym regułom można zaimplementować złożone DSL, takie jak SQL, bezpośrednio w Javie, tak jak robi to jOOQ , biblioteka, którą utworzyłem. Zobacz dość złożony przykład SQL wzięty z mojego bloga tutaj:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

Innym fajnym przykładem jest jRTF , mały DSL przeznaczony do tworzenia dokumentów RTF bezpośrednio w Javie. Przykład:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );
Lukas Eder
źródło
@ user877329: Tak, może być używany w prawie każdym języku programowania obiektowego, który zna coś takiego jak interfejsy i polimorfizm podtypów
Lukas Eder.
4

Łańcuch metod może być po prostu nowością w większości przypadków, ale myślę, że ma swoje miejsce. Jeden przykład można znaleźć w użyciu modułu Active Record w CodeIgniter :

$this->db->select('something')->from('table')->where('id', $id);

To wygląda dużo czyściej (i moim zdaniem ma więcej sensu) niż:

$this->db->select('something');
$this->db->from('table');
$this->db->where('id', $id);

To naprawdę jest subiektywne; Każdy ma swoje zdanie.

Nathan
źródło
To jest przykład Fluent Interface z Method Chaining, więc UseCase jest tutaj nieco inny. Nie tylko tworzysz łańcuch, ale tworzysz język specyficzny dla domeny wewnętrznej, który łatwo się czyta. Na marginesie, ActiveRecord CI nie jest ActiveRecord.
Gordon
3

Myślę, że głównym błędem jest myślenie, że jest to podejście zorientowane obiektowo, podczas gdy w rzeczywistości jest to bardziej funkcjonalne podejście do programowania niż cokolwiek innego.

Głównym powodem, dla którego go używam, jest zarówno czytelność, jak i zapobieganie zalewaniu mojego kodu przez zmienne.

Naprawdę nie rozumiem, o czym mówią inni, kiedy mówią, że szkodzi to czytelności. Jest to jedna z najbardziej zwięzłych i spójnych form programowania, z których korzystałem.

Także to:

convertTextToVoice.LoadText ("source.txt"). ConvertToVoice ("destination.wav");

tak bym go zazwyczaj używał. Używanie go do łączenia x liczby parametrów nie jest tym, jak zwykle go używam. Gdybym chciał wpisać x liczbę parametrów w wywołaniu metody, użyłbym składni params :

public void foo (params object [] items)

I rzutuj obiekty na podstawie typu lub po prostu użyj tablicy lub kolekcji typów danych, w zależności od przypadku użycia.

Shane Thorndike
źródło
1
+1 na „podstawowym błędem jest myślenie, że jest to podejście zorientowane obiektowo, podczas gdy w rzeczywistości jest to bardziej funkcjonalne podejście do programowania niż cokolwiek innego”. Najważniejszy przypadek użycia dotyczy manipulacji bezstanowych na obiektach (gdzie zamiast zmieniać jego stan, zwracasz nowy obiekt, na którym nadal działasz). Pytania i inne odpowiedzi PO pokazują stanowe działania, które rzeczywiście wydają się niewygodne w przypadku tworzenia łańcuchów.
OmerB
Tak, masz rację, jest to manipulacja bezstanowa, z wyjątkiem tego, że zazwyczaj nie tworzę nowego obiektu, ale zamiast tego używam iniekcji zależności, aby uczynić go dostępną usługą. I tak, stanowe przypadki użycia nie są tym, do czego moim zdaniem było przeznaczone tworzenie łańcuchów metod. Jedynym wyjątkiem, jaki widzę, jest sytuacja, gdy inicjalizujesz usługę DI z pewnymi ustawieniami i masz jakiś rodzaj watchdoga do monitorowania stanu, takiego jak pewnego rodzaju usługa COM. Po prostu IMHO.
Shane Thorndike
2

Zgadzam się, dlatego zmieniłem sposób implementacji płynnego interfejsu w mojej bibliotece.

Przed:

collection.orderBy("column").limit(10);

Po:

collection = collection.orderBy("column").limit(10);

W implementacji "before" funkcje modyfikowały obiekt i kończyły się na return this. Zmieniłem implementację, aby zwrócić nowy obiekt tego samego typu .

Moje uzasadnienie tej zmiany :

  1. Wartość zwracana nie miała nic wspólnego z funkcją, służyła wyłącznie do obsługi części łańcuchowej. Zgodnie z OOP powinna to być funkcja void.

  2. Łańcuch metod w bibliotekach systemowych również implementuje to w ten sposób (jak linq lub string):

    myText = myText.trim().toUpperCase();
    
  3. Oryginalny obiekt pozostaje nienaruszony, dzięki czemu użytkownik API może zdecydować, co z nim zrobić. Pozwala na:

    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
    
  4. Realizacja kopia może być również stosowany do obiektów budowlanych:

    painting = canvas.withBackground('white').withPenSize(10);
    

    Gdzie setBackground(color)funkcja zmienia instancji i nic nie zwraca (jak jej podobno) .

  5. Zachowanie funkcji jest bardziej przewidywalne (patrz punkt 1 i 2).

  6. Użycie krótkiej nazwy zmiennej może również zmniejszyć bałagan w kodzie, bez wymuszania interfejsu API w modelu.

    var p = participant; // create a reference
    p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save()
    

Wniosek:
Moim zdaniem płynny interfejs korzystający z return thisimplementacji jest po prostu zły.

Bob Fanger
źródło
1
Ale czy zwracanie nowej instancji dla każdego wywołania nie spowodowałoby dużego obciążenia, zwłaszcza jeśli używasz większych elementów? Przynajmniej w przypadku języków niezarządzanych.
Apeiron
@Apeiron Wydajność jest zdecydowanie szybsza w return thiszarządzaniu lub w inny sposób. Moim zdaniem uzyskuje płynne api w „nienaturalny” sposób. (Dodano powód 6: aby pokazać niepłynną alternatywę, która nie ma narzutów / dodatkowych funkcji)
Bob Fanger,
Zgadzam się z Tobą. Przeważnie lepiej jest pozostawić stan bazowy DSL nietknięty i zwracać nowe obiekty przy każdym wywołaniu metody, zamiast tego ... Jestem ciekawy: co to za biblioteka, o której wspomniałeś?
Lukas Eder,
1

Całkowicie pominiętym punktem jest to, że metoda łańcuchowa pozwala na DRY . Jest to skuteczny zamiennik „z” (który jest słabo zaimplementowany w niektórych językach).

A.method1().method2().method3(); // one A

A.method1();
A.method2();
A.method3(); // repeating A 3 times

Ma to znaczenie z tego samego powodu, dla którego DRY zawsze ma znaczenie; jeśli A okaże się błędem, a te operacje trzeba wykonać na B, wystarczy zaktualizować tylko w 1 miejscu, a nie w 3.

Pragmatycznie korzyść w tym przypadku jest niewielka. Mimo to, trochę mniej pisania, trochę bardziej wytrzymały (DRY), wezmę to.

Anfurny
źródło
14
Powtarzanie nazwy zmiennej w kodzie źródłowym nie ma nic wspólnego z zasadą DRY. DRY stwierdza, że ​​„Każdy fragment wiedzy musi mieć jedną, jednoznaczną, autorytatywną reprezentację w systemie” lub innymi słowy, należy unikać powtarzania wiedzy (a nie powtarzania tekstu ).
pedromanoel
4
Z pewnością jest to powtarzanie, które narusza suchość. Powtarzanie nazw zmiennych (niepotrzebnie) powoduje całe zło w taki sam sposób, jak inne formy suchości: stwarza więcej zależności i więcej pracy. W powyższym przykładzie, jeśli zmienimy nazwę A, wersja mokra będzie wymagać 3 zmian i spowoduje błędy do debugowania, jeśli którykolwiek z trzech zostanie pominięty.
Anfurny
1
Nie widzę problemu, który wskazałeś w tym przykładzie, ponieważ wszystkie wywołania metod są blisko siebie. Ponadto, jeśli zapomnisz zmienić nazwę zmiennej w jednej linii, kompilator zwróci błąd, który zostanie poprawiony, zanim będziesz mógł uruchomić program. Ponadto nazwa zmiennej jest ograniczona do zakresu jej deklaracji. Chyba że ta zmienna jest globalna, co już jest złą praktyką programistyczną. IMO, DRY to nie mniej pisania, ale izolacja.
pedromanoel
„Kompilator” może zwrócić błąd, a może używasz PHP lub JS, a interpreter może wyświetlać błąd w czasie wykonywania tylko wtedy, gdy osiągniesz ten warunek.
Anfurny,
1

Generalnie nienawidzę łańcuchów metod, ponieważ uważam, że pogarsza to czytelność. Zwartość jest często mylona z czytelnością, ale nie są to te same terminy. Jeśli robisz wszystko w jednej instrukcji, jest to zwięzłe, ale w większości przypadków jest mniej czytelne (trudniejsze do zrozumienia) niż w przypadku wielu instrukcji. Jak zauważyłeś, chyba że nie możesz zagwarantować, że wartość zwracana przez używane metody jest taka sama, wtedy łączenie metod będzie źródłem nieporozumień.

1.)

participant
    .addSchedule(events[1])
    .addSchedule(events[2])
    .setStatus('attending')
    .save();

vs

participant.addSchedule(events[1]);
participant.addSchedule(events[2]);
participant.setStatus('attending');
participant.save()

2.)

participant
    .getSchedule('monday')
        .saveTo('monnday.file');

vs

mondaySchedule = participant.getSchedule('monday');
mondaySchedule.saveTo('monday.file');

3.)

participant
    .attend(event)
    .setNotifications('silent')
    .getSocialStream('twitter')
        .postStatus('Joining '+event.name)
        .follow(event.getSocialId('twitter'));

vs

participant.attend(event);
participant.setNotifications('silent')
twitter = participant.getSocialStream('twitter')
twitter.postStatus('Joining '+event.name)
twitter.follow(event.getSocialId('twitter'));

Jak widać, wygrywasz prawie nic, ponieważ musisz dodać podziały wierszy do pojedynczego oświadczenia, aby było bardziej czytelne, i musisz dodać wcięcie, aby było jasne, że mówisz o różnych obiektach. Cóż, gdybym chciał użyć języka opartego na identyfikacji, zamiast tego nauczyłbym się Pythona, nie wspominając o tym, że większość IDE usunie wcięcia przez automatyczne formatowanie kodu.

Myślę, że jedynym miejscem, w którym ten rodzaj łączenia może być przydatny, jest przesyłanie strumieni w CLI lub łączenie wielu zapytań razem w SQL. Oba mają cenę za wiele wyciągów. Ale jeśli chcesz rozwiązać złożone problemy, skończysz nawet z tymi, którzy płacą cenę i piszą kod w wielu instrukcjach przy użyciu zmiennych lub piszą skrypty bash i procedury składowane lub widoki.

Według interpretacji SUCHE: „Unikaj powtarzania wiedzy (nie powtarzania tekstu)”. i „Mniej wpisuj, nawet nie powtarzaj tekstów”. Pierwsza z nich to, co tak naprawdę oznacza ta zasada, ale druga to powszechne nieporozumienie, ponieważ wiele osób nie może zrozumieć zbyt skomplikowanych bzdur, takich jak „Każda wiedza musi mieć jedną, jednoznaczną, autorytatywna reprezentacja w systemie ”. Drugi to zwartość za wszelką cenę, co w tym scenariuszu psuje, bo pogarsza czytelność. Pierwsza interpretacja jest przerywana przez DDD podczas kopiowania kodu między ograniczonymi kontekstami, ponieważ luźne sprzężenie jest ważniejsze w tym scenariuszu.

inf3rno
źródło
0

Dobra:

  1. Jest zwięzły, ale pozwala elegancko umieścić więcej w jednej linii.
  2. Czasami można uniknąć użycia zmiennej, która czasami może być przydatna.
  3. Może działać lepiej.

Źli:

  1. Implementujesz zwroty, zasadniczo dodając funkcje do metod na obiektach, które tak naprawdę nie są częścią tego, co te metody mają robić. Zwraca coś, co już masz, aby zaoszczędzić kilka bajtów.
  2. Ukrywa przełączniki kontekstu, gdy jeden łańcuch prowadzi do drugiego. Możesz to uzyskać za pomocą getterów, z wyjątkiem tego, że jest całkiem jasne, gdy zmienia się kontekst.
  3. Tworzenie łańcucha wielu wierszy wygląda brzydko, nie współgra dobrze z wcięciami i może powodować zamieszanie w obsłudze operatora (szczególnie w językach z ASI).
  4. Jeśli chcesz zacząć zwracać coś innego, co jest przydatne w przypadku metody łańcuchowej, potencjalnie będziesz miał trudniej to naprawić lub napotkać więcej problemów.
  5. Przekazujesz kontrolę jednostce, której normalnie byś nie przeładował, aby była ona wygodna, nawet w językach ściśle wpisywanych na maszynie błędy spowodowane przez to nie zawsze mogą zostać wykryte.
  6. Może działać gorzej.

Generał:

Dobrym podejściem jest generalne nieużywanie łańcuchów, dopóki nie pojawią się sytuacje lub określone moduły nie będą do niego szczególnie dopasowane.

W niektórych przypadkach łączenie łańcuchów może poważnie zaszkodzić czytelności, zwłaszcza podczas ważenia w punktach 1 i 2.

W przypadku oskarżenia może być nadużywany, na przykład zamiast innego podejścia (na przykład przekazanie tablicy) lub mieszania metod w dziwaczny sposób (parent.setSomething (). GetChild (). SetSomething (). GetParent (). SetSomething ()).

jgmjgm
źródło
0

Oparta na opiniach odpowiedź

Największą wadą tworzenia łańcuchów jest to, że czytelnikowi może być trudno zrozumieć, jak każda metoda wpływa na oryginalny obiekt, jeśli tak, i jaki typ zwraca każda metoda.

Kilka pytań:

  • Czy metody w łańcuchu zwracają nowy obiekt, czy ten sam obiekt został zmutowany?
  • Czy wszystkie metody w łańcuchu zwracają ten sam typ?
  • Jeśli nie, w jaki sposób jest wskazywane w przypadku zmiany typu w łańcuchu?
  • Czy wartość zwróconą przez ostatnią metodę można bezpiecznie odrzucić?

Debugowanie w większości języków może być rzeczywiście trudniejsze w przypadku łączenia w łańcuch. Nawet jeśli każdy krok w łańcuchu znajduje się na osobnej linii (co jest sprzeczne z celem tworzenia łańcucha), sprawdzenie wartości zwracanej po każdym kroku może być trudne, szczególnie w przypadku metod niemutujących.

Czas kompilacji może być wolniejszy w zależności od języka i kompilatora, ponieważ rozwiązanie wyrażeń może być znacznie bardziej złożone.

Uważam, że jak ze wszystkim, łańcuchowanie jest dobrym rozwiązaniem, które może być przydatne w pewnym scenariuszu. Należy go używać ostrożnie, rozumiejąc konsekwencje i ograniczając liczbę elementów łańcucha do kilku.

Eneko Alonso
źródło
0

W językach typowanych (których brak autolub równoważne) to oszczędza implementatorowi od konieczności deklarowania typów wyników pośrednich.

import Participant
import Schedule

Participant participant = new Participant()
... snip...
Schedule s = participant.getSchedule(blah)
s.saveTo(filename)

W przypadku dłuższych łańcuchów możesz mieć do czynienia z kilkoma różnymi typami pośrednimi, musisz zadeklarować każdy z nich.

Uważam, że to podejście naprawdę rozwinęło się w Javie, gdzie a) wszystkie wywołania funkcji są wywołaniami funkcji składowych oraz b) wymagane są typy jawne. Oczywiście istnieje tu kompromis w kategoriach utraty pewności, ale w niektórych sytuacjach niektórzy uważają, że warto.

Dave
źródło