Dlaczego potrzebujemy klasy Builder podczas wdrażania wzorca Builder?

31

Widziałem wiele implementacji wzorca Builder (głównie w Javie). Wszystkie mają klasę encji (powiedzmy Personklasę) i klasę konstruktora PersonBuilder. Konstruktor „układa” różne pola i zwraca new Personargument z przekazanymi argumentami. Dlaczego jawnie potrzebujemy klasy konstruktora, zamiast umieszczać wszystkie metody konstruktora w Personsamej klasie?

Na przykład:

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

Mogę po prostu powiedzieć Person john = new Person().withName("John");

Dlaczego potrzeba PersonBuilderzajęć?

Jedyną korzyścią, jaką widzę, jest to, że możemy zadeklarować Personpola jako final, zapewniając w ten sposób niezmienność.

Boyan Kushlev
źródło
4
Uwaga: normalna nazwa tego wzoru to chainable setters: D
Mooing Duck
4
Zasada jednej odpowiedzialności. Jeśli umieścisz logikę w klasie Person, będzie to miało więcej niż jedno zadanie do wykonania
reggaeguitar
1
Twoja korzyść może być osiągnięta w dowolny sposób: mogę withNamezwrócić kopię Osoby ze zmienionym tylko polem nazwiska. Innymi słowy, Person john = new Person().withName("John");może działać, nawet jeśli Personjest niezmienny (i jest to powszechny wzorzec w programowaniu funkcjonalnym).
Brian McCutchon
9
@MooingDuck: innym popularnym terminem jest płynny interfejs .
Mac
2
@Mac Myślę, że płynny interfejs jest nieco bardziej ogólny - unikasz voidmetod. Na przykład, jeśli Personma metodę, która wypisuje ich nazwę, nadal możesz połączyć ją w płynny interfejs person.setName("Alice").sayName().setName("Bob").sayName(). Nawiasem mówiąc, adnotuję te w JavaDoc z dokładnie twoją sugestią @return Fluent interface- jest to ogólne i wystarczająco jasne, gdy stosuje się do dowolnej metody, która robi to return thispod koniec jej wykonywania i jest dość jasne. Konstruktor będzie więc również płynnie posługiwał się interfejsem.
VLAZ

Odpowiedzi:

27

Dzięki temu możesz być niezmienny ORAZ symulować jednocześnie nazwane parametry .

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

To trzyma twoje rękawiczki z dala od osoby, dopóki jej stan nie zostanie ustawiony, a po ustawieniu nie pozwoli ci go zmienić, ale każde pole jest wyraźnie oznaczone. Nie możesz tego zrobić za pomocą tylko jednej klasy w Javie.

Wygląda na to, że mówisz o Josh Blochs Builder Pattern . Nie należy tego mylić z wzorcem Gang czterech budowniczych . To są różne bestie. Oba rozwiązują problemy konstrukcyjne, ale na dość różne sposoby.

Oczywiście możesz zbudować obiekt bez użycia innej klasy. Ale wtedy musisz wybrać. Tracisz zdolność symulowania nazwanych parametrów w językach, które ich nie mają (np. Java) lub tracisz zdolność do niezmienności przez cały okres istnienia obiektów.

Niezmienny przykład nie ma nazw parametrów

Person p = new Person("Arthur Dent", 42);

Tutaj budujesz wszystko za pomocą jednego prostego konstruktora. Pozwoli ci to pozostać niezmiennym, ale stracisz symulację nazwanych parametrów. Trudno to odczytać przy wielu parametrach. Komputery nie dbają o to, ale jest to trudne dla ludzi.

Symulowany przykładowy parametr z tradycyjnymi ustawieniami. Niezmienne.

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

Tutaj budujesz wszystko za pomocą seterów i symulujesz nazwane parametry, ale nie jesteś już niezmienny. Każde użycie settera zmienia stan obiektu.

Dzięki dodaniu klasy możesz zrobić jedno i drugie.

Sprawdzanie poprawności można wykonać, build()jeśli wystarczający jest błąd czasu wykonania dla brakującego pola wieku. Możesz to uaktualnić i wymusić age()wywołanie z błędem kompilatora. Po prostu nie z wzorem konstruktora Josh Bloch.

Do tego potrzebny jest wewnętrzny język specyficzny dla domeny (iDSL).

Dzięki temu możesz żądać, aby zadzwonili age()i name()przed połączeniem build(). Ale nie możesz tego zrobić po prostu wracając za thiskażdym razem. Każda zwracana rzecz zwraca inną rzecz, która zmusza cię do wezwania następnej rzeczy.

