Wzorzec Budowniczy

(ang. Builder)

Jest popularnym wzorcem kreacyjnym. Buildera najczęściej używamy,  gdy dana klasa ma bardzo dużo pól i nie chcemy dla niej tworzyć dużej liczby konstruktorów.  Daje możliwość tworzenia złożonych obiektów etapami, krok po kroku.

</> Lepszy przykład niż wykład

✘ Błędny kod

Zgodnie z nazwą wzorca będziemy budować. Potraktujemy temat z rozmachem i zajmiemy się budową całych miast. Elementy naszych miast takie jak sklep, kościół, aquapark, szkoła itd. będą polami prywatnymi w klasie City. Natomiast różne miasta wybudujemy tworząc różne konstruktory. Do czytelnego wypisania składników naszych miast użyjemy metody toString().

public class City {
    private String shop;
    private String church;
    private String waterPark;
    private String school;
    private String busStation;
    private String postOffice;
    private String hospital;

    public City(String shop, String church, String waterPark, String school, String busStation, String postOffice, String hospital) {
        this.shop = shop;
        this.church = church;
        this.waterPark = waterPark;
        this.school = school;
        this.busStation = busStation;
        this.postOffice = postOffice;
        this.hospital = hospital;
    }

    public City(String church, String waterPark, String school, String hospital, String postOffice) {
        this.church = church;
        this.waterPark = waterPark;
        this.school = school;
        this.hospital = hospital;
        this.postOffice = postOffice;
    }

    public City(String shop, String church, String school) {
        this.shop = shop;
        this.church = church;
        this.school = school;
    }

    public String getShop() {
        return shop;
    }

    public String getChurch() {
        return church;
    }

    public String getWaterPark() {
        return waterPark;
    }

    public String getSchool() {
        return school;
    }

    public String getBusStation() {
        return busStation;
    }

    public String getPostOffice() {
        return postOffice;
    }

    public String getHospital() {
        return hospital;
    }

    @Override
    public String toString() {
        return "City{" +
                "shop='" + shop + '\'' +
                ", church='" + church + '\'' +
                ", waterPark='" + waterPark + '\'' +
                ", school='" + school + '\'' +
                ", busStation='" + busStation + '\'' +
                ", postOffice='" + postOffice + '\'' +
                ", hospital='" + hospital + '\'' +
                '}';
    }
}

Gdy rozpoczniemy budowę kolejnych miast w klasie Main, pojawiają się problemy:

  • istnieje spore ryzyko pomyłek przy wpisywaniu pól przekazywanych w konstruktorze np. ich kolejności
  • dla każdego typu miasta musielibyśmy mieć osobny konstruktor, finalnie kosmiczną ilość konstruktorów
public class Main {
    public static void main(String[] args) {
        City city1 = new City("sklep","kościół", "aquapark", "szkoła", "dworzec autobusowy", "poczta", "szpital");
        //City city2 = new City("?");
        //...
        //City city157 = new City("?");
        System.out.println(city1);
    }
}
✔ Prawidłowy kod

Istnieje kilka sposobów implementacji buildera. My skupimy się na dwóch:

1. Builder z klasą wewnętrzną

Tego buildera najczęściej używamy, gdy dana klasa ma bardzo dużo pól i nie chcemy dla niej tworzyć dużej liczby konstruktorów, a jednocześnie wolelibyśmy, by użytkownik nie miał dostępu do setterów w naszej klasie. Innymi słowy: chcemy zapewnić swobodę osobie, która będzie tworzyła obiekt pozwalając jej na ustawianie wartości wybranych pól, ale po utworzeniu obiektu nie chcemy, aby przy nim coś zmieniała.

