Po co używać wzorca Budowniczy
Tematem niniejszego wpisu jest kolejny z wzorców projektowych w programowaniu – Budowniczy. Jest to wzorzec, który stosuje się w celu hermetyzowania tworzenia produktu i umożliwienia jego wieloetapowego inicjowania. Innymi słowy, ma rozwiązać problem tworzenia złożonych obiektów (takich, które wymagają wprowadzenia wielu argumentów podczas ich tworzenia), które będą tworzone wiele razy w różny sposób. Ogólnie rzecz biorąc, Budowniczowie to klasy, które ułatwiają tworzenie innych, złożonych w inicjalizacji obiektów. Zwracam uwagę, że implementację przedstawionego tutaj prostego kodu można by rozwiązać inaczej – w łatwiejszy sposób. Chodzi głównie o pokazanie idei omawianego wzorca.
Wzorzec Budowniczy w praktyce
Wyobraźmy sobie, że pracujemy w firmie zajmującej się budową domów. Aktualnie chcemy zbudować standardowy, ekonomiczny domek dla średnio zamożnej rodziny. Domek taki posiada określone właściwości (pola przedstawionej klasy).
public class DomStandard {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
public DomStandard(String okna, String ogrod, String basen, String brama, String garaz) {
this.okna = okna;
this.ogrod = ogrod;
this.basen = basen;
this.brama = brama;
this.garaz = garaz;
}
}
Klasa posiada 5 pól oraz konstruktor. Aby stworzyć nowy dom stosujemy poniższy kod.
DomStandard domStandard = new DomStandard("Średnia wielkość", "Mały", "Brak", "Ręczna", "Mały");
Dla potrzeb niniejszego wpisu dodajmy klasę, która ma za zadanie „budować” tenże dom.
public class BudujDomStandard {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
public void montujOkna() {
this.okna = "Średnia wielkość";
}
public void tworzOgrod() {
this.ogrod = "Mały";
}
public void budujBasen() {
this.basen = "Brak";
}
public void montujBrame() {
this.brama = "Ręczna";
}
public void budujGaraz() {
this.garaz = "Mały";
}
}
Budowa domu przebiega według pewnego schematu.
BudujDomStandard domStandard = new BudujDomStandard(0;
domStandard.montujOkna();
domStandard.tworzOgrod();
domStandard.budujBasen();
domStandard.montujBrame();
domStandard.budujGaraz();
Z czasem zdobyliśmy doświadczenie i chcieliśmy zająć się budową domów bardziej ekskluzywnych. Przeznaczonych dla bardziej wymagających klientów. Jednak taki dom ma inne komponenty – większe okna, wymyślny ogród a także wprowadzoną automatyzację niektórych jego elementów.
public class BudujDomPremium {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
public void montujOkna() {
this.okna = "Duże";
}
public void tworzOgrod() {
this.ogrod = "Średni";
}
public void budujBasen() {
this.basen = "Mały";
}
public void montujBrame() {
this.brama = "Automatyczna";
}
public void budujGaraz() {
this.garaz = "Duży";
}
}
Co prawda nowy, ekskluzywny rodzaj nieruchomości ma inne komponenty, jednak sposób jego budowy pozostaje taki sam jak domu ekonomicznego.
BudujDomPremium domPremium = new BudujDomPremium(0;
domPremium.montujOkna();
domPremium.tworzOgrod();
domPremium.budujBasen();
domPremium.montujBrame();
domPremium.budujGaraz();
Rozdzielmy teraz stworzone klasy na te zajmujące się budową oraz przedstawiające sam dom.
public class DomStandard {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
}
public class DomPremium {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
}
public class BudowniczyDomStandard {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
public void montujOkna() {
this.okna = "Średnia wielkość";
}
public void tworzOgrod() {
this.ogrod = "Mały";
}
public void budujBasen() {
this.basen = "Brak";
}
public void montujBrame() {
this.brama = "Ręczna";
}
public void budujGaraz() {
this.garaz = "Mały";
}
}
public class BudowniczyDomPremium {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
public void montujOkna() {
this.okna = "Duże";
}
public void tworzOgrod() {
this.ogrod = "Średni";
}
public void budujBasen() {
this.basen = "Mały";
}
public void montujBrame() {
this.brama = "Automatyczna";
}
public void budujGaraz() {
this.garaz = "Duży";
}
}
Obie klasy: DomPremium oraz DomStandard mają te same właściwości. Stwórzmy więc dla nich jedną klasę.
public class Dom {
public String okna {set; get;}
public String ogrod {set; get;}
public String basen {set; get;}
public String brama {set; get;}
public String garaz {set; get;}
}
Podobnie, oboje budowniczowie mają te same metody – a więc stwórzmy dla nich jeden, wspólny interfejs.
public interface IBudowniczyDomow {
public void montujOkna();
public void tworzOgrod();
public void budujBasen();
public void montujBrame();
public void budujGaraz();
Dom wydajZbudowanyDom();
}
public class BudowniczyDomStandard : IBudowniczyDomow {
...
...
}
public class BudowniczyDomPremium : IBudowniczyDomow {
...
...
}
Dodana została metoda wydająca zbudowany już obiekt (w momencie, kiedy któryś z budowniczych skończy budować obiekt (dom), nieruchomość można uznać za gotową). A kto zamawia wykonanie poszczególnych domów (obiektów) – oczywiście klient.
Odnośnie klienta – nie mamy go jeszcze zaimplementowanego. Klient będzie prosił o wykonanie danego obiektu przez budowniczego.
public class Klient {
public void zbudujDom(IBudowniczyDomow budowniczy) {
budowniczy.montujOkna();
budowniczy.tworzOgrod();
budowniczy.budujBasen();
budowniczy.montujBrame();
budowniczy.budujGaraz();
}
}
Pokażmy teraz, jak to wszystko miałoby działać.
static void Main(string[] args)
{
Klient klient = new Klient();
IBudowniczyDomow standard = new BudownczyDomowStandard();
IBudowniczyDomow premium = new BudowniczyDomowPremium();
//buduj według logiki klienta - tak jak on chce, używając implementacji z standard
klient.zbudujDom(standard);
//buduj według logiki klienta - tak jak on chce, używając implementacji z premium
klient.zbudujDom(premium);
Dom domStandard = standard.wydajZbudowanyDom();
Dom domPremium = premium.wydajZbudowanyDom();
}
Elementy wzorca Budowniczy oraz ich rola
Przeanalizujmy teraz podstawowe elementy, które występują w omawianym wzorcu:
– budowniczy (IBudowniczyDomow) – abstrakcyjny interfejs implementowany przez poszczególnych rzeczywistych budowniczych, który służy do budowania finalnego produktu,
– konkretny, rzeczywisty budowniczy (BudowniczyDomowStandard oraz BudowniczyDomowPremium) – dostarcza implementację metodom budowniczego,
– klient, kierownik (Klient) – konstruuje obiekt z wykorzystaniem konkretnego budowniczego – klient dostarcza logikę, według której ma zostać utworzony przez budowniczego obiekt,
– produkt (domStandard, domPremium) – złożony obiekt.
Dwa najważniejsze elementy tego wzorca do konkretny, rzeczywisty budowniczy oraz klient, kierownik. Pierwszy z nich implementuję logikę budowania, którą z kolei dostarcza klient. Dzięki oddzieleniu logiki od implementacji możliwa jest zmiana sposobu budowania obiektu (logiki) w jednym miejscu i automatyczne zaimplementowanie tych zmian w całym systemie (każdy budowniczy przejmie tenże sposób).
Interfejs budowniczy ma zapewnić wspólny interfejs polimorficzny dla rzeczywistych budowniczych.
Budowniczy a Fabryka
Oba wzorce, zarówno omawiany tutaj Budowniczy jak i Fabryka, są wzorcami kreacyjnymi. Jeżeli przyjrzymy się powyższemu przykładowi, możemy dojść do wniosku, że są one do siebie bardzo podobne. Jednak podstawową różnicą jest to, że Budowniczy służy do kreacji skomplikowanych obiektów – stosowany powinien być wtedy, gdy instancji obiektu nie da się zahermetyzować w jednej funkcji i budowanie obiektu jest zbyt złożone nawet dla Fabryki Abstrakcyjnej. Dodatkową różnicą jest to, że fabryka tworzy instancję od razu a budowniczy – przekazuje nam ją na żądanie.
Inny rodzaj wzorca Budowniczy
W programowaniu korzysta się także z pewnej prostszej odmiany omawianego wzorca, nazywanej Statycznym Budowniczym.
Opiera się on na użyciu tzw. metody fluent interface. Charakteryzuje się ona tym, że każda metoda w danej klasie zwraca instancję klasy, w której się znajduje. Daje to nam wiele korzyści:
– jesteśmy w stanie budować skomplikowane obiekty be oddzielania logiki od implementacji,
– upraszczamy złożone konstruktory,
– możliwe jest tworzenie obiektów niemutowalnych.
Implementacja Statycznego Budowniczego może wyglądać następująco.
public class BudowniczyDomow {
Dom dom = new Dom();
public BudowniczyDomow montujOkna(String rodzaj) {
dom.okna.dodaj(rodzaj);
return this;
}
public BudowniczyDomow budujBasen(String rodzaj) {
dom.basen.dodaj(rodzaj);
return this;
}
public BudowniczyDomow tworzOgrod(String rodzaj) {
dom.ogrod.dodaj(rodzaj);
return this;
}
}
static void Main(String[] args) {
Dom dom = new BudowniczyDomow();
.montujOkna("Średnie");
.budujBasen("Mały");
.tworzOgrod("Duży");
}
Statyczny Budowniczy nie jest oficjalnym wzorcem projektowym, niekiedy jest także nazywany po prostu Budowniczy i tym samym mylony z oryginalnym wzorcem.