BackendBit

К списку паттернов

Builder позволяет отделить процесс создания сложного объекта от конечной репрезентации этого объекта.

UML диаграмма классов паттерна СтроительПоказывает интерфейс BurgerBuilder с конкретной реализацией BigMacBuilder, продукт Burger и опциональный класс BurgerDirector.<<interface>>BurgerBuilder+addPatty()+addSauce()+addBacon()+addOnions()+addCheese()+addTomato()+addPickles()+addLettuce()+build()+reset()Burger-bun: Bun-patties: List<Patty>-sauce: Sauce-cheese: Cheese-bacon: Bacon-onions: Onions-lettuce: Lettuce-pickles: Pickles-tomato: TomatoBurgerDirector-builder: BurgerBuilder+buildBurgerWithoutVegetables()+buildBurgerWithEverything()BigMacBuilderОпциональныйИспользуетСоздаёт

Пример

Предположим, что у нас есть сложный объект – бургер. К ингредиентам бургера у нас будет два обязательных требования: должна быть булка и как минимум одна котлета. Остальные ингредиенты опциональны.

package builder;

import java.util.List;

public final class Burger {
    private final Bun bun;           // булка
    private final List<Patty> patties; // котлеты
    private final Sauce sauce;        // соус
    private final Cheese cheese;      // сыр
    private final Bacon bacon;        // бекон
    private final Onions onions;      // лук
    private final Lettuce lettuce;    // салат
    private final Pickles pickles;    // огурцы
    private final Tomato tomato;      // помидоры

    public Burger(
            Bun bun,
            List<Patty> patties,
            Sauce sauce,
            Cheese cheese,
            Bacon bacon,
            Onions onions,
            Lettuce lettuce,
            Pickles pickles,
            Tomato tomato
    ) {
        if (bun == null) {
            throw new IllegalArgumentException("Burger must have a bun");
        }
        if (patties == null || patties.isEmpty()) {
            throw new IllegalArgumentException("Burger must have at least one patty");
        }
        this.bun = bun;
        this.patties = List.copyOf(patties);
        this.sauce = sauce;
        this.cheese = cheese;
        this.bacon = bacon;
        this.onions = onions;
        this.lettuce = lettuce;
        this.pickles = pickles;
        this.tomato = tomato;
    }

    // Геттеры опущены для краткости
}
java

У класса получился большой конструктор. Допустим, мы захотим собрать биг мак. Вот так будет выглядеть создание объекта:

Burger bigMac = new Burger(
    new Bun("Булка с кунжутом"),
    List.of(new Patty("Говяжья котлета"), new Patty("Говяжья котлета")),
    new Sauce("Специальный соус Big Mac"),
    null,
    null,
    new Onions("Репчатый лук"),
    new Lettuce(),
    new Pickles(),
    null
);
java

Нам приходится пропускать некоторые аргументы, передавая null. Кроме того, придется постоянно заглядывать в конструктор, чтобы передать аргументы в правильном порядке. Все это выглядит монструозно и тяжело читается.

В Java нет именованных аргументов, поэтому нам придется передавать все параметры в правильном порядке. Это делает код еще менее читаемым, чем в языках с поддержкой именованных параметров.

Можно упростить создание объекта с помощью паттерна Builder.

Создадим интерфейс BurgerBuilder.

package builder;

public interface BurgerBuilder {
    BurgerBuilder addPatty();

    BurgerBuilder addSauce();

    BurgerBuilder addBacon();

    BurgerBuilder addOnions();

    BurgerBuilder addCheese();

    BurgerBuilder addTomato();

    BurgerBuilder addPickles();

    BurgerBuilder addLettuce();

    Burger build();

    BurgerBuilder reset();
}
java

И создадим конкретный класс BigMacBuilder.

package builder;

import java.util.ArrayList;
import java.util.List;

public final class BigMacBuilder implements BurgerBuilder {
    private List<Patty> patties;
    private Sauce sauce;
    private Bacon bacon;
    private Onions onions;
    private Cheese cheese;
    private Tomato tomato;
    private Pickles pickles;
    private Lettuce lettuce;

    public BigMacBuilder() {
        this.patties = defaultPatties();
    }

    @Override
    public BurgerBuilder addPatty() {
        this.patties.add(beefPatty());
        return this;
    }

    @Override
    public BurgerBuilder addSauce() {
        this.sauce = new Sauce("Соус Big Mac");
        return this;
    }

    @Override
    public BurgerBuilder addBacon() {
        this.bacon = new Bacon("Жареный бекон");
        return this;
    }

    @Override
    public BurgerBuilder addOnions() {
        this.onions = new Onions("Репчатый лук");
        return this;
    }

    @Override
    public BurgerBuilder addCheese() {
        this.cheese = new Cheese("Чеддер");
        return this;
    }

    @Override
    public BurgerBuilder addTomato() {
        this.tomato = new Tomato("Красный помидор");
        return this;
    }

    @Override
    public BurgerBuilder addPickles() {
        this.pickles = new Pickles();
        return this;
    }

    @Override
    public BurgerBuilder addLettuce() {
        this.lettuce = new Lettuce();
        return this;
    }

    @Override
    public Burger build() {
        return new Burger(
            new Bun("Булка с кунжутом"),
            List.copyOf(patties),
            sauce,
            cheese,
            bacon,
            onions,
            lettuce,
            pickles,
            tomato
        );
    }

    @Override
    public BurgerBuilder reset() {
        this.patties = defaultPatties();
        this.sauce = null;
        this.cheese = null;
        this.bacon = null;
        this.onions = null;
        this.lettuce = null;
        this.tomato = null;
        this.pickles = null;
        return this;
    }

    private Patty beefPatty() {
        return new Patty("Говяжья котлета");
    }

    private List<Patty> defaultPatties() {
        List<Patty> patties = new ArrayList<>();
        patties.add(beefPatty());
        patties.add(beefPatty());
        return patties;
    }
}
java

Теперь мы можем создать биг мак таким образом:

Burger bigMac = new BigMacBuilder()
    .addSauce()
    .addPickles()
    .addLettuce()
    .addOnions()
    .build();
java

Мы можем опционально добавить дополнительные ингредиенты. Например, бекон:

builder.addBacon();
java

Или не добавлять лук, например. В общем мы получаем гибкость вместе с простотой построения объекта.

Director

В структуре паттерна “строитель” есть опциональная часть, которая называется Director. Director – это класс, который оркеструет вызовы методов билдера для переиспользования. К примеру мы можем создать методы для сборки бургера без овощей или бургера со всеми ингредиентами сразу.

package builder;

public final class BurgerDirector {
    private final BurgerBuilder builder;

    public BurgerDirector(BurgerBuilder builder) {
        this.builder = builder;
    }

    /**
     * Бургер без овощей.
     */
    public Burger buildBurgerWithoutVegetables() {
        // Нужно сбрасывать конфигурацию билдера, чтобы убедиться,
        // что не осталось ничего лишнего после прошлой сборки.
        builder.reset();

        builder.addSauce();
        builder.addBacon();
        builder.addCheese();

        return builder.build();
    }

    /**
     * Бургер со всеми ингредиентами.
     */
    public Burger buildBurgerWithEverything() {
        builder.reset();

        builder.addBacon();
        builder.addOnions();
        builder.addSauce();
        builder.addCheese();
        builder.addTomato();
        builder.addLettuce();
        builder.addPickles();

        return builder.build();
    }
}
java

Эти методы можно переиспользовать с любыми конкретными классами билдеров, так как конструктор Director’а принимает интерфейс.

Кстати, зачем нам интерфейс для билдера? Благодаря интерфейсу Director работает с любым билдером через общий контракт – ему не нужно знать, какой именно бургер собирается. Каждый конкретный билдер сам решает, какую булку, соус и другие ингредиенты использовать. Например, BigMacBuilder кладёт булку с кунжутом и специальный соус, а билдер для другого бургера может использовать совсем другие ингредиенты.

Однако в реальных проектах действительно часто встречается версия билдера без использования интерфейса.

Реальные примеры

Библиотека OkHttp

OkHttp – популярный HTTP клиент для JVM приложений.

Рассмотрим пример с OkHttpClient. У этого класса много необязательных параметров, что усложняет создание объекта напрямую.

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    // Поля опущены для краткости.

    public OkHttpClient() {
        this(new Builder());
    }

    OkHttpClient(Builder builder) {
        this.dispatcher = builder.dispatcher;
        this.connectionPool = builder.connectionPool;
        this.interceptors = builder.interceptors;
        this.networkInterceptors = builder.networkInterceptors;
        this.connectTimeout = builder.connectTimeout;
        this.readTimeout = builder.readTimeout;
        this.writeTimeout = builder.writeTimeout;
        // ... и другие параметры
    }

    // Методы опущены для краткости.
}
java

По этой причине библиотека предоставляет встроенный билдер для класса OkHttpClient.

public static final class Builder {
    // Свойства опущены для краткости.

    public Builder connectTimeout(long timeout, TimeUnit unit) {
        // …
        return this;
    }

    public Builder readTimeout(long timeout, TimeUnit unit) {
        // …
        return this;
    }

    public Builder addInterceptor(Interceptor interceptor) {
        // …
        return this;
    }

    // Прочие шаги построения объекта…

    /**
     * Результирующий метод, который собирает объект.
     */
    public OkHttpClient build() {
        return new OkHttpClient(this);
    }
}
java

Заметим, что здесь один конкретный класс билдера и нет интерфейса. Использование выглядит так:

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .addInterceptor(new LoggingInterceptor())
    .build();
java

Библиотека squirrel

В Go паттерн Builder можно встретить при построении SQL-запросов. Библиотека squirrel – популярный пример такого подхода.

import sq "github.com/Masterminds/squirrel"

// Построение SELECT-запроса
query := sq.Select("id", "name", "email").
    From("users").
    Where(sq.Eq{"status": "active"}).
    Where(sq.Gt{"age": 18}).
    OrderBy("created_at DESC").
    Limit(10)

// Генерация SQL и аргументов
sql, args, err := query.ToSql()
// sql:  "SELECT id, name, email FROM users WHERE status = ? AND age > ? ORDER BY created_at DESC LIMIT 10"
// args: ["active", 18]
go

Каждый метод (Select, From, Where, OrderBy, Limit) возвращает новый объект SelectBuilder, что позволяет выстраивать цепочку вызовов. Метод ToSql() – это результирующий метод, аналог build().