Podczas korzystania z metody łączenia, takiej jak:
var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();
mogą być dwa podejścia:
Ponownie użyj tego samego obiektu, jak poniżej:
public Car PaintedIn(Color color) { this.Color = color; return this; }
Utwórz nowy obiekt typu
Car
na każdym kroku, na przykład:public Car PaintedIn(Color color) { var car = new Car(this); // Clone the current object. car.Color = color; // Assign the values to the clone, not the original object. return car; }
Czy pierwszy jest zły, czy raczej osobisty wybór dewelopera?
Wierzę, że pierwsze podejście może szybko spowodować intuicyjny / wprowadzający w błąd kod. Przykład:
// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);
// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();
// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?
jakieś pomysły?
coding-style
readability
method-chaining
Arseni Mourzenko
źródło
źródło
var car = new Car(Brand.Ford, 12345, Color.Silver);
?Odpowiedzi:
Umieściłem płynny interfejs API we własnej klasie „builder” oddzielonej od obiektu, który tworzy. W ten sposób, jeśli klient nie chce korzystać z płynnego interfejsu API, nadal można go używać ręcznie i nie zanieczyszcza on obiektu domeny (zgodnie z zasadą pojedynczej odpowiedzialności). W takim przypadku zostaną utworzone:
Car
który jest obiektem domenyCarBuilder
który posiada płynne APIUżycie byłoby takie:
CarBuilder
Klasa będzie wyglądać następująco (używam C # konwencję nazewnictwa tutaj):Zauważ, że ta klasa nie będzie bezpieczna dla wątków (każdy wątek będzie potrzebował własnej instancji CarBuilder). Zauważ też, że chociaż płynne API jest naprawdę fajną koncepcją, prawdopodobnie jest to przesada w celu tworzenia prostych obiektów domenowych.
Ta oferta jest bardziej przydatna, jeśli tworzysz interfejs API dla czegoś znacznie bardziej abstrakcyjnego i ma bardziej skomplikowane konfigurowanie i wykonywanie, dlatego świetnie sprawdza się w testowaniu jednostkowym i frameworkach DI. Możesz zobaczyć inne przykłady w sekcji Java artykułu w Wikipedii Płynny interfejs z trwałością, obsługą dat i próbnymi obiektami.
EDYTOWAĆ:
Jak zauważono w komentarzach; można uczynić klasę Konstruktora statyczną klasą wewnętrzną (wewnątrz Samochodu), a Samochód można uczynić niezmiennym. Ten przykład pozwalający Carowi pozostać niezmiennym wydaje się trochę głupi; ale w bardziej złożonym systemie, w którym absolutnie nie chcesz zmieniać zawartości budowanego obiektu, możesz to zrobić.
Poniżej znajduje się przykład tego, jak wykonać zarówno statyczną klasę wewnętrzną, jak i jak obsługiwać niezmienne stworzenie obiektu, które buduje:
Wykorzystanie byłoby następujące:
Edycja 2: Pete w komentarzach napisał post na blogu o używaniu konstruktorów z funkcjami lambda w kontekście pisania testów jednostkowych złożonych obiektów domeny. To ciekawa alternatywa, aby budowniczy był bardziej wyrazisty.
W takim przypadku
CarBuilder
musisz zamiast tego skorzystać z tej metody:Które mogą być użyte jako to:
źródło
build()
(lubBuild()
), a nie nazwę typu, który buduje (Car()
w twoim przykładzie). Ponadto, jeśliCar
jest to naprawdę niezmienny obiekt (np. Wszystkie jego pola sąreadonly
), to nawet konstruktor nie będzie w stanie go zmutować, więcBuild()
metoda staje się odpowiedzialna za budowę nowej instancji. Jednym ze sposobów na to jestCar
posiadanie tylko jednego konstruktora, który przyjmuje jako argument Konstruktor; wtedyBuild()
metoda możereturn new Car(this);
.To zależy.
Czy Twój samochód jest podmiotem czy obiektem wartości ? Jeśli samochód jest bytem, ważna jest tożsamość obiektu, dlatego powinieneś zwrócić to samo odniesienie. Jeśli obiekt jest obiektem wartości, powinien być niezmienny, co oznacza, że jedynym sposobem jest zwracanie nowej instancji za każdym razem.
Przykładem tego ostatniego może być klasa DateTime w .NET, która jest obiektem wartości.
Jeśli jednak model jest bytem, podoba mi się odpowiedź Spoike dotycząca użycia klasy konstruktora do zbudowania obiektu. Innymi słowy, podany przez ciebie przykład ma sens tylko wtedy, gdy samochód jest obiektem wartości.
źródło
Utwórz osobny statyczny wewnętrzny konstruktor.
Użyj normalnych argumentów konstruktora dla wymaganych parametrów. I płynne API dla opcjonalnego.
Nie twórz nowego obiektu podczas ustawiania koloru, chyba że zmienisz nazwę metody NewCarInColour lub coś podobnego.
Zrobiłbym coś takiego z marką zgodnie z wymaganiami, a reszta opcjonalna (to java, ale twój wygląda jak javascript, ale całkiem pewne, że można je zamieniać z odrobiną nitowania):
źródło
Najważniejsze jest to, że niezależnie od wybranej decyzji, jest to wyraźnie określone w nazwie metody i / lub komentarzu.
Nie ma standardu, czasami metoda zwróci nowy obiekt (większość metod String to robi) lub zwróci ten obiekt w celu utworzenia łańcucha lub wydajności pamięci).
Kiedyś zaprojektowałem obiekt Vector Vector i dla każdej operacji matematycznej wdrożyłem obie metody. Dla natychmiastowej metody skalowania:
źródło
scale
(mutatorem) iscaledBy
(generatorem).Widzę tutaj kilka problemów, które mogą być mylące ... Twoja pierwsza linia w pytaniu:
Wywołujesz konstruktor (nowy) i metodę tworzenia ... Metoda create () prawie zawsze byłaby metodą statyczną lub metodą konstruktora, a kompilator powinien złapać ją w ostrzeżeniu lub błędzie, aby poinformować Cię, albo sposób ta składnia jest nieprawidłowa lub ma jakieś okropne nazwy. Ale później nie używasz obu, więc spójrzmy na to.
Znowu z tworzeniem, ale nie z nowym konstruktorem. Rzecz w tym, że myślę, że zamiast tego szukasz metody copy (). Więc jeśli tak jest, a to tylko kiepskie imię, spójrzmy na jedną rzecz ... nazywasz mercedes.Paintedin (Color.Yellow) .Copy () - Powinno być łatwo na to spojrzeć i powiedzieć, że jest malowany „przed skopiowaniem - dla mnie zwykły przepływ logiki. Więc umieść kopię na pierwszym miejscu.
dla mnie łatwo jest zauważyć, że malujesz kopię, robiąc swój żółty samochód.
źródło
Pierwsze podejście ma tę wadę, o której wspomniałeś, ale tak długo, jak wyjaśnisz to w dokumentach, żaden na wpół kompetentny programista nie powinien mieć problemów. Cały kod łańcucha, z którym osobiście pracowałem, działał w ten sposób.
Drugie podejście ma oczywiście tę wadę, że wymaga więcej pracy. Musisz także zdecydować, czy kopie, które zwrócisz, będą wykonywać kopie płytkie, czy głębokie: co jest najlepsze, może różnić się w zależności od klasy lub metody, tak więc albo wprowadzisz niekonsekwencję, albo kompromis w sprawie najlepszego zachowania. Warto zauważyć, że jest to jedyna opcja dla niezmiennych obiektów, takich jak łańcuchy.
Cokolwiek robisz, nie mieszaj i nie dopasowuj w tej samej klasie!
źródło
Wolę myśleć tak jak mechanizm „Metody rozszerzenia”.
źródło
Jest to odmiana powyższych metod. Różnice polegają na tym, że istnieją klasy statyczne w klasie Car, które pasują do nazw metod w Konstruktorze, więc nie trzeba jawnie tworzyć Konstruktora:
Możesz użyć tych samych nazw metod, których używasz w połączonych wywołaniach konstruktora:
Ponadto w klasie znajduje się metoda .copy (), która zwraca konstruktora wypełnionego wszystkimi wartościami z bieżącej instancji, dzięki czemu można utworzyć wariację na temat:
Na koniec metoda .build () konstruktora sprawdza, czy wszystkie wymagane wartości zostały podane, i zgłasza, jeśli brakuje. Może być pożądane, aby wymagać pewnych wartości w konstruktorze konstruktora i pozwolić, aby reszta była opcjonalna; w takim przypadku chcesz jeden z wzorców w pozostałych odpowiedziach.
źródło