Zastosowanie może wyglądać następująco:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Ale to:

Person p = personBuilder
    .age(42)
    .build()
;

powoduje błąd kompilatora, ponieważ age()można wywoływać tylko typ zwracany przez name().

Te iDSL są niezwykle wydajne ( np. Strumienie JOOQ lub Java8 ) i są bardzo miłe w użyciu, szczególnie jeśli używasz IDE z uzupełnianiem kodu, ale ich konfiguracja wymaga sporo pracy. Polecam zapisywanie ich dla rzeczy, które będą miały sporo kodu źródłowego napisanego przeciwko nim.

candied_orange
źródło
3
A potem ktoś pyta, dlaczego mają podać nazwę przed wieku, a jedyną odpowiedzią jest „ponieważ kombinatorycznej eksplozji”. Jestem całkiem pewien, że można tego uniknąć w źródle, jeśli ma się odpowiednie szablony, takie jak C ++, lub w dalszym ciągu używa innego typu do wszystkiego.
Deduplicator,
1
Nieszczelny abstrakcji wymaga znajomości bazowego złożoności, aby móc wiedzieć, jak go używać. Oto co tu widzę. Każda klasa, która nie wie, jak się zbudować, musi koniecznie być połączona z Konstruktorem, który ma dalsze zależności (taki jest cel i charakter koncepcji Konstruktora). Pewnego razu (nie na obozowisku) prosta potrzeba utworzenia instancji obiektu wymagała 3 miesięcy, aby cofnąć właśnie taką szczelinę klastra. Nie żartuję.
radarbob
Jedną z zalet klas, które nie znają żadnych reguł konstrukcyjnych, jest to, że gdy te reguły się zmieniają, nie obchodzi ich to. Mogę tylko dodać AnonymousPersonBuilderwłasny zestaw reguł.
candied_orange
Konstrukcja klasy / systemu rzadko pokazuje głębokie zastosowanie „maksymalizacji spójności i minimalizacji sprzężenia”. Legiony projektowania i środowiska wykonawczego są rozszerzalne, a wady można naprawić tylko wtedy, gdy stosowane są maksymy i wytyczne OO z poczuciem, że są fraktalne lub „to żółwie, aż do samego końca”.
radarbob
1
@radarbob Budowana klasa wciąż wie, jak się budować. Jego konstruktor odpowiada za zapewnienie integralności obiektu. Klasa konstruktora jest jedynie pomocnikiem dla konstruktora, ponieważ konstruktor często przyjmuje dużą liczbę parametrów, co nie czyni bardzo przyjemnego API.
Nie U
57

Dlaczego warto korzystać / udostępniać klasę konstruktora:

  • Aby tworzyć niezmienne obiekty - korzyści, które już zidentyfikowałeś. Przydatne, jeśli konstrukcja wymaga wielu kroków. FWIW, niezmienność powinna być istotnym narzędziem w naszych dążeniach do pisania programów, które można utrzymywać i nie zawierają błędów.
  • Jeśli reprezentacja środowiska wykonawczego końcowego (być może niezmiennego) obiektu jest zoptymalizowana do odczytu i / lub wykorzystania miejsca, ale nie do aktualizacji. String i StringBuilder są tutaj dobrymi przykładami. Wielokrotnie łączenie łańcuchów nie jest bardzo wydajne, więc StringBuilder używa innej wewnętrznej reprezentacji, która jest dobra do dodawania - ale nie tak dobra pod względem wykorzystania miejsca i nie tak dobra do czytania i używania jak zwykła klasa String.
  • Aby wyraźnie oddzielić skonstruowane obiekty od obiektów w budowie. Podejście to wymaga wyraźnego przejścia od budowy w budowie do budowy. Dla konsumenta nie ma możliwości pomylenia obiektu będącego w budowie z obiektem skonstruowanym: wymusi to system typów. Oznacza to, że czasami możemy zastosować to podejście, aby „wpaść w pułapkę sukcesu”, a gdy robimy abstrakcję do wykorzystania przez innych (lub nas samych) (jak API lub warstwę), może to być bardzo dobre rzecz.
