Budowniczy

Wzorce projektowe

  |   9 min czytania

Zastosowanie

Budowniczy (ang. Builder) (wzorzec kreacyjny) rozdziela sposób tworzenia obiektów od ich reprezentacji. Dzieli proces wytwórczy obiektu na etapy. Każdy z etapów może być zaimplementowany na wiele sposobów co umożliwia tworzenie różnych reprezentacji obiektów tej samej klasy. Dzięki zastosowaniu podejścia odwrócenia zależności zmiana sposobu tworzenia obiektów jest elastyczna i odbywa się niskim kosztem. Ponadto kod jest łatwiejszy w testowaniu, utrzymaniu, zapewnieniu obsługi wyjątków oraz zapobiega duplikacji. Wzorzec ten działa na podobnej zasadzie co plan budowy. Dyrektor odpowiedzialny za całą budowę zleca wykonanie poszczególnej pracy konkretnym Budowniczym. W efekcie połączenia ich pracy powstaje finalny Produkt. Mówiąc o wzorcu Budowniczy nie sposób nie wspomnieć o jego odmianie tzw. Fluent Builder. Jego rola sprowadza się do tworzenia obiektów (z dużą ilością parametrów) w czytelny sposób poprzez zastępienie konstruktora.

Ograniczenia

Stosowanie wzorca wymusza tworzenie konkretnego Budowniczego dla każdego typu Produktu co zwiększa ilość klas. Ponadto nie ma gwarancji inicjalizacji pól klasy, a Wstrzykiwanie Zależności może być utrudnione.

Użycie

Budowniczy używany jest w implementacji złożonych obiektów (kompozytów), które mogą być budowane na różne sposoby, a ich inicjalizacja jest procesem wieloetapowym. Zastosowanie wzorca pozwala uniknąć tworzenia super klasy o rozbudowanej odpowiedzialności. Wykorzystywany jest w inicjalizacji kompozytów w wielu bibliotekach zewnętrznych oraz także systemowych. W sytuacjach, gdy konstruktor klas ma długą listę parametrów (ok 5) należy rozważyć zastosowanie Fluent Builder.

Implementacja

W klasycznej wersji wzorca obiekt klasy Director zleca wykonanie produktu (Product) konkretnemu builderowi (ConcreteBuilder) nadzorując jego pracę. Director przechowuje referencje do Buildera i w momencie budowania produktu wywołuje na nim poszczególne operacje. Każdy budowniczy implementuje wspólne zachowania abstrakcyjnej klasy Builder lub interfejsu, tworząc produkt na swój określony sposób. Builder zawiera referencję do produktu i zwraca go nadzorcy w momencie zakończenia pracy.

Budowniczy diagram

Poniższy listing przedstawia implementacje klasycznej postaci wzorca Budowniczy.

public class Product {
	
    private String part1;
    private String part2;

    public void setPart1(String part1) {
    	this.part1 = part1;
    }

    public void setPart2(String part2) {
    	this.part2 = part2;
    }
}

public abstract class Builder {
    
    protected Product product;

    public Product getProduct() {
    	return product;
    }

    public abstract void buildPart1();
    public abstract void buildPart2();
}

public class ConcreteBuilder extends Builder {
    
    public void buildPart1() {
    	product.setPart1("oval");
    }

    public void buildPart2() {
    	product.setPart2("green");
    }
}

public class Director {
	
    private Builder builder;

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

    public Product build() {
        builder.buildPart1();
        builder.buildPart2();
        return builder.getProduct();
    }
}

Klient korzystając z nadzorcy wykorzystuje zaimplementowany wzorzec w następujący sposób.

Builder concreteBuilder = new ConcreteBuilder();
Director director = new Director(concreteBuilder);
Product product = director.build();

W wersji Fluent Builder, instancja klasy Product tworzona jest poprzez wywołanie statycznej klasy Builder. Stosując ten sposób tworzenia obiektów, należy zadbać o inicjalizacje wszystkich wymaganych pól oraz jej kolejność. Poniższy listing pokazuje podstawową implementacje wzorca w wariancie Fluent Builder.

public class Product {
    
    private String part1;
    private String part2;

    private Product(Builder builder) {
        this.part1 = builder.part1;
        this.part2 = builder.part2;
    }

    public static class Builder {
        
        private String part1;
        private String part2;

        public Builder part1(String part1) {
            this.part1 = part1;
            return this;
        }
        
        public Builder part2(String part2) {
            this.part2 = part2;
            return this;
        }

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

Dzięki takiemu zabiegowi zamiast tworzyć obiekt poprzez wywołanie jego konstruktora można posłużyć się jego budowniczym.

Product product = new Product.Builder()
        .part1("oval")
        .part2("green")
        .build();

Przykład

Aplikacja wspomaga pracowników sieci gastronomicznej w przyjmowaniu i wydawaniu zamówień. Pracownik obsługi Staff wybiera na ekranie wskazane przez klienta zdefiniowane zestawy obiadowe Meal. Sztab kucharzy Cook w którym każdy specjalizuje się w konkretnym daniu otrzymuje listę zamówień do realizacji. Po przygotowaniu posiłku przekazuje go do obsługi, a stamtąd trafia ono do klienta. Poniższy listing przedstawia sposób realizacji składania zamówień przy użyciu Budowniczego.

public class Meal {

    private Burger burger;
    private Beverage beverage;
    private Extra extra;

    public void addBurger(Burger burger) {
        this.burger = burger;
    }

    public void addBeverage(Beverage beverage) {
        this.beverage = beverage;
    }

    public void addExtra(Extra extra) {
        this.extra = extra;
    }
}

public abstract class Cook {

    protected Meal meal;

    public Meal getMeal() {
        return meal;
    }

    public abstract void prepareBurger();
    public abstract void prepareBeverage();
    public abstract void prepareExtra();
}

public class BigMeal extends Cook {

    @Override
    public void prepareBurger() {
        meal.addBurger(new Burger("Bacon Burger"));
    }

    @Override
    public void prepareBeverage() {
        meal.addBeverage(new Beverage("Lager Beer"));
    }

    @Override
    public void prepareExtra() {
        meal.addExtra(new Extra("French fries"));
    }
}

public class KidsMeal extends Cook {

    @Override
    public void prepareBurger() {
        meal.addBurger(new Burger("Cheeseburger"));
    }

    @Override
    public void prepareBeverage() {
        meal.addBeverage(new Beverage("Orange Juice"));
    }

    @Override
    public void prepareExtra() {
        meal.addExtra(new Extra("Apple"));
    }
}

public class Staff {

    private Cook builder;

    public Staff(Cook builder) {
        this.builder = builder;
    }

    public Meal makeMeal() {
        builder.prepareBurger();
        builder.prepareBeverage();
        builder.prepareExtra();
        return builder.getMeal();
    }
}

Dodatkowo klient może skomponować swój własny zestaw. W tym celu pracownik obsługi ręcznie wybiera produkty. Poniższy listing przedstawia implementację manualnego tworzenia zestawu przy użyciu Fluent Builder.

public class SpecificMeal {

    private Burger burger;
    private Beverage beverage;
    private Extra extra;

    private SpecificMeal(Chef chef) {
        this.burger = chef.burger;
        this.beverage = chef.beverage;
        this.extra = chef.extra;
    }

    public static class Chef {

        private Burger burger;
        private Beverage beverage;
        private Extra extra;

        public Chef prepareBurger(Burger burger) {
            this.burger = burger;
            return this;
        }
        
        public Chef prepareBeverage(Beverage beverage) {
            this.beverage = beverage;
            return this;
        }
        
        public Chef prepareExtra(Extra extra) {
            this.extra = extra;
            return this;
        }

        public SpecificMeal makeMeal() {
            return new SpecificMeal(this);
        }
    }
}

Proces wyboru zestawów wygląda następująco.

Staff john = new Staff(new BigMeal());
Meal definedMeal = john.makeMeal();
SpecificMeal specificMeal = new SpecificMeal.Chef()
        .prepareBurger(new Burger("Double Cheesburger"))
        .prepareBeverage(new Beverage("Cola"))
        .prepareExtra(new Extra("Onion Rings"))
        .makeMeal();
//give meals to client

Biblioteki

Wiele bibliotek zewnętrznych jak np.: Retrofit czy elementów systemu np.: AlertDialog, Notification korzysta z Budowniczego. Jednakże do samej implementacji wzorca przeważnie nie używa się bibliotek. Metoda append klas StringBuilder oraz StringBuffer mogą być przykładem realizacji wzorca Budowniczy w Javie.