#4 Wzorce projektowe: Builder – zbudujmy to lepiej

designPatternArt

Builder, czyli nasz polski budowniczy należy do wzorców kreacyjnych. Dzięki niemu możemy tworzyć złożone obiekty dzieląc jeden duży proces na kilka mniejszych etapów. Każdy z tych etapów może być implementowany na wiele różnych sposobów. Daje nam to możliwość tworzenia różnych reprezentacji tego samego obiektu.

Diagram

Zacznijmy od diagramu z klasami, na przykładzie których przedstawię Wam wzorzec:

Produkt

To co nas najbardziej interesuje, czyli tworzony produkt. W naszym przypadku będzie to paczka świąteczna (ChristmasPack) jako, że jeszcze jesteśmy w tym świątecznym klimacie. Klasa składa się z prezentów oraz opakowania (pola Present oraz Wrapping) oraz settery do nich, które nie zostały przedstawione na diagramie, żeby nie zaciemniać rysunku.

Builder

Kolejnym elementem wzorca jest sam builder. Jest to zazwyczaj interfejs lub klasa abstrakcyjna, w której mamy metody odpowiedzialne za tworzenie kolejnych części produktu. W naszym przypadku jest to interfejs ChristmasPackBuilder z dwoma metodami dodającymi do paczki prezenty oraz opakowanie.

Konkretne implementacje buildera

Są to klasy implementujące opisany wyżej builder. Na diagramie widzimy, że są to klasy NiceChristmasPackBuilder oraz NaughtyChristmasPackBuilder. Tworząc dwie osobne klasy jesteśmy w stanie określić jakie prezenty chcemy spakować dla grzecznych dzieci, a jakie dla niegrzecznych. Klient nie musi znać parametrów, dla każdego typu paczki. Zdefiniowane są one w konkretnych implementacjach buildera.

Director (zarządca)

Naszym zarządcą jest klasa Packer, która decyduje/zarządza według jakiej instrukcji ma być tworzony produkt. W metodzie createChristmasPack() wywoływane są metody z kolejnymi krokami pakowania paczki.

class Packer {
    private final ChristmasPackBuilder christmasPackBuilder;

    public Packer(ChristmasPackBuilder builder) {
        christmasPackBuilder = builder;
    }

    public ChristmasPack createChristmasPack() {
        christmasPackBuilder.addPresent();
        christmasPackBuilder.addWrapping();
        return christmasPackBuilder.getChristmasPack();
    }
}

Uruchamiamy

W naszej klasie klienckiej tworzymy zarządcę (Packer), do którego przekazujemy konkretną implementację buildera, a tym samym instrukcję z odpowiednimi parametrami dla konkretnej paczki (naughty or nice).

Packer packer = new Packer(new NaughtyChristmasPackBuilder());
ChristmasPack christmasPack = packer.createChristmasPack();
System.out.println(christmasPack.toString());

packer = new Packer(new NiceChristmasPackBuilder());
christmasPack = packer.createChristmasPack();
System.out.println(christmasPack.toString());

Podsumowanie

Nie ma co ukrywać, przykład nie jest skomplikowany, ale na takich najlepiej można zrozumieć sens wzorca. Wyobraźmy sobie teraz, że chcemy dodać jeszcze jeden typ paczki. Wystarczy przygotować instrukcję tworzenia obiektu z nowymi parametrami, tzn. nową implementację buildera. I to wszystko, potem tylko wywołujemy zarządcę przekazując w konstruktorze nowo stworzoną klasę.

Wzorzec builder pozwala nam całkowicie zakryć sposób tworzenia obiektów przed klientem. Otrzymuje on gotowy produkt, który w zależności od rodzaju jest tworzony w inny sposób. W naszym przykładzie zarówno tworzenie prezentu jak i opakowania wyglądało tak samo, czyli wywoływane były kolejne settery ustawiające odpowiednie wartości, ale równie dobrze możemy wykonać zupełnie inną logikę dla jednego typu paczki i inną dla drugiego.

Wzorzec builder ostatnio często mylony jest z wzorcem zwanym Test Data Builder, który nazywany jest również Fluent Builder’em lub Appender’em, ale o nim opowiem w kolejnym wpisie.

GitHub

Całość znajdziecie na GitHubie.

  • Ceranek

    Cześć,
    zastanawiam się czy jest to dobre rozwiązanie i mam wątpliwości. Wzorzec builder należący do wzorców kreacyjnych ma na celu tylko nałożyć warstwę abstrakcji na konstruktor budowanego obiektu by uniezależnić go od kolejności oraz obowiązkowości argumentów w konstruktorze. Tutaj się tak nie dzieje. To co zrobiłaś w tym przykładzie to wymieszanie wzorca builder ze wzorcem dekorator i metodą wytwórczą. To raczej nie jest dobry pomysł. Poprawnym rozwiązaniem byłoby stworzenie wzorca builder dla klasy ChristmasPack, który po prostu wypełniał by pola. Jeżeli chcielibyśmy opakować nasz prezent w papier powinniśmy stworzyć do niego dekorator i tu pojawia się możliwość stworzenia kolejnego builder’a do nowej klasy zapakowanego prezentu. A dalej moglibyśmy stworzyć metody wytwórcze do klas Naughty… oraz Nice… Wzorce projektowe radziły sobie z tym case’em wystarczająco dobrze. Co prawda należy się punkt za podjętą próbę, to owa próba wydaje się być nietrafiona. Co jeżeli chciałbym mieć prezent bez papieru albo w dwóch papierach?
    Pozdrawiam serdecznie.

    • CodeCouple.pl

      Cześć, wzorzec o którym piszesz nazywa się test data builder (opisany w artykule z 26 stycznia 2018). We wzorcu tym udostępnia się mechanizm ustawia poszczególnych pól w klasie. Tym samym od uzależniamy się od kolejności oraz obowiązkowości argumentów w konstruktorze. Wzorzec opisany w tym wpisie ma za zadanie przygotowanie “przepisu” jak tworzyć dany obiekt, a klient wykorzystuje tylko ten przepisz do tworzenia. Dzięki temu ukrywamy to jak tworzony jest pod spodem. “Co jeżeli chciałbym mieć prezent bez papieru albo w dwóch papierach?” tworzysz odpowiednie dla buildery przykładowo: ChristmasPackWithoutPaperBuilder.