Erik Eidt
źródło
4
Odnośnie ostatniego punktu. Więc idea polega na tym, że a PersonBuildernie ma getterów, a jedynym sposobem na sprawdzenie bieżących wartości jest wywołanie, .Build()aby zwrócić a Person. W ten sposób .Build może zweryfikować poprawnie skonstruowany obiekt, prawda? Czy to mechanizm, który ma zapobiegać użyciu obiektu „w budowie”?
AaronLS,
@AaronLS, tak, oczywiście; złożoną walidację można wprowadzić w Buildoperację, aby zapobiec złym i / lub niedokonanym obiektom.
Erik Eidt,
2
Możesz również zweryfikować swój obiekt w jego konstruktorze, preferencje mogą być osobiste lub zależne od złożoności. Niezależnie od tego, co zostanie wybrane, najważniejsze jest to, że gdy masz już niezmienny obiekt, wiesz, że jest on właściwie zbudowany i nie ma nieoczekiwanej wartości. Niezmienny obiekt z niepoprawnym stanem nie powinien się zdarzyć.
Walfrat
2
@AaronLS konstruktor może mieć programy pobierające; nie o to chodzi. Nie musi, ale jeśli konstruktor ma moduły pobierające, należy pamiętać, że wywołanie getAgekonstruktora może powrócić, nulljeśli ta właściwość nie została jeszcze określona. W przeciwieństwie do tego Personklasa może wymusić niezmienność, że wiek nigdy nie może być null, co jest niemożliwe, gdy budowniczy i rzeczywisty obiekt są pomieszane, jak w new Person() .withName("John")przykładzie OP , który na szczęście tworzy Personwiek bez wieku. Dotyczy to nawet klas zmiennych, ponieważ setery mogą wymuszać niezmienniki, ale nie mogą wymuszać wartości początkowych.
Holger
1
@Holger twoje punkty są prawdziwe, ale głównie popierają argument, że Konstruktor powinien unikać getterów.
user949300,
21

Jednym z powodów byłoby zapewnienie, że wszystkie przekazywane dane są zgodne z regułami biznesowymi.

Twój przykład nie bierze tego pod uwagę, ale powiedzmy, że ktoś przekazał pusty ciąg znaków lub ciąg znaków specjalnych. Chcielibyście zrobić logikę opartą na upewnieniu się, że ich nazwa jest w rzeczywistości prawidłową nazwą (co w rzeczywistości jest bardzo trudnym zadaniem).

Możesz umieścić to wszystko w klasie Person, szczególnie jeśli logika jest bardzo mała (na przykład upewniając się, że wiek nie jest ujemny), ale gdy logika rośnie, sensowne jest jej rozdzielenie.

Diakon
źródło
7
Przykład w pytaniu zawiera proste naruszenie zasad: nie podano wieku dlajohn
Caleth
6
+1 I bardzo trudno jest tworzyć reguły dla dwóch pól jednocześnie bez klasy konstruktora (pola X + Y nie mogą być większe niż 10).
Reginald Blue,
7
@ReginaldBlue jest jeszcze trudniej, jeśli są powiązane przez „x + y jest parzyste”. Nasz początkowy warunek z (0,0) jest OK, stan (1,1) jest również OK, ale nie ma sekwencji aktualizacji pojedynczej zmiennej, która zarówno zachowuje stan i prowadzi nas z (0,0) do (1) , 1).
Michael Anderson
Uważam, że to największa zaleta. Wszystkie pozostałe są ładne.
Giacomo Alzetta
5

Trochę inaczej pod tym kątem niż w innych odpowiedziach.

withFooPodejście tutaj jest problematyczne, ponieważ zachowują się jak ustawiaczy ale są określone w taki sposób, aby uczynić go pojawiają niezmienność wsporniki klasy. W klasach Java, jeśli metoda modyfikuje właściwość, zwykle rozpoczyna się od „set”. Nigdy nie podobało mi się to jako standard, ale jeśli zrobisz coś innego, zaskoczy ludzi i to nie jest dobre. Istnieje inny sposób, aby wesprzeć niezmienność za pomocą podstawowego interfejsu API, który masz tutaj. Na przykład:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

Nie zapewnia wiele w zapobieganiu niewłaściwie wykonanym częściowo częściowo skonstruowanym obiektom, ale zapobiega zmianom istniejących obiektów. Prawdopodobnie jest to głupie dla tego rodzaju rzeczy (podobnie jak JB Builder). Tak, stworzysz więcej obiektów, ale nie jest to tak drogie, jak mogłoby się wydawać.