Klasa City będzie posiadać:

  • pola prywatne
  • prywatny konstruktor zwracający nasze miasto, w nim przekażemy naszą klasę wewnętrzną CityBuilder. Wewnątrz konstruktora będą ustawione wartości wszystkich pól w naszym oryginalnym mieście. Ponieważ jest on prywatny, jeśli ktoś będzie chciał utworzyć instancję naszego domu, będzie musiał skorzystać z CityBuildera . Użyje wtedy dowolnej ilości metod w takiej kolejności, w jakiej będzie chciał (wybuduje wybrane przez siebie elementy miasta)
  • gettery
  • metody toString() do czytelnego wypisania tworzonych miast
  • publiczną statyczną klasę wewnętrzną CityBuilder, a w niej:
    takie same pola jak nasza klasa oryginalna City
    publiczne metody, do których będziemy przekazywali konkretne wartości naszych pól i każda z tych metod będzie zwracała CityBuilder(this). Dzięki temu będziemy mogli te metody chainować jedna po drugiej, zależnie od tego które pola będziemy chcieli inicjalizować
    po wywołaniu odpowiedniej ilości metod będziemy mogli ostatecznie wywołać publiczną metodę build(), która zwróci gotowe, utworzone miasto (nową instancję naszego miasta)
 public class City {
    private String shop;
    private String church;
    private String waterPark;
    private String school;
    private String busStation;
    private String postOffice;
    private String hospital;

    private City(CityBuilder cityBuilder) {
        this.shop = cityBuilder.shop;
        this.church = cityBuilder.church;
        this.waterPark = cityBuilder.waterPark;
        this.school = cityBuilder.school;
        this.busStation = cityBuilder.busStation;
        this.postOffice = cityBuilder.postOffice;
        this.hospital = cityBuilder.hospital;
    }

    public String getShop() {
        return shop;
    }

    public String getChurch() {
        return church;
    }

    public String getWaterPark() {
        return waterPark;
    }

    public String getSchool() {
        return school;
    }

    public String getBusStation() {
        return busStation;
    }

    public String getPostOffice() {
        return postOffice;
    }

    public String getHospital() {
        return hospital;
    }

    @Override
    public String toString() {
        return "City{" +
                "shop='" + shop + '\'' +
                ", church='" + church + '\'' +
                ", waterPark='" + waterPark + '\'' +
                ", school='" + school + '\'' +
                ", busStation='" + busStation + '\'' +
                ", postOffice='" + postOffice + '\'' +
                ", hospital='" + hospital + '\'' +
                '}';
    }

    public static class CityBuilder {
        private String shop;
        private String church;
        private String waterPark;
        private String school;
        private String busStation;
        private String postOffice;
        private String hospital;

        public CityBuilder buildShop(String shop) {
            this.shop = shop;
            return this;
        }

        public CityBuilder buildChurch(String church) {
            this.church = church;
            return this;
        }

        public CityBuilder buildWaterPark(String waterPark) {
            this.waterPark = waterPark;
            return this;
        }

        public CityBuilder buildSchool(String school) {
            this.school = school;
            return this;
        }

        public CityBuilder buildBusStation(String busStation) {
            this.busStation = busStation;
            return this;
        }

        public CityBuilder buildPostOffice(String postOffice) {
            this.postOffice = postOffice;
            return this;
        }

        public CityBuilder buildHospital(String hospital) {
            this.hospital = hospital;
            return this;
        }

        public City build() {
            return new City(this);
        }
    }
}

Klasa Main

Utworzymy nowe miasta korzystając z CityBuildera i jego gotowych metod, które służą do budowania poszczególnych elementów naszego miasta. Wybieramy tylko te metody, które chcemy wykorzystać. Wypisujemy je po kropce, a na końcu wywołujemy metodę build().

public class Main {
    public static void main(String[] args) {
        City city1 = new City.CityBuilder()
                .buildBusStation("dworzec autobusowy")
                .buildChurch("kościół")
                .build();
        System.out.println(city1);

        City city2 = new City.CityBuilder()
                .buildHospital("szpital")
                .buildShop("sklep")
                .buildWaterPark("aquapark")
                .build();
        System.out.println(city2);
    }
}

Po uruchomieniu metoda toString() wypisuje miasta zbudowane z wybranych przez nas elementów.

City{shop='null', church='kościół', waterPark='null', school='null', busStation='dworzec autobusowy', postOffice='null', hospital='null'}
City{shop='sklep', church='null', waterPark='aquapark', school='null', busStation='null', postOffice='null', hospital='szpital'}
2. Builder wersja klasyczna

Jeśli nie chcemy dawać możliwości ustawiania wartości na poszczególnych polach podczas tworzenia obiektu, to możemy implementować klasycznego buildera, w którym dyrektor będzie wiedział w jaki sposób skonstruować dany obiekt, a użytkownik (w znaczeniu programista z naszego zespołu) nie będzie miał się czym przejmować.

Klasa City zawiera:

  • pola prywatne odwzorowujące elementy miasta
  • gettery i settery do tych pól
  • metodę toString()
public class City {
    private String shop;
    private String church;
    private String waterPark;
    private String school;
    private String busStation;
    private String postOffice;
    private String hospital;

    public String getShop() {
        return shop;
    }

    public void setShop(String shop) {
        this.shop = shop;
    }

    public String getChurch() {
        return church;
    }

    public void setChurch(String church) {
        this.church = church;
    }

    public String getWaterPark() {
        return waterPark;
    }

    public void setWaterPark(String waterPark) {
        this.waterPark = waterPark;
    }

    public String getSchool() {
        return school;
    }

    public void setSchool(String school) {
        this.school = school;
    }

    public String getBusStation() {
        return busStation;
    }

    public void setBusStation(String busStation) {
        this.busStation = busStation;
    }

    public String getPostOffice() {
        return postOffice;
    }

    public void setPostOffice(String postOffice) {
        this.postOffice = postOffice;
    }

    public String getHospital() {
        return hospital;
    }

    public void setHospital(String hospital) {
        this.hospital = hospital;
    }

    @Override
    public String toString() {
        return "City{" +
                "shop='" + shop + '\'' +
                ", church='" + church + '\'' +
                ", waterPark='" + waterPark + '\'' +
                ", school='" + school + '\'' +
                ", busStation='" + busStation + '\'' +
                ", postOffice='" + postOffice + '\'' +
                ", hospital='" + hospital + '\'' +
                '}';
    }
}

Interfejs CityBuilder stanowiący główną część buildera klasycznego posiada:

  • deklaracje publicznych metod, które pozwalają na ustawienie wartości odpowiednich pól czyli budowania poszczególnych elementów miasta
  • deklarację metody getCity(), która zwróci gotowe, wybudowane miasto – obiekt typu City
public interface CityBuilder {
    void buildShop();
    void buildChurch();
    void buildWaterPark();
    void buildSchool();
    void buildBusStation();
    void buildPostOffice();
    void buildHospital();

    City getCity();
}

Dyrektor, u nas klasa CityDirector posiada:

  • jedno pole prywatne typu CityBuilder, które będzie przekazywane do niego w konstruktorze
  • konstruktor CityDirector, w którym będą przekazywane obiekty typu CityBuilder (obiekty te będą implementowały nasz interfejs CityBuilder)
  • publiczną metodę buildSmallCity(), która pozwala na ustawienie wartości odpowiednich pól (czyli budowanie poszczególnych elementów małego miasta) za pomocą CityBuildera
  • publiczną metodę buildBigCity(), która pozwala na ustawienie wartości odpowiednich pól (czyli budowanie poszczególnych elementów dużego miasta) za pomocą CityBuildera
  • publiczną metodę getCity() zwracającą gotowe wybudowane miasto
public class CityDirector {
    private CityBuilder cityBuilder;

    public CityDirector(CityBuilder cityBuilder) {
        this.cityBuilder = cityBuilder;
    }

    public void buildSmallCity() {
        cityBuilder.buildShop();
        cityBuilder.buildChurch();
        cityBuilder.buildSchool();
    }

    public void buildBigCity() {
        cityBuilder.buildShop();
        cityBuilder.buildChurch();
        cityBuilder.buildWaterPark();
        cityBuilder.buildSchool();
        cityBuilder.buildBusStation();
        cityBuilder.buildPostOffice();
        cityBuilder.buildHospital();
    }

    public City getCity() {
        return this.cityBuilder.getCity();
    }
}

Mamy teraz możliwość tworzenia dowolnej ilości konkretnych builderów (SmallCityBuilder, BigCityBuilder itd.), które będą implementowały nasz interfejs CityBuilder.

Konkretny Builder, u nas klasa SmallCityBuilder lub BigCityBuilder posiada:

  • prywatne pole typu City
  • publiczny konstruktor – tutaj nie będziemy przekazywać obiektu typu City, chcemy tylko stworzyć nową instancję tego obiektu
  • odpowiednią implementację metod z interfejsu CityBuilder, ustawiamy w nich konkretne wartości pól
  • implementację metody getCity() zwracającej gotowe miasto
public class SmallCityBuilder implements CityBuilder{
    private City city;

    public SmallCityBuilder() {
        this.city = new City();
    }

    @Override
    public void buildShop() {
        this.city.setShop("mały sklep");
    }

    @Override
    public void buildChurch() {
        this.city.setChurch("mały kościół");
    }

    @Override
    public void buildWaterPark() {
        this.city.setWaterPark("mały aquapark");
    }

    @Override
    public void buildSchool() {
        this.city.setSchool("mała szkoła");
    }

    @Override
    public void buildBusStation() {
        this.city.setBusStation("mały dworzec autobusowy");
    }

    @Override
    public void buildPostOffice() {
        this.city.setPostOffice("mała poczta");
    }

    @Override
    public void buildHospital() {
        this.city.setHospital("mały szpital");
    }

    @Override
    public City getCity() {
        return city;
    }
}

public class BigCityBuilder implements CityBuilder{
    private City city;

    public BigCityBuilder() {
        this.city = new City();
    }

    @Override
    public void buildShop() {
        this.city.setShop("Duży sklep");
    }

    @Override
    public void buildChurch() {
        this.city.setChurch("Duży kościół");
    }

    @Override
    public void buildWaterPark() {
        this.city.setWaterPark("Duży aquapark");
    }

    @Override
    public void buildSchool() {
        this.city.setSchool("Duża szkoła");
    }

    @Override
    public void buildBusStation() {
        this.city.setBusStation("Duży dworzec autobusowy");
    }

    @Override
    public void buildPostOffice() {
        this.city.setPostOffice("Duża poczta");
    }

    @Override
    public void buildHospital() {
        this.city.setHospital("Duży szpital");
    }

    @Override
    public City getCity() {
        return city;
    }
}

Klasa Main

Aby utworzyć obiekty typu City, w pierwszej kolejności musimy utworzyć instancje konkretnych Builderów (SmallCityBuilder i BigCityBuilder).
Następnie musimy utworzyć naszego Dyrektora (możemy go sobie wyobrazić jako dyrektora budowy), w konstruktorze przekazujemy mu konkretnego Buildera.
Potem musimy wybudować nasze miasta wywołując odpowiednio metody buildSmallCity() oraz buildBigCity() na dyrektorach.
Na końcu tworzymy instancje naszych miast wywołując metodę getCity() na dyrektorach.
Teraz możemy wypisać nasze miasta.

public class Main {
    public static void main(String[] args) {
        SmallCityBuilder smallCityBuilder = new SmallCityBuilder();
        BigCityBuilder bigCityBuilder= new BigCityBuilder();

        CityDirector smallCityDirector = new CityDirector(smallCityBuilder);
        smallCityDirector.buildSmallCity();

        CityDirector bigCityDirector = new CityDirector(bigCityBuilder);
        bigCityDirector.buildBigCity();

        City smallCity = smallCityDirector.getCity();
        City bigCity = bigCityDirector.getCity();

        System.out.println("Małe miasto: " + smallCity);
        System.out.println("Duże miasto: " + bigCity);
    }
}
Małe miasto: City{shop='mały sklep', church='mały kościół', waterPark='null', school='mała szkoła', busStation='null', postOffice='null', hospital='null'}
Duże miasto: City{shop='Duży sklep', church='Duży kościół', waterPark='Duży aquapark', school='Duża szkoła', busStation='Duży dworzec autobusowy', postOffice='Duża poczta', hospital='Duży szpital'}

Podsumowując:

  • builder z klasą wewnętrzną jest najpopularniejszą odmianą w Javie
  • w builderze w wersji klasycznej Dyrektor powinien decydować, które elementy budować (np. buildSmallCity(), buildBigCity()), natomiast konkretne Buildery definiują, jak je budować
  • buildera w wersji klasycznej często implementuje się też bez Dyrektora (uproszczony wariant)

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *