Patterns: Builder (Будівельник) 2


Наступний представник твірних патернів може добряче винести мозок, особливо якщо намагатися зрозуміти його натверезо, як це робив я. А все тому, що новачку деякі його властивості просто неочевидні. Ми ж спробуємо пояснити що таке патерн “будівельник” (або ж патерн “будівник”) на простому алкогольному прикладі, а саме приготування коктейлю. Але спочатку для загального розуміння копінаста з вікіпедії. Отож, патерн “будівельник” відокремлює конструювання складного об’єкта від його подання, таким чином у результаті одного й того ж процесу конструювання можуть бути отримані різні подання.

Патерн Будівельник

А тепер поговоримо про коктейлі. Отож уявімо собі якийсь бар “Блакитний Місяць”, де бармен готує різноманітні коктейлі. Ну нехай середньостатиситичний коктейль готується за наступним принципом: беремо стакан\келих\бокал\невеличкий банячок, заливаємо туди якусь основну частину коктейлю (у нашому барі це переважно горілка), додаємо 3-5 різних алкогольних інгредієнтів, потім ще всякі м’яти-лайми і , звісно ж, соломинку (всім відомо, що як п’эш через соломинку, то більше “вставляє”). Давайте подивимося як виглядає процес приготування коктейлю “Бурий ведмідь”поки без всяких білдерів. Спочатку подивимося що ж входить до складу коктейлю:

public class BrownBear implements Cocktail {
    private String glass; //Стакан
    private String mainAlco; //Основна частина 🙂
    private String additionalAlco1; //Всяке бухло
    private String additionalAlco2;
    private String additionalAlco3;
    private String additionalAlco4;
    private String additionalAlco5;
    private String additionalIngredient1; //Лайми-кориці
    private String additionalIngredient2;
    private String pipe; //Трубочка
    private String beautifier; // Інші гламурні прикраси
}

Ну от, а тепер подумаємо як же все таки проготувати коктейль. Найпростіший варіант – зробити конструктор, у який передавати всі необхідні параметри:

Cocktail cocktail = new BrownBear("Гранчак", "Горілка", "коньяк", "пивасик", "", "", "", "лимон", "", "нафіг трубочки", "занюхати рукавом");

Ой, та це ж страшно! По-перше, “Бурий Ведмідь” потребує лише 2 алкогольні інгредієнти додатково до горілки, а з всяких неалкогольних додатків туди підійде максимум що лимон – маємо вже порожні поля у конструкторі; по-друге, мені лікар заборонив вживати пиво, і тому я завжди замовляю “лайтову” версію коктейлю, а от один між дружбан має алергію на лимони, зате коньяку посить подвійну порцію. В принципі, ми можемо зробити кілька конструкторів і просто викликати потрібний, але тоді це вже буде не бар, а , перепрошую, якесь конструкторське бюро.

А давайте тоді зробимо інакше: всім наливатимемо якусь “базову” версію коктейлю з мінімальним необхідним набором складників (навряд чи стакан горілки можна вважати коктейлем) а все інше додаватимемо за бажанням (для цього треба передбачити набір методів сетерів і гетерів)? Десь отак:

public BrownBear() {
        this.glass = "Гранчак";
        this.mainAlco = "Горілка";
        this.additionalAlco1 = "Коньяк";
        this.pipe = "Нафіг трубочки";
        this.beautifier = "занюхати рукавом";

        public String getAdditionalAlco1() {
                 return additionalAlco1;
        }

        public void setAdditionalAlco1(String additionalAlco1) {
                 this.additionalAlco1 = additionalAlco1;
        }
}

public class BlueMoon {
    public static void main(String[] args) {
        BrownBear cocktail = new BrownBear();
        cocktail.setAdditionalAlco1("Пиво");
        cocktail.setAdditionalIngredient1("Лимончик");
    }
}

А що? Все б нічого, якби не велика плахта коду у методі main, але і з цим можна дати раду, якщо хоча б просто все оте додавання інгредієнтів винести у окремий метод буде тоді якось отак:

public class BlueMoon {
    public static void main(String[] args) {
        BrownBear cocktail = new BrownBear();
        bearify(cocktail);
    }

    private static void bearify(BrownBear cocktail) {
        cocktail.setAdditionalAlco1("Пиво");
        cocktail.setAdditionalIngredient1("Лимончик");
    }
}

І всі щасливі без всяких будівельників! Було би, якби не невеличке западло: поки готовий коктейль несли Петровичу, хтось взяв, та й підмішав туди касторки 🙂

cocktail.setAdditionalIngredient2("Касторка"); //На тобі, знатимеш як не віддавати позичену "до зарплати" двадцятку

З цим треба якось боротися, бо у кого ж ворогів нема? Було би добре, аби коктейль подавався у якійсь закритій пляшці з пломбою, чи що, аби ніхто вже не міг після приготування туди щось додати. І забрати, бо я чув, що у сусідньому закладі просто зі стаканів вже й лимони пропадають! Треба би якогось офіціанта-бодібілдера, щоб відсіч давав, або просто білдер коктейлів реалізувати, який готував би строго за рецептом, свято беріг і все таке.

Щоб цього досягти ми зробимо білдер внутрішнім класом нашого коктейлю:

public class BrownBear implements Cocktail {

    public BrownBear() {
        this.glass = "Гранчак";
        this.mainAlco = "Горілка";
        this.additionalAlco1 = "Коньяк";
        this.pipe = "Нафіг трубочки";
        this.beautifier = "занюхати рукавом";
    }

    private String glass; //Стакан
    private String mainAlco; //Основна частина 🙂
    private String additionalAlco1; //Всяке бухло
    private String additionalAlco2;
    private String additionalAlco3;
    private String additionalAlco4;
    private String additionalAlco5;
    private String additionalIngredient1; //Лайми-кориці
    private String additionalIngredient2;
    private String pipe; //Трубочка
    private String beautifier; // Інші гламурні прикраси

    public String getAdditionalAlco1() {
        return additionalAlco1;
    }

    private void setAdditionalAlco1(String additionalAlco1) {
        this.additionalAlco1 = additionalAlco1;
    }

    public String getAdditionalAlco2() {
        return additionalAlco2;
    }

    private void setAdditionalAlco2(String additionalAlco2) {
        this.additionalAlco2 = additionalAlco2;
    }

    public String getAdditionalAlco3() {
        return additionalAlco3;
    }

    private void setAdditionalAlco3(String additionalAlco3) {
        this.additionalAlco3 = additionalAlco3;
    }

    public static class BrownBearBuilder {
        protected BrownBear bb;

        public BrownBear serveBearCocktail() { return bb; }
        public void createNewBrownBear() { bb = new BrownBear(); }

        public void addAclo1(String alco1){bb.setAdditionalAlco1(alco1);};
        public void addAclo2(String alco2){bb.setAdditionalAlco2(alco2);};
        public void addAclo3(String alco3){bb.setAdditionalAlco3(alco3);};

    }
}

І робитимемо коктейль наступним чином:

public class BlueMoon {
    public static void main(String[] args) {
        BrownBear.BrownBearBuilder bbb = new BrownBear.BrownBearBuilder();
        bbb.createNewBrownBear();
        bbb.addAclo1("Пиво");
        BrownBear bb = bbb.serveBearCocktail();
    }
}

І тепер уже не вийде нічого підмішати у благородний напій, а ще краще якщо взяти і змінити всі “будівельні” методи так, аби кожен з них повертав екземпляр самого білдера:

public static class BrownBearBuilder {
protected BrownBear bb;

public BrownBear serveBearCocktail() { return bb; }

public BrownBearBuilder createNewBrownBear() { bb = new BrownBear(); return this;}
public BrownBearBuilder addAclo1(String alco1){bb.setAdditionalAlco1(alco1); return this;}
public BrownBearBuilder addAclo2(String alco2){bb.setAdditionalAlco2(alco2); return this;}
public BrownBearBuilder addAclo3(String alco3){bb.setAdditionalAlco3(alco3); return this;}

}

І тоді можна зробити отак:

public class BlueMoon {
    public static void main(String[] args) {
        BrownBear bb = new BrownBear.BrownBearBuilder()
                .createNewBrownBear()
                .addAclo1("Beer")
                .serveBearCocktail();
    }
}

Інше застосування білдера може знадобитися якщо ми маємо потребу готвувати кілька коктейлів за наперед заданими рецептами (це класичний приклад із GoF, який якраз відповідає UML-діаграмі на початку статті), у цьому випадку у гру вступає ще один персонаж: director (у нашому випадку бармен), який і займається приготуванням коктейлів відповідно до конкретних рецептів (кожен рецепт – це окремий білдер). Оскільки напихати 100500 внутрішніх білдерів у кожен коктейль це дико, то від внутрішнього класу доведеться відмовитися і зробити білдер окремим класом, а щоб забезпечити незмінність коктейлю після приготування просто покладемо всі рецепти і коктейлі у окремий пакет, а всім сетерам задамо модифікатори доступу default або protected (раптом ми схочемо ще й наслідувати рецепти):

package stuff;
public abstract class CocktailFormula {
    //Абстрактний білдер (тут мені не подобаються void методи: думаю що повертати варто сам коктейль)
    protected Cocktail cocktail;
    public Cocktail serveCocktail() { return cocktail; }
    public void createCocktail() { cocktail = new Cocktail(); }
    public abstract void setAlco1(String alco1);
}

package stuff;
public class BrownBearFormula extends CocktailFormula {
    // Одна з імплементацій
    public void setAlco1(String alco1) {
        cocktail.setAdditionalAlco1("Коньяк");
    }
    //Тут ще методи для побудови
}

package stuff;
public class Waiter { //Бармен 🙂
    private CocktailFormula cocktailBuilder;
    public void setBuilder(CocktailFormula cocktailBuilder) { this.cocktailBuilder = cocktailBuilder; }
    public Cocktail getCocktail() { return cocktailBuilder.serveCocktail(); }
    public void makeDrink() {
        cocktailBuilder.createCocktail();
        cocktailBuilder.setAlco1("Абсент");
    }
}

import stuff.Waiter;
import stuff.GreenBearFormula;
public class BlueMoon {
    public static void main(String[] args) { //А отак воно працює
        Waiter barman = new Waiter();
        barman.setBuilder(new GreenBearFormula());
        barman.makeDrink();
    }
}

Отака штука виходить. Основний момент тут – відокремлення процесу створення коктейлю від самого коктейлю і, відповідно, ми можемо отримати який завгодно коктейль гарантовано незмінним, а споживач може його хіба що випити. Бонусом йде можливість засадити додаткову сутність за створення типових коктейлів відповідно до бажання замовника: для кожного койтейлю можна задати наперед рецепт. А щоб рецепти не валялися десь невідомо де, можна згадати про патерн “фабричний метод” і використовувати його для отримання одного із рецептів.

Наостанок традиційно короткий список готових реалізацій патерну Builder у Java:

А ще, вживайте лише безалкогольні коктейлі 😉

Почитайте ще оце:


Залиште коментар

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

2 thoughts on “Patterns: Builder (Будівельник)

  • Віктор

    В кінці описаний паттерт Strategy.
    BrownBearBuilder получається тільки враппер над методами обєкта(які можуть бути погруповані логічно у одному методі білдера) + Fluent Interface. Якось так?

    • Akceptor Від автора

      Не буду спорити, але наче типовий приклад як у книжці GoF.
      Я це не вважаю стратегією, бо поведінка незмінна завжди у цьому випадку