Tego rodzaju podejście będzie najczęściej stosowane w przypadku współbieżnych struktur danych, takich jak CopyOnWriteArrayList . A to wskazuje, dlaczego niezmienność jest ważna. Jeśli chcesz, aby Twój kod był wątkowo bezpieczny, prawie zawsze należy wziąć pod uwagę niezmienność. W Javie każdy wątek może przechowywać lokalną pamięć podręczną stanu zmiennej. Aby jeden wątek widział zmiany dokonane w innych wątkach, należy zastosować synchronizowany blok lub inną funkcję współbieżności. Każdy z nich doda trochę narzutu do kodu. Ale jeśli twoje zmienne są ostateczne, nie ma nic do zrobienia. Wartość będzie zawsze taka, jak na początku, dlatego wszystkie wątki widzą to samo bez względu na wszystko.

JimmyJames
źródło
2

Innym powodem, który nie został tutaj wyraźnie wymieniony, jest to, że build()metoda może sprawdzić, czy wszystkie pola są „polami zawierającymi prawidłowe wartości (albo ustawione bezpośrednio, albo wyprowadzone z innych wartości innych pól), co jest prawdopodobnie najbardziej prawdopodobnym trybem awarii. inaczej by się to zdarzyło.

Kolejną korzyścią jest to, że Twój Personobiekt będzie miał prostszy okres życia i prosty zestaw niezmienników. Wiesz, że kiedy masz Person p, masz p.namei ważne p.age. Żadna z twoich metod nie musi być zaprojektowana do radzenia sobie z sytuacjami takimi jak „cóż, jeśli wiek jest ustawiony, ale nie imię, lub co jeśli imię jest ustawione, ale nie wiek?” Zmniejsza to ogólną złożoność klasy.

Alexander - Przywróć Monikę
źródło
„[...] może zweryfikować, czy wszystkie pola są ustawione [...]”. Wzorzec Konstruktora polega na tym, że nie musisz sam ustawiać wszystkich pól i możesz wrócić do rozsądnych wartości domyślnych. Jeśli i tak musisz ustawić wszystkie pola, być może nie powinieneś używać tego wzoru!
Vincent Savard
@VincentSavard Rzeczywiście, ale według mojej oceny jest to najczęstsze użycie. Aby sprawdzić, czy ustawiony jest pewien „podstawowy” zestaw wartości, i wykonać fantazyjne traktowanie niektórych opcjonalnych (ustawianie wartości domyślnych, wyprowadzanie ich wartości z innych wartości itp.).
Alexander - Przywróć Monikę
Zamiast powiedzieć „sprawdź, czy wszystkie pola są ustawione”, zmieniłbym to na „sprawdzenie, czy wszystkie pola zawierają prawidłowe wartości [czasami zależne od wartości innych pól]” (tj. Pole może dopuszczać tylko niektóre wartości w zależności od wartości innego pola). Można to zrobić w każdym seterze, ale łatwiej jest ustawić obiekt bez zgłaszania wyjątków, dopóki obiekt nie zostanie ukończony i nie będzie gotowy do budowy.
yitzih
Konstruktor budowanej klasy jest ostatecznie odpowiedzialny za prawidłowość swoich argumentów. Sprawdzanie poprawności w kreatorze jest co najwyżej zbędne i może łatwo zsynchronizować się z wymaganiami konstruktora.
Nie U
@TKK Rzeczywiście. Ale potwierdzają różne rzeczy. W wielu przypadkach, w których użyłem Konstruktora, zadaniem konstruktora było skonstruowanie danych wejściowych, które ostatecznie zostały przekazane konstruktorowi. Np. Konstruktor jest skonfigurowany z a URI, a Filelub a FileInputStreami używa wszystkiego, co zostało dostarczone, aby uzyskać a, FileInputStreamktóry ostatecznie przechodzi do wywołania konstruktora jako arg
Alexander - Przywróć Monikę
2

Można również zdefiniować konstruktora, aby zwracał interfejs lub klasę abstrakcyjną. Możesz użyć konstruktora do zdefiniowania obiektu, a konstruktor może na przykład określić, którą konkretną podklasę zwrócić, na podstawie ustawionych właściwości lub ustawionych dla nich właściwości.

Ed Marty
źródło
2

Wzorzec konstruktora służy do budowania / tworzenia obiektu krok po kroku przez ustawienie właściwości, a po ustawieniu wszystkich wymaganych pól, należy zwrócić ostateczny obiekt za pomocą metody budowania Nowo utworzony obiekt jest niezmienny. Należy przede wszystkim zauważyć, że obiekt jest zwracany tylko wtedy, gdy wywoływana jest ostateczna metoda kompilacji. Zapewnia to, że wszystkie właściwości są ustawione na obiekt, a zatem obiekt nie jest w niespójnym stanie, gdy jest zwracany przez klasę konstruktora.

Jeśli nie użyjemy klasy konstruktora i bezpośrednio umieścimy wszystkie metody klasy konstruktora w samej klasie Person, musimy najpierw utworzyć obiekt, a następnie wywołać metody ustawiające na utworzonym obiekcie, co doprowadzi do niespójnego stanu obiektu między tworzeniem obiektu i ustawienie właściwości.

Tak więc, używając klasy konstruktora (tj. Jakiegoś zewnętrznego elementu innego niż sama klasa Person), zapewniamy, że obiekt nigdy nie będzie w niespójnym stanie.

Shubham Bondarde
źródło
@DawidO masz mój punkt. Główny nacisk, który staram się tutaj położyć, to niespójny stan. Jest to jedna z głównych zalet używania wzorca Konstruktora.
Shubham Bondarde
2

Ponownie użyj obiektu konstruktora

Jak wspomnieli inni, niezmienność i weryfikacja logiki biznesowej wszystkich pól w celu sprawdzenia poprawności obiektu są głównymi przyczynami osobnego obiektu konstruktora.

Jednak ponowne użycie jest kolejną korzyścią. Jeśli chcę utworzyć instancję wielu bardzo podobnych obiektów, mogę wprowadzić niewielkie zmiany w obiekcie konstruktora i kontynuować tworzenie instancji. Nie ma potrzeby ponownego tworzenia obiektu konstruktora. To ponowne użycie pozwala konstruktorowi działać jako szablon do tworzenia wielu niezmiennych obiektów. Jest to niewielka korzyść, ale może być przydatna.

yitzih
źródło
1

W rzeczywistości możesz mieć metody budujące w swojej klasie i nadal mieć niezmienność. Oznacza to tylko, że metody konstruktora zwrócą nowe obiekty, zamiast modyfikować istniejący.

Działa to tylko wtedy, gdy istnieje sposób na uzyskanie początkowego (ważnego / użytecznego) obiektu (np. Z konstruktora, który ustawia wszystkie wymagane pola lub metoda fabryczna, która ustawia wartości domyślne), a dodatkowe metody konstruktora zwracają następnie zmodyfikowane obiekty na podstawie na istniejącym. Te metody konstruktora muszą mieć pewność, że po drodze nie dostaniesz nieprawidłowych / niespójnych obiektów.

Oczywiście oznacza to, że będziesz mieć wiele nowych obiektów i nie powinieneś tego robić, jeśli tworzenie obiektów jest kosztowne.

Użyłem tego w kodzie testowym do tworzenia dopasowań Hamcrest dla jednego z moich obiektów biznesowych. Nie pamiętam dokładnego kodu, ale wyglądał mniej więcej tak (uproszczony):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

Użyłbym go w ten sposób w testach jednostkowych (z odpowiednim importem statycznym):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));
Paŭlo Ebermann
źródło
1
To prawda - i myślę, że jest to bardzo ważna kwestia, ale nie rozwiązuje ostatniego problemu - Nie daje szansy na sprawdzenie, czy obiekt jest spójny podczas budowy. Po zbudowaniu będziesz potrzebować pewnych sztuczek, takich jak wywołanie modułu sprawdzającego poprawność przed dowolną z metod biznesowych, aby upewnić się, że program wywołujący ustawił wszystkie metody (co byłoby straszną praktyką). Myślę, że dobrze jest przemyśleć tego rodzaju rzeczy, aby zobaczyć, dlaczego używamy wzorca Konstruktora tak, jak my.
Bill K
@BillK true - obiekt z zasadniczo niezmiennymi ustawieniami (które zwracają nowy obiekt za każdym razem) oznacza, że ​​każdy zwracany obiekt powinien być poprawny. Jeśli zwracasz nieprawidłowe obiekty (np. Osoba z, nameale nie age), to koncepcja nie działa. Cóż, możesz zwracać coś takiego, PartiallyBuiltPersongdy nie jest to ważne, ale wydaje się, że to hack w celu zamaskowania Konstruktora.
VLAZ
@BillK to właśnie miałem na myśli, „jeśli istnieje sposób na uzyskanie początkowego (ważnego / użytecznego) obiektu” - zakładam, że zarówno początkowy obiekt, jak i obiekt zwrócony z każdej funkcji konstruktora są prawidłowe / spójne. Twoim zadaniem jako twórcy takiej klasy jest dostarczenie tylko metod budowniczych, które dokonują tych spójnych zmian.
Paŭlo Ebermann