Builder (Строитель)
Builder позволяет отделить процесс создания сложного объекта от конечной репрезентации этого объекта.
Пример
Предположим, что у нас есть сложный объект – бургер. К ингредиентам бургера у нас будет два обязательных требования: должна быть булка и как минимум одна котлета. Остальные ингредиенты опциональны.
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().