Wzorce projektowe #3 – Budowniczy

builder
Wydawać by się mogło, że tworzenie nowych obiektów to nic trudnego – wystarczy użyć operatora „new”. Co zrobić w przypadku, gdy proces tworzenia obiektu przebiega w wielu etapach, a sam obiekt składa się z kilku mniejszych podobiektów? Odpowiedź brzmi: wykorzystać wzorce projektowe. A konkretnie – wzorzec Budowniczy.

Trochę teorii

Wzorzec Budowniczy jest wykorzystywany w celu hermetyzacji algorytmu tworzenia produktu oraz jego wieloetapowej inicjalizacji. Budowniczy należy do grupy wzorców kreacyjnych (konstrukcyjnych), których zadaniem jest tworzenie oraz konfiguracja obiektów.

Problem projektowy – budowniczy domów

Skoro wzorzec nazywa się Budowniczy, to wcielmy się w rolę takiego budowniczego. Chcemy napisać program umożliwiający budowę domów. Zakładamy, że chcemy budować dwa rodzaje domów: ekonomiczny oraz klasy premium. Każdy z domów, niezależnie od rodzaju, będzie tym samym obiektem składającym się z podłogi, ścian i dachu. Dom premium będzie dodatkowo zawierał garaż. Poszczególne rodzaje domów różnić się będą od siebie pewnymi szczegółami:
  • dom ekonomiczny posiada podłogę z linoleum, ściany drewniane i dach z papy,
  • dom premium posiada podłogę w postaci parkietu, ściany z cegły i dach z dachówek ceramicznych.
Każdy z domów, ze względu na inne zastosowane materiały, będzie budowany na nieco inny sposób. Poniżej przykład kodu umożliwiającego tworzenie domów:
House house = new House();

if (houseType == "ekonomiczny") {
    Floor floor = new Floor("linoleum");
    house.floor = floor;

    WallSystem wallSystem = new WallSystem("drewno");
    house.wallSystem = wallSystem;

    Roof roof = new Roof("papa");
    house.roof = roof;
    
} else if (houseType == "premium") {
    Floor floor = new Floor("parkiet");
    house.floor = floor;

    WallSystem wallSystem = new WallSystem("cegła");
    house.wallSystem = wallSystem;

    Roof roof = new Roof("dachówka ceramiczna");
    house.roof = roof;

    Garage garage = new Garage();
    house.garage = garage;
} 
Tego typu kod będzie sprawiał wiele problemów, m.in.:
  • kod jest mało czytelny,
  • tego typu nieuporządkowany kod może generować błędy w przypadku jego rozbudowy,
  • tworzenie obiektów nie jest odseparowane od kodu klienta.
Zaimplementowanie wzorca Budowniczy wyeliminuje wszystkie te niedogodności

Zastosowanie wzorca Budowniczy w praktyce

Strukturę wzorca Budowniczy przedstawia poniższy diagram:
Na strukturę wzorca Budowniczy składają się 4 podstawowe elementy:
  • Product, będący klasą zawierającą obiekt tworzony przez wzorzec. Gotowy produkt może być pojedynczym obiektem, może też być kompozytem złożonym z mniejszych podobiektów,
  • Interfejs Builder, w którym zadeklarowane są wszytkie metody tworzące gotowy produkt,
  • Klasy ConcreteBuilder implementujące interfejs Builder. Klasy te są odpowiedzialne za stworzenie konkretnej wersji produktu końcowego,
  • klasa Director, która wydaje polecenie zbudowania produktu przy użyciu konkretnego budowniczego.

Diagram UML budowniczego domów

Po zaimplementowaniu wzorca Budowniczy, diagram naszego programu przedstawia się tak:
UML
Klasa Director oddelegowuje budowanie obiektów do konkretnych budowniczych, czyli EconomicHouseBuilder i PremiumHouseBuilder.
Klasy konkretnych budowniczych tworzą obiekt klasy House, a następnie wszystkie podobiekty. Każdy z podobiektów otrzymuje od konkretnego budowniczego określony typ, zależny od tego jaki rodzaj domu chcemy wybudować.

Implementacja wzorca Budowniczy

public class Director {
    private HouseBuilder builder;

    public Director(HouseBuilder builder) {
        this.builder = builder;
    }

    public void buildHouse() {
        builder.buildFloor();
        builder.buildWallSystem();
        builder.buildRoof();
        builder.buildGarage();
    }

    public House getHouse() {
        return builder.getHouse();
    }
} 
Klasa Director to główna klasa, z którą się komunikuje klient. Director zawiera referencję do obiektu budowniczego i do niego deleguje odpowiedzialność za utworzenie obiektu. Budowanie obiektu realizuje metoda buildHouse. Mamy tu też metodę getHouse zwracającą gotowy już obiekt.
public interface HouseBuilder {
    public void buildFloor();
    public void buildWallSystem();
    public void buildRoof();
    public void buildGarage();

    public House getHouse();
}

public class EconomicHouseBuilder implements HouseBuilder {
    private House house;

    public EconomicHouseBuilder() {
        house = new House();
        house.setType("ekonomiczny");
    }

    @Override
    public void buildFloor() {
        house.setFloor(new Floor("linoleum"));
    }

    @Override
    public void buildWallSystem() {
        house.setWallSystem(new WallSystem("drewno"));
    }

    @Override
    public void buildRoof() {
        house.setRoof(new Roof("papa"));
    }

    @Override
    public void buildGarage() {}

    @Override
    public House getHouse() {
        return house;
    }
}

public class PremiumHouseBuilder implements HouseBuilder {
    private House house;

    public PremiumHouseBuilder() {
        house = new House();
        house.setType("premium");
    }

    @Override
    public void buildFloor() {
        house.setFloor(new Floor("parkiet"));
    }

    @Override
    public void buildWallSystem() {
        house.setWallSystem(new WallSystem("cegła"));
    }

    @Override
    public void buildRoof() {
        house.setRoof(new Roof("dachówka ceramiczna"));
    }

    @Override
    public void buildGarage() {
        house.setGarage(new Garage());
    }

    @Override
    public House getHouse() {
        return house;
    }
} 
Tutaj mamy interfejs HouseBuilder i jego implementacje odpowiedzialne za tworzenie domu konkretnego typu. Każdy z konkretnych budowniczych tworzy nowy obiekt House, a następnie implementuje wszystkie metody interfejsu.
Obiekt garaż występuje tylko w domu budowanym przez budowniczego PremiumHouseBuilder. Z tego względu metoda buildGarage w tej implementacji tworzy nowy obiekt klasy Garage. Natomiast dom ekonomiczny nie posiada garażu, dlatego metoda buildGarage w klasie EconomicHouseBuilder jest pusta.
public class House {
    private String type;
    private Floor floor;
    private WallSystem wallSystem;
    private Roof roof;
    private Garage garage;

    public String getType() {
        return type;
    }

    public String getFloorType() {
        return floor.getType();
    }

    public String getWallSystemType() {
        return wallSystem.getType();
    }

    public String getRoofType() {
        return roof.getType();
    }

    public String isGarage() {
        if (this.garage != null) {
            return "Dom posiada garaż";
        }
        return "Dom nie posiada garażu";
    }

    public void setType(String type) {
        this.type = type;
    }

    public void setFloor(Floor floor) {
        this.floor = floor;
    }

    public void setWallSystem(WallSystem wallSystem) {
        this.wallSystem = wallSystem;
    }

    public void setRoof(Roof roof) {
        this.roof = roof;
    }

    public void setGarage(Garage garage) {
        this.garage = garage;
    }
}
 
Home to klasa produktu końcowego, jakim jest gotowy dom. Jak widać, każdy z domów składa się z podłogi, ścian i dachu. Mamy tu też pole type zawierające informację o typie domu. Poniżej mamy gettery i settery poszczególnych podobiektów obiektu dom.
public class Floor {
    private String type;

    public Floor(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
} 
Klasa Floor reprezentuje obiekt podłogi. Mamy pole zawierające typ podłogi oraz metodę zwracającą typ.
Klasy WallSystem i Roof wyglądają analogicznie do klasy Floor. Natomiast klasa Garage nie posiada żadnych pól ani funkcji.
public class HouseBuilderTest {
    public static void main(String[] args) {
        HouseBuilder premiumHouseBuilder = new PremiumHouseBuilder();
        Director director = new Director(premiumHouseBuilder);

        director.buildHouse();

        House house = director.getHouse();

        System.out.println("Dom: " + house.getType());
        System.out.println("Podłogi: " + house.getFloorType());
        System.out.println("Ściany: " + house.getWallSystemType());
        System.out.println("Dach: " + house.getRoofType());
        System.out.println(house.isGarage());
    }
} 
Pora przetestować naszego budowniczego domów. Tworzymy obiekt konkretnego budowniczego oraz dyrektora. Do konstruktora dyrektora przesyłamy uprzednio utworzonego budowniczego. Następnie budujemy dom metodą buildHouse i zwracamy gotowy produkt metodą getHouse. Na końcu użyjemy getterów obiektu dom do wyświetlenia parametrów gotowego produktu.
result

Podsumowanie

Wzorzec Budowniczy jest nieodzownym narzędziem w przypadku wieloetapowej inicjalizacji obiektu. Nieczytelny kod złożony z wielu rozbudowanych instrukcji warunkowych można uprościć to jednej metody. Uproszczona jest także rozbudowa struktury o nowe sposoby inicjalizacji obiektu – wystarczy dodać nowego konkretnego budowniczego.
Początkowo wzorzec Budowniczy może się wydawać dosyć trudny i skomplikowany, ale po jego poznaniu potrafi on znacznie poprawić jakość pisanych przez nas programów.

Znajdź mnie również na:

arrow