Wzorzec projektowy – Builder

Czym jest builder?

Najprościej wytłumaczyć zasadę buildera przedstawiając problem oraz w jaki sposób ten wzorzec go rozwiązuje, a więc przejdźmy do przykładu.

Załóżmy, że chcemy zbudować dowolny samochód. Na potrzeby przykładu skorzystamy z języka Java, a więc standardowo zaczynamy z naszym Mainem.

public class Main {
    public static void main(String[] args) {}
}

Dodajemy klasę Car reprezentującą nasz samochód, oraz ustalamy z czego ten samochód będzie się składał dodając od razu gettery i settery.

package car;

public class Car {

    private String doors;
    private String wheels;
    private String steeringWheel;
    private String windows;
    private String engine;
    private String radio;
    private String lighter;
    private String cupHolder;

    public Car(String doors, String wheels, String steeringWheel, String windows, 
               String engine, String radio, String lighter, String cupHolder) {
        this.doors = doors;
        this.wheels = wheels;
        this.steeringWheel = steeringWheel;
        this.windows = windows;
        this.engine = engine;
        this.radio = radio;
        this.lighter = lighter;
        this.cupHolder = cupHolder;
    }

    public Car(String doors, String wheels, String steeringWheel, String windows, 
               String engine, String radio) {
        this.doors = doors;
        this.wheels = wheels;
        this.steeringWheel = steeringWheel;
        this.windows = windows;
        this.engine = engine;
        this.radio = radio;
    }

    public Car(String doors, String wheels, String steeringWheel, String windows, 
               String engine) {
        this.doors = doors;
        this.wheels = wheels;
        this.steeringWheel = steeringWheel;
        this.windows = windows;
        this.engine = engine;
    }

    public String getDoors() {
        return doors;
    }

    public void setDoors(String doors) {
        this.doors = doors;
    }

    public String getWheels() {
        return wheels;
    }

    public void setWheels(String wheels) {
        this.wheels = wheels;
    }

    public String getSteeringWheel() {
        return steeringWheel;
    }

    public void setSteeringWheel(String steeringWheel) {
        this.steeringWheel = steeringWheel;
    }

    public String getWindows() {
        return windows;
    }

    public void setWindows(String windows) {
        this.windows = windows;
    }

    public String getEngine() {
        return engine;
    }

    public void setEngine(String engine) {
        this.engine = engine;
    }

    public String getRadio() {
        return radio;
    }

    public void setRadio(String radio) {
        this.radio = radio;
    }

    public String getLighter() {
        return lighter;
    }

    public void setLighter(String lighter) {
        this.lighter = lighter;
    }

    public String getCupHolder() {
        return cupHolder;
    }

    public void setCupHolder(String cupHolder) {
        this.cupHolder = cupHolder;
    }
}

Stworzyliśmy naszą klasę Car razem z trzema konstruktorami pozwalającymi na stworzenie trzech różnych rodzajów aut:

  • pierwszy konstruktor – wersja premium nasze auto zawiera wszystkie możliwe elementy
  • drugi konstruktor – wersja z radiem
  • trzeci konstruktor – wersja bez dodatkowych niepotrzebnych elementów

Stwórzmy sobie teraz przykładowe samochody w Mainie:

import car.Car;

public class Main {
    public static void main(String[] args) {

        Car car1 = new Car("doors", "wheels", "steeringWheel", "windows", "engine", "radio",   
                           "lighter", "cupHolder");
        Car car2 = new Car("doors", ...);
        
    }
}

Udało nam się stworzyć pierwszy wariant samochodu, ale nie pamiętamy z czego składał się drugi konstruktor ani tym bardziej trzeci.

Problem

Mimo prostego przykładu naszego problemu i zaledwie trzech konstruktorach, zbudowanie obiektu jest strasznie nieporęczne, a przy bardziej złożonych klasach może pojawić się tych konstruktorów dużo więcej, dodatkowo nie wiadomo które pola są wymagane do przekazania w celu stworzenia obiektu.

Jak builder nam w tym pomoże?

Zmierzmy się z tym samym wyzwaniem stosując wzorzec projektowy builder. Zbudujmy sobie kilka samochodów, jak byśmy to zaimplementowali?

Startujemy z czystym Mainem:

public class Main {
    public static void main(String[] args) {}
}

Budujemy klasę Car, bez implementacji setterów:

package car;

public class Car {

    private String doors;
    private String wheels;
    private String steeringWheel;
    private String windows;
    private String engine;
    private String radio;
    private String lighter;
    private String cupHolder;


    public String getDoors() {
        return doors;
    }

    public String getWheels() {
        return wheels;
    }

    public String getSteeringWheel() {
        return steeringWheel;
    }

    public String getWindows() {
        return windows;
    }

    public String getEngine() {
        return engine;
    }

    public String getRadio() {
        return radio;
    }

    public String getLighter() {
        return lighter;
    }

    public String getCupHolder() {
        return cupHolder;
    }

}

Oraz tworzymy klasę wewnętrzną CarBuilder która pomorze nam w tworzeniu tego jak ma wyglądać nasz samochód:

package car;

public class Car {

    private String doors;
    private String wheels;
    private String steeringWheel;
    private String windows;
    private String engine;
    private String radio;
    private String lighter;
    private String cupHolder;
    

    public String getDoors() {
        return doors;
    }

    public String getWheels() {
        return wheels;
    }

    public String getSteeringWheel() {
        return steeringWheel;
    }

    public String getWindows() {
        return windows;
    }

    public String getEngine() {
        return engine;
    }

    public String getRadio() {
        return radio;
    }

    public String getLighter() {
        return lighter;
    }

    public String getCupHolder() {
        return cupHolder;
    }

    public static class CarBuilder() {

        private String doors;
        private String wheels;
        private String steeringWheel;
        private String windows;
        private String engine;
        private String radio;
        private String lighter;
        private String cupHolder;

        public CarBuilder installDoors(String doors) {
            this.doors = doors;
            return this;
        }

        public CarBuilder installWheels(String wheels) {
            this.wheels = wheels;
            return this;
        }

        public CarBuilder installSteeringWheel(String steeringWheel) {
            this.steeringWheel = steeringWheel;
            return this;
        }

        public CarBuilder installWindows(String windows) {
            this.windows = windows;
            return this;
        }

        public CarBuilder installRadio(String radio) {
            this.radio = radio;
            return this;
        }

        public CarBuilder installLighter(String lighter) {
            this.lighter = lighter;
            return this;
        }

        public CarBuilder installCupHolder(String cupHolder) {
            this.cupHolder = cupHolder;
            return this;
        }

        public CarBuilder installEngine(String engine) {
            this.engine = engine;
            return this;
        }

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

Metody z przedrostkiem install które zastosowaliśmy służą do osobnego montowania poszczególnych części naszego samochodu, używamy ich dowolnie, wybierając te które pozwolą nam zbudować wymyślony przez nas rodzaj samochodu. Dodatkowo mamy metodę build którą użyjemy jak już zakończymy składanie samochodu w całość.

Zanim przejdziemy do Maina musimy jeszcze poruszyć kwestię konstruktora:

package car;

public class Car {

    private String doors;
    private String wheels;
    private String steeringWheel;
    private String windows;
    private String engine;
    private String radio;
    private String lighter;
    private String cupHolder;

    private Car(CarBuilder carBuilder) {
        this.doors = carBuilder.doors;
        this.wheels = carBuilder.wheels;
        this.steeringWheel = carBuilder.steeringWheel;
        this.windows = carBuilder.windows;
        this.engine = carBuilder.engine;
        this.radio = carBuilder.radio;
        this.lighter = carBuilder.lighter;
        this.cupHolder = carBuilder.cupHolder;
    }

    public String getDoors() {
        return doors;
    }

    public String getWheels() {
        return wheels;
    }

    public String getSteeringWheel() {
        return steeringWheel;
    }

    public String getWindows() {
        return windows;
    }

    public String getEngine() {
        return engine;
    }

    public String getRadio() {
        return radio;
    }

    public String getLighter() {
        return lighter;
    }

    public String getCupHolder() {
        return cupHolder;
    }

    public static class CarBuilder() {

        private String doors;
        private String wheels;
        private String steeringWheel;
        private String windows;
        private String engine;
        private String radio;
        private String lighter;
        private String cupHolder;

        public CarBuilder installDoors(String doors) {
            this.doors = doors;
            return this;
        }

        public CarBuilder installWheels(String wheels) {
            this.wheels = wheels;
            return this;
        }

        public CarBuilder installSteeringWheel(String steeringWheel) {
            this.steeringWheel = steeringWheel;
            return this;
        }

        public CarBuilder installWindows(String windows) {
            this.windows = windows;
            return this;
        }

        public CarBuilder installRadio(String radio) {
            this.radio = radio;
            return this;
        }

        public CarBuilder installLighter(String lighter) {
            this.lighter = lighter;
            return this;
        }

        public CarBuilder installCupHolder(String cupHolder) {
            this.cupHolder = cupHolder;
            return this;
        }

        public CarBuilder installEngine(String engine) {
            this.engine = engine;
            return this;
        }

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

Uczyniliśmy konstruktor prywatnym aby użytkownik musiał się posłużyć naszym CarBuilderem, chcąc stworzyć swój samochód. Jak możemy zauważyć pozbyliśmy się multum zbędnych konstruktorów które komplikowały nam poprzedni przykład.

Teraz jedyne co musimy zrobić to przekazać te elementy które chcemy mieć w samochodzie do naszego CarBuildera, nie przejmując się ilością przekazywanych argumentów ani tym bardziej ich kolejnością w konstruktorze!

Zobaczmy jak to wygląda w praktyce, tworząc w Mainie nasz samochód:

import car.Car;

public class Main {
    public static void main(String[] args) {

        Car car = new Car.CarBuilder()
                .installEngine("engine")
                .installWheels("wheels")
                .installDoors("doors")
                .installWindows("windows")
                .installSteeringWheel("steeringWheel")
                .installRadio("radio")
                .build();
                
    }
}

Nic prostszego, wybieramy co chcemy zamontować, a gdy już skończymy wywołujemy metodę build() i gotowe!

W naszym przykładzie użyliśmy tzw. ,,buildera z klasą wewnętrzną”.

Zalety:

  • Zapewnia kontrolę nad tworzonym obiektem
  • Przy dużej ilości atrybutów klasy pozwala na większą przejrzystość i zrozumiałość kodu

Wady:

  • Powtórzenie kodu – konieczność przekopiowania atrybutów klasy Car do CarBuildera