Строитель (Builder)
Дмитрий БахтенковСтроитель

Строитель - это порождающий паттерн проектирования, позволяющий пошагово создавать сложные объекты. Этот паттерн стоит использовать, когда необходимо получать объекты с различными вариациями полей.
Проблема
Рассмотрим несколько вариантов решения проблемы - создания сущности с разными вариациями наборов полей.
Перегрузка конструкторов
Механизм перегрузки конструкторов позволяет создать несколько разных вариаций конструкторов с разными наборами параметров. Например:
public class Product
{
// наборы свойств для использования
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
// конструктор для первого случая
public Product(string property1, string property2, string property3)
{
Property1 = property1;
Property2 = property2;
Property3 = property3;
}
// конструктор для второго случая
public Product(string property4, string property5)
{
Property4 = property4;
Property5 = property5;
}
// конструктор для третьего случая
public Product(string property1, string property2, string property3, string property4, string property5)
{
Property1 = property1;
Property2 = property2;
Property3 = property3;
Property4 = property4;
Property5 = property5;
}
}
Однако этот способ имеет несколько недостатков:
- Отсутствие гибкости - для нового набора полей, который не был предусмотрен изначально, нужно будет делать новый конструктор
- Отсутствие удобства - разработчику при создании экземпляра класса Product придётся подбирать нужный конструктор - через подсказки IDE или же лезть в реализацию класса. Это не очень удобно
Необязательные параметры в конструкторе
Если много конструкторов - плохо, то почему бы не создать один конструктор с набором необязательных параметров?
Попробуем:
public class Product
{
// наборы свойств для использования
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
public Product(string property1 = null,
string property2 = null,
string property3 = null,
string property4 = null,
string property5 = null)
{
Property1 = property1;
Property2 = property2;
Property3 = property3;
Property4 = property4;
Property5 = property5;
}
}
В таком случае, разные наборы полей можно инициализировать с помощью соответствующих параметров конструктора, например:
var p = new Product(property1 = "1", property3 = "3");
Этот способ также имеет недостаток: очень много параметров у одного конструктора. Представьте, насколько ухудшится читабельность при десятке таких параметров!
Реализация паттерна "Строитель"
Существует две реализации данного паттерна - классическа и "Fluent Builder". Обе реализации являются достаточно простыми.
В каждой реализации есть:
- Класс, который мы будем строить - назовём его
Product - Класс-строитель, пусть будет
Builder - Также может присутствовать класс, который управляет процессом создания - назовём его
Director. Он используется, если нам необходимо установить конкретный набор и порядок полей классаProduct
Для большей гибкости можно также создать интерфейс или абстрактный класс и сделать несколько реализаций строителя.
Классическая реализация
Концепция
На диаграмме UML классическая реализация выглядит следующим образом:

- Класс Product - это класс, который мы будем строить
public class Product
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
}
- Абстрактный класс Builder описывает методы, с помощью которых можно построить продукт, но не реализовывает их:
public abstract class Builder
{
// объект, который мы будем строить
protected Product product;
protected Builder()
{
// инициализируем объект в конструкторе
product = new Product();
}
// методы для построения разных частей объекта
public abstract void BuildProp1();
public abstract void BuildProp2();
public abstract void BuildProp3();
public abstract void BuildProp4();
public abstract void BuildProp5();
// метод получения построенного продукта
public Product GetProduct()
{
return product;
}
}
- Класс ConcreteBuilder унаследован от Builder и реализует его методы:
public class ConcreteBuilder : Builder
{
public override void BuildProp1()
{
product.Property1 = "property 1 value";
}
public override void BuildProp2()
{
product.Property2 = "property 2 value";
}
public override void BuildProp3()
{
product.Property3 = "property 3 value";
}
public override void BuildProp4()
{
product.Property4 = "property 4 value";
}
public override void BuildProp5()
{
product.Property5 = "property 5 value";
}
}
- Класс Director с помощью метода Construct() создаёт и использует экземпляр продукта в правильном порядке:
public class Director
{
private readonly Builder _builder;
public Director(Builder builder)
{
_builder = builder;
}
public void Construct()
{
// выполняем различные этапы построения
_builder.BuildProp1();
_builder.BuildProp3();
_builder.BuildProp5();
var product = _builder.GetProduct();
Console.WriteLine($"Продукт построен. Свойство 1: {product.Property1}, Свойство 3: {product.Property3}, Свойство 5: {product.Property5}");
}
}
Попробуем использовать класс Director в методе Main:
static void Main(string[] args)
{
var director = new Director(new ConcreteBuilder());
director.Construct();
}
И запустим программу:

Разные классы могут по разному использовать Builder. Подобных Director классов может быть много.
Пример
Разработчикам часто приходится отправлять различные http-запросы, с разными методами, заголовками, телом и т.д. Для возможности динамического создания различных запросов может использоваться данный паттерн.
Рассмотрим UML-диаграмму:

Класс HttpRequest будет описывать запрос, который можно отправить. У него есть заголовки, тело и т.д.
public class HttpRequest
{
// метод запроса
public HttpMethod Method { get; set; }
// url запроса
public string Url { get; set; }
// данные из формы
public Dictionary<string, string> FormData { get; set; } = new();
// данные application/json
public string JsonData { get; set; }
// заголовки запроса
public Dictionary<string, string> Headers { get; set; } = new();
// строковое представление параметров запроса
public string Query { get; set; }
}
Абстрактный класс HttpRequestBuilder описывает возможности для построения запроса:
public abstract class HttpRequestBuilder
{
protected readonly HttpRequest HttpRequest;
public HttpRequestBuilder()
{
HttpRequest = new HttpRequest();
}
public abstract void SetMethod();
public abstract void SetUrl(string url);
// метод для установки заголовка авторизации
public abstract void SetAuth(string auth);
public abstract void SetForm(Dictionary<string, string> data);
public abstract void SetJson(object data);
public abstract void SetQuery(string query);
public abstract void AddHeader(string key, string value);
public abstract void SetHeaders(Dictionary<string, string> headers);
public HttpRequest Build()
{
return HttpRequest;
}
Конкретные классы GetHttpRequestBuilder и PostHttpRequestBuilder реализуют абстрактные методы, учитывая особенности методов get и post:
public class GetHttpRequestBuilder : HttpRequestBuilder
{
public override void SetMethod()
{
HttpRequest.Method = HttpMethod.Get;
}
public override void SetUrl(string url)
{
HttpRequest.Url = url;
}
public override void SetAuth(string auth)
{
HttpRequest.Headers["Authorization"] = auth;
}
public override void SetForm(Dictionary<string, string> data)
{
// кидаем исключение, так как у get-запросов не может быть тела
throw new Exception("У get-запроса не может быть тела");
}
public override void SetJson(object data)
{
// кидаем исключение, так как у get-запросов не может быть тела
throw new Exception("У get-запроса не может быть тела");
}
public override void SetQuery(string query)
{
HttpRequest.Query = query;
}
public override void AddHeader(string key, string value)
{
HttpRequest.Headers[key] = value;
}
public override void SetHeaders(Dictionary<string, string> headers)
{
HttpRequest.Headers = headers;
}
}
public class PostHttpRequestBuilder : HttpRequestBuilder
{
public override void SetMethod()
{
HttpRequest.Method = HttpMethod.Post;
}
public override void SetUrl(string url)
{
HttpRequest.Url = url;
}
public override void SetAuth(string auth)
{
HttpRequest.Headers["Authorization"] = auth;
}
public override void SetForm(Dictionary<string, string> data)
{
HttpRequest.FormData = data;
}
public override void SetJson(object data)
{
HttpRequest.JsonData = JsonSerializer.Serialize(data);
}
public override void SetQuery(string query)
{
HttpRequest.Query = query;
}
public override void AddHeader(string key, string value)
{
HttpRequest.Headers[key] = value;
}
public override void SetHeaders(Dictionary<string, string> headers)
{
HttpRequest.Headers = headers;
}
}
Класс HttpClient использует вариации строителей, чтобы создать запрос и отправить его :
public class HttpClient
{
public void SendGet(string url, Dictionary<string, string> headers = null, string query = null)
{
var builder = new GetHttpRequestBuilder();
builder.SetMethod();
builder.SetUrl(url);
// если строка query не пуста
if (!string.IsNullOrEmpty(query))
{
builder.SetQuery(query);
}
// если заголовки переданы
if (headers is not null)
{
builder.SetHeaders(headers);
}
var request = builder.Build();
// имитируем отправку http-запроса
Console.WriteLine($"Запрос отправлен. Метод: {request.Method}. Url: {request.Url + "?" + request.Query}");
}
public void SendJsonPost(string url, Dictionary<string, string> headers = null, object data = null)
{
var builder = new PostHttpRequestBuilder();
builder.SetMethod();
builder.SetUrl(url);
// если data не пуста
if (data is not null)
{
builder.SetJson(data);
}
// если заголовки переданы
if (headers is not null)
{
builder.SetHeaders(headers);
}
var request = builder.Build();
// имитируем отправку http-запроса
Console.WriteLine($"Запрос отправлен. Метод: {request.Method}. Url: {request.Url}. Data: {request.JsonData}");
}
}
ВАЖНО! В C# есть встроенные реализацииHttpRequestMessageиHttpClient. Этот код создан только для примера.
Примечательно, что классStringBuilderв .NET также является примером паттерна "Строитель". С помощью методовAppendLine(),AppendJoin()и др. мы как бы "строим" строку, а вызов методаToString()возвращает нам готовый результат.
Fluent Builder
Данная реализация паттерна очень похожа на классическую, но она позволяет "строить" различные объекты с помощью цепочек вызовов, а не простых методов, например -
var product = new Builder().WithProp1().WithProp2()...
Концепция:
Рассмотрим диаграмму UML реализации Fluent Builder:

Также, как и в классической реализации, на диаграмме присутствует класс Product, описывающий создаваемую сущность:
public class Product
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
}
Однако обратите внимание, что все методы строителя возвращают... объект строителя (в данном случае объект самого себя). В коде это пишется следующим образом: return this;
public class FluentBuilder
{
private readonly Product _product;
public FluentBuilder()
{
_product = new Product();
}
public FluentBuilder WithProp1(string value)
{
_product.Property1 = value;
return this;
}
public FluentBuilder WithProp2(string value)
{
_product.Property2 = value;
return this;
}
public FluentBuilder WithProp3(string value)
{
_product.Property3 = value;
return this;
}
public FluentBuilder WithProp4(string value)
{
_product.Property4 = value;
return this;
}
public FluentBuilder WithProp5(string value)
{
_product.Property5 = value;
return this;
}
public Product Build()
{
return _product;
}
}
Такой способ позволяет классу Director, использующему строитель, делать красивые цепочки вызовов:
public class Director
{
private readonly FluentBuilder _builder;
public Director(FluentBuilder builder)
{
_builder = builder;
}
public void Construct()
{
// "строим" объект с помощью цепочки вызовов
var product = _builder
.WithProp1("Prop 1")
.WithProp3("Prop 3")
.WithProp5("Prop 5")
.Build();
Console.WriteLine($"Продукт построен. Свойство 1: {product.Property1}, Свойство 3: {product.Property3}, Свойство 5: {product.Property5}");
}
}
Используем это в методе Main:
static void Main(string[] args)
{
var director = new Director(new FluentBuilder());
director.Construct();
}
Вывод программы:

Пример:
Представим, что мы разработчики новой игры, где есть различные игровые объекты - элементы окружения. Этим игровым объектам можно задавать различные события (касание предмета, уничтожение предмета), название, координаты и другие данные. Также у нас будет локация, которую этими объектами мы будем заполнять. У объектов могут иметься разные наборы свойств, и паттерн "Строитель" отлично подойдёт для решения этой проблемы.
На UML-диаграмме наш модуль игровых объектов будет выглядеть следующим образом:

Рассмотрим каждый класс.
GameObject описывает игровой объект с помощью большого набора различных свойств:
public class GameObject
{
// можно ли взаимодействовать с элементом
public bool IsInteractive { get; set; }
// двигается элемент
public bool IsMovable { get; set; }
// событие при движении элемента
public Action Move { get; set; }
// видимость элемента
public bool IsVisible { get; set; }
// название элемента
public string Name { get; set; }
// событие при касании с элементом
public Action TouchEvent { get; set; }
// событие уничтожения элемента
public Action DestroyEvent { get; set; }
// расположение элемента по X
public double CoordX { get; set; }
// расположение элемента по Y
public double CoordY { get; set; }
// расположение элемента по Z
public double CoordZ { get; set; }
}
Класс GameObjectBuilder задаёт методы для заполнения полей игрового объекта:
public class GameObjectBuilder
{
private GameObject _gameObject;
public GameObjectBuilder()
{
_gameObject = new GameObject();
}
public GameObjectBuilder IsInteractive()
{
_gameObject.IsInteractive = true;
return this;
}
public GameObjectBuilder IsVisible()
{
_gameObject.IsVisible = true;
return this;
}
public GameObjectBuilder SetName(string name)
{
_gameObject.Name = name;
return this;
}
public GameObjectBuilder SetLocation(double x, double y, double z)
{
_gameObject.CoordX = x;
_gameObject.CoordY = y;
_gameObject.CoordZ = z;
return this;
}
public GameObjectBuilder SetMove(Action action)
{
_gameObject.IsMovable = true;
_gameObject.Move += action;
return this;
}
public GameObjectBuilder AddTouchEvent(Action action)
{
_gameObject.TouchEvent += action;
return this;
}
public GameObjectBuilder AddDestroyEvent(Action action)
{
_gameObject.DestroyEvent += action;
return this;
}
public GameObject Build()
{
return _gameObject;
}
}
В классе Place, в методе Construct мы применяем GameObjectBuilder для создания элементов окружения:
public class Place
{
public void Construct()
{
// строим игровой объект "Кролик"
var bunny = new GameObjectBuilder()
.SetName("Кролик")
.IsInteractive()
.IsVisible()
.SetLocation(10, 10, 10)
.SetMove(() => Console.WriteLine("Кролик прыгает"))
.AddDestroyEvent(() => Console.WriteLine("Кролик умер"))
.AddTouchEvent(() => Console.WriteLine("Кролик озлобленно огрызнулся"))
.Build();
// строим игровой объект "чай"
var tea = new GameObjectBuilder()
.IsInteractive()
.IsVisible()
.SetLocation(110, 112, 111)
.SetName("TESS (не реклама)")
.AddDestroyEvent(() => Console.WriteLine("Чай выпит"))
.AddTouchEvent(() => Console.WriteLine("Чай подобран"))
.Build();
Console.WriteLine("Локация загружена");
Console.WriteLine($"Кролик расположен в координатах {bunny.CoordX} {bunny.CoordY} {bunny.CoordZ}");
bunny.Move?.Invoke();
bunny.TouchEvent?.Invoke();
Console.WriteLine($"Чай расположен в координатах {tea.CoordX} {tea.CoordY} {tea.CoordZ}");
tea.DestroyEvent?.Invoke();
}
}
Попробуем создать нашу локацию в методе Main:
static void Main(string[] args)
{
var place = new Place();
place.Construct();
}
Вывод программы:

ВАЖНО: Автор статьи не является разработчиком игр. Данный пример был придуман для того чтобы показать вам этот паттерн, но автор уверен, что этот пример не является лучшим в разработке игр.
В .NET есть язык запросов к данным под названием LINQ, Методы расширения LINQ являются отличным примером паттерна Fluent Builder
Вывод:
Мы научились использовать паттерн Builder для создания разных вариаций крупных объектов. Этот паттерн имеет следующие преимущества:
- Возможность внедрения дополнительной логики в методы установки значений
- Разделение данных и их создания
- Отсутствие больших конструкторов и использование методов строителя повышает читаемость кода
По наблюдениям автора, чаще всего используется реализация Fluent Builder за счёт своей гибкости и читаемости.
Исходный код доступен на гитхабе.
Спасибо за внимание! С вами был Flex Code