Kiedy metoda klasy powinna zwrócić tę samą instancję po modyfikacji?

9

Mam klasy, która ma trzy metody A(), B()i C(). Te metody modyfikują własną instancję.

Podczas gdy metody muszą zwracać instancję, gdy instancja jest osobną kopią (tak jak Clone()), mam wolny wybór do zwrócenia voidlub tej samej instancji ( return this;) podczas modyfikowania tej samej instancji w metodzie i nie zwracania żadnej innej wartości.

Decydując się na zwrócenie tej samej zmodyfikowanej instancji, mogę zrobić porządne łańcuchy metod, takie jak obj.A().B().C();.

Czy byłby to jedyny powód tego?

Czy modyfikowanie własnej instancji i zwracanie jej jest w porządku? A może powinien zwrócić tylko kopię i pozostawić oryginalny obiekt jak poprzednio? Ponieważ zwracając tę ​​samą zmodyfikowaną instancję, użytkownik może założyć, że zwrócona wartość jest kopią, w przeciwnym razie nie zostałaby zwrócona? Jeśli jest w porządku, jaki jest najlepszy sposób na wyjaśnienie takich rzeczy w metodzie?

Martin Braun
źródło

Odpowiedzi:

7

Kiedy stosować łańcuchy?

Tworzenie łańcuchów funkcji jest najbardziej popularne w językach, w których powszechne jest IDE z autouzupełnianiem. Na przykład prawie wszyscy programiści C # używają programu Visual Studio. Dlatego jeśli programujesz w języku C #, dodawanie łańcuchów do swoich metod może być oszczędnością czasu dla użytkowników tej klasy, ponieważ Visual Studio pomoże ci w budowaniu łańcucha.

Z drugiej strony, języki takie jak PHP, które są z natury bardzo dynamiczne i często nie mają obsługi autouzupełniania w IDE, zobaczą mniej klas obsługujących tworzenie łańcuchów. Tworzenie łańcuchów będzie odpowiednie tylko wtedy, gdy do ujawnienia metod łańcuchowych zostaną użyte poprawne phpDocs.

Co to jest łączenie?

Biorąc pod uwagę klasę o nazwie, Foodwie następujące metody są łańcuchowe.

function what() { return this; }
function when() { return new Foo(this); }

Fakt, że jest to odniesienie do bieżącej instancji i tworzy się nową instancję, nie zmienia faktu, że są to metody łańcuchowe.

Nie ma złotej zasady, że metoda łańcuchowa musi odwoływać się tylko do bieżącego obiektu. Rzeczywiście, metody łańcuchowe mogą być w dwóch różnych klasach. Na przykład;

class B { function When() { return true; } };
class A { function What() { return new B(); } };

var a = new A();
var x = a.What().When();

W thisżadnym z powyższych przykładów nie ma odniesienia . Kod a.What().When()jest przykładem łączenia. Co ciekawe, typ klasy Bnigdy nie jest przypisywany do zmiennej.

Metoda jest łączona łańcuchowo, gdy jej wartość zwracana zostanie wykorzystana jako następny składnik wyrażenia.

Oto kilka innych przykładów

 // return value never assigned.
 myFile.Open("something.txt").Write("stuff").Close();

// two chains used in expression
int x = a.X().Y() * b.X().Y();

// a chain that creates new strings
string name = str.Substring(1,10).Trim().ToUpperCase();

Kiedy stosować thisinew(this)

Ciągi znaków w większości języków są niezmienne. Tak więc wywołania metod łączenia zawsze skutkują tworzeniem nowych ciągów. Gdzie jako obiekt taki jak StringBuilder może być modyfikowany.

Spójność jest najlepszą praktyką.

Jeśli masz metody modyfikujące stan obiektu i zwracające this, nie mieszaj metod zwracających nowe instancje. Zamiast tego utwórz specjalną metodę o nazwie Clone(), która zrobi to jawnie.

 var x  = a.Foo().Boo().Clone().Foo();

To jest o wiele jaśniejsze, co dzieje się w środku a.

Krok na zewnątrz i do tyłu

Nazywam to lewą i lewą sztuczką, ponieważ rozwiązuje wiele typowych problemów związanych z łańcuchem. Zasadniczo oznacza to, że wychodzisz z oryginalnej klasy do nowej klasy tymczasowej, a następnie wracasz do oryginalnej klasy.

Klasa tymczasowa istnieje tylko w celu zapewnienia specjalnych cech oryginalnej klasie, ale tylko w szczególnych warunkach.

Często zdarza się, że łańcuch musi zmienić stan , ale klasa Anie może reprezentować wszystkich tych możliwych stanów . Tak więc podczas łańcucha wprowadzana jest nowa klasa, która zawiera odniesienie do A. To pozwala programiście wejść w stan i wrócić do A.

Oto mój przykład, niech stan specjalny będzie znany jako B.

class A {
    function Foo() { return this; }
    function Boo() { return this; }
    function Change() return new B(this); }
}

class B {
    var _a;
    function (A) { _a = A; }
    function What() { return this; }
    function When() { return this; }
    function End() { return _a; }
}

var a = new A();
a.Foo().Change().What().When().End().Boo();

To jest bardzo prosty przykład. Jeśli chcesz mieć większą kontrolę, możesz Bwrócić do nowego supertypu, Aktóry ma różne metody.

Reactgular
źródło
4
Naprawdę nie rozumiem, dlaczego zaleca się łączenie metod w oparciu wyłącznie o obsługę autouzupełniania podobną do Intellisense. Łańcuchy metod lub płynne interfejsy są wzorcem projektowym API dla dowolnego języka OOP; jedyną funkcją autouzupełniania jest zapobieganie literówkom i błędom pisowni.
amon
@amon źle odczytałeś co powiedziałem. Powiedziałem, że jest bardziej popularny, gdy inteligencja jest wspólna dla języka. Nigdy nie mówiłem, że to zależy od funkcji.
Reactgular,
3
Chociaż ta odpowiedź bardzo pomaga mi zrozumieć możliwości tworzenia łańcuchów, wciąż nie mam decyzji, kiedy zastosować łańcuchy. Jasne, spójność jest najlepsza . Mam jednak na myśli przypadek, gdy modyfikuję obiekt w funkcji i return this. Jak mogę pomóc użytkownikom moich bibliotek poradzić sobie i zrozumieć tę sytuację? (Pozwalając im na łączenie metod, nawet jeśli nie jest to wymagane, czy to naprawdę byłoby w porządku? Czy powinienem trzymać się tylko jednego sposobu, w ogóle wymagać zmiany?)
Martin Braun
@modiX, jeśli moja odpowiedź jest prawidłowa, proszę przyjąć ją jako odpowiedź. Inne rzeczy, których szukasz, to opinie na temat korzystania z łańcucha, i nie ma na to właściwej / złej odpowiedzi. To naprawdę zależy od ciebie. Może zbytnio martwisz się drobnymi szczegółami?
Reactgular
3

Te metody modyfikują własną instancję.

W zależności od języka posiadanie metod zwracających void/ uniti modyfikujących ich instancję lub parametry nie jest idiomatyczne. Nawet w językach, w których robiono to częściej (C #, C ++), wychodzi z mody wraz z przejściem na bardziej funkcjonalne programowanie w stylu (obiekty niezmienne, czyste funkcje). Ale załóżmy, że jest na to dobry powód.

Czy byłby to jedyny powód tego?

W przypadku niektórych zachowań (pomyśl x++) oczekuje się, że operacja zwróci wynik, nawet jeśli modyfikuje zmienną. Ale to w dużej mierze jedyny powód, aby zrobić to sam.

Czy modyfikowanie własnej instancji i zwracanie jej jest w porządku? A może powinien zwrócić tylko kopię i pozostawić oryginalny obiekt jak poprzednio? Ponieważ gdy zwrócisz tę samą zmodyfikowaną instancję, użytkownik może przyznać, że zwrócona wartość jest kopią, w przeciwnym razie nie zostanie zwrócona? Jeśli jest w porządku, jaki jest najlepszy sposób na wyjaśnienie takich rzeczy w metodzie?

To zależy.

W językach, w których kopiowanie / nowe i zwracanie jest powszechne (C # LINQ i ciągi), wówczas zwracanie tego samego odwołania byłoby mylące. W językach, w których modyfikowanie i zwracanie jest powszechne (niektóre biblioteki C ++), kopiowanie byłoby mylące.

voidNajlepszym sposobem wyjaśnienia byłoby uczynienie podpisu jednoznacznym (poprzez zwracanie lub używanie konstrukcji języka takich jak właściwości). Po tym dobrze jest podać nazwę tak, SetFooaby pokazywała, że ​​modyfikujesz istniejącą instancję. Ale kluczem jest utrzymanie idiomów języka / biblioteki, z którą pracujesz.

Telastyn
źródło
Dziękuję za odpowiedź. Pracuję głównie z frameworkiem .NET, a twoja odpowiedź jest pełna nie-idiomatycznych rzeczy. Podczas gdy rzeczy takie jak metody LINQ zwracają nową kopię oryginału po dołączeniu operacji itp. Rzeczy takie jak Clear()lub Add()dowolnego typu kolekcji zmodyfikują to samo wystąpienie i zwrócą void. W obu przypadkach mówisz, że byłoby to mylące ...
Martin Braun
@modix - LINQ nie zwraca kopii podczas dołączania operacji.
Telastyn
Dlaczego więc mogę to zrobić obj = myCollection.Where(x => x.MyProperty > 0).OrderBy(x => x.MyProperty);? Where()zwraca nowy obiekt kolekcji. OrderBy()zwraca nawet inny obiekt kolekcji, ponieważ treść jest uporządkowana.
Martin Braun
1
@modix - Wracają nowych obiektów, które implementują IEnumerable, ale żeby było jasne, nie są kopiami, a oni nie są zbiorami (z wyjątkiem ToList, ToArrayitp).
Telastyn
To prawda, nie są kopiami, a ponadto są rzeczywiście nowymi przedmiotami. Mówiąc także, że kolekcja odnosi się do obiektów, które można przeliczyć (obiekty, które zawierają więcej niż jeden obiekt w dowolnej tablicy), a nie do klas, które dziedziczą ICollection. Mój błąd.
Martin Braun
0

(Przyjąłem C ++ jako język programowania)

Dla mnie jest to głównie aspekt czytelności. Jeśli A, B, C są modyfikatorami, szczególnie jeśli jest to obiekt tymczasowy przekazywany jako parametr do jakiejś funkcji, np

do_something(
      CPerson('name').age(24).height(160).weight(70)
      );

w porównaniu do

{
   CPerson person('name');
   person.set_age(24);
   person.set_height(160);
   person.set_weight(70);
   do_something(person);
}

Zarejestruj się, jeśli możesz odwołać odwołanie do zmodyfikowanej instancji, powiedziałbym „tak” i skierowałem cię na przykład do operatorów strumieni „>>” i „<<” ( http://www.cprogramming.com/tutorial/ operator_overloading.html )

Adrian I.
źródło
1
Założyłeś, że to C #. Chcę jednak, aby moje pytanie było bardziej powszechne.
Martin Braun
Jasne, mogłaby to być również Java, ale to był drobny problem, aby umieścić mój przykład z operatorami w kontekście. Istotą było to, że sprowadza się to do czytelności, na którą wpływ mają oczekiwania i pochodzenie czytelnika, na które sami mają wpływ zwykłe praktyki w określonym języku / ramach, z którymi ma do czynienia - jak wskazały inne odpowiedzi.
Adrian,
0

Możesz również zastosować metodę łączenia łańcuchowego ze zwrotem kopii zamiast modyfikować return.

Dobrym przykładem C # jest string.Replace (a, b), który nie zmienia ciągu, na którym został wywołany, ale zamiast tego zwraca nowy ciąg, umożliwiając wesołe połączenie.

Peter Wone
źródło
Tak, wiem. Nie chciałem jednak odnosić się do tego przypadku, ponieważ muszę go użyć, gdy i tak nie chcę edytować własnego wystąpienia. Jednak dzięki za zwrócenie na to uwagi.
Martin Braun
W przypadku innych odpowiedzi ważna jest spójność semantyki. Ponieważ możesz być spójny ze zwrotem kopii i nie możesz być spójny z modyfikacją return (ponieważ nie możesz tego zrobić z niezmiennymi obiektami), powiedziałbym, że odpowiedź na twoje pytanie brzmi: nie używaj zwrotu kopii.
Peter Wone