Builder (Строитель)¶
Категория: порождающий паттерн.
Проблема¶
Объект нужно собрать из большого числа частей, многие из которых опциональны, а порядок или сама сборка может требовать промежуточных шагов. Если попытаться выразить все комбинации через конструкторы, получится либо один конструктор с десятком параметров (часть из которых - null/значения по умолчанию для необязательных частей), либо "телескопические" перегрузки конструктора на каждую комбинацию параметров - оба варианта плохо читаются и плохо расширяются.
Решение¶
- Процесс сборки объекта выносится в отдельный класс - Builder.
- Builder предоставляет набор методов для пошаговой настройки будущего объекта, каждый из которых отвечает за одну часть (
WithTitle,AddSection, ...). - Финальный метод (
Build()) собирает итоговый объект из накопленного состояния - часто неизменяемый (immutable), что удобно, так как продукт после сборки уже не должен меняться "снаружи".
Частая реализация в C# - fluent-интерфейс: каждый метод настройки возвращает this, что позволяет писать сборку одной цепочкой вызовов.
Структура¶
Product- собираемый сложный объект.Builder- интерфейс/класс с методами пошаговой настройки и методомBuild().Director(опционально) - класс, знающий типовые последовательности вызовов builder'а для стандартных вариантов продукта; в C# часто опускается, так как порядок вызовов и так очевиден из кода клиента.
Варианты реализации¶
У Builder нет официального набора из «трёх» или «четырёх» отдельных подпаттернов. Ниже - распространённые формы одной идеи. Они отличаются строгостью, универсальностью и тем, когда клиент получает доступ к продукту.
1. Классический Builder с Director¶
Director знает последовательности шагов, но работает с общим интерфейсом Builder. Несколько конкретных строителей выполняют одинаковые шаги по-разному и могут создавать даже несвязанные продукты. Например, один строит дом, другой по тем же этапам формирует инструкцию по его строительству.
Этот вариант оправдан, когда типовые сценарии сборки переиспользуются, а представлений продукта действительно несколько. Если продукт один и алгоритм очевиден, интерфейс строителя и Director часто только усложняют код.
2. Упрощённый fluent-builder для одного продукта¶
Отдельный конкретный класс накапливает параметры и возвращает продукт из Build(). Каждый метод возвращает this, поэтому вызовы образуют читаемую цепочку. Именно этот практичный вариант показан в Builder.cs и чаще всего встречается в современном .NET.
3. Builder на методах расширения¶
Методы расширения могут придать существующему изменяемому типу fluent-синтаксис: new MailMessage().From(...).To(...). Это лёгкий вариант, но он манипулирует уже созданным продуктом: промежуточное невалидное состояние доступно клиенту, а builder не может надёжно скрыть его до финального Build().
4. Строго типизированный Step Builder¶
Каждый обязательный этап возвращает интерфейс следующего этапа, а Build() появляется только после выполнения всех обязательных шагов. Ошибка использования переносится из рантайма в компиляцию. Цена - дополнительные интерфейсы/типы и более тяжёлая поддержка; для большинства моделей достаточно проверки в Build() или обязательных аргументов конструктора builder'а.
5. Builder для неизменяемого продукта¶
Builder хранит изменяемое промежуточное состояние, а Build() создаёт immutable-объект только с геттерами. Часто builder делают вложенным типом продукта или предоставляют через ToBuilder(). Это удобно и может быть эффективнее последовательного создания множества промежуточных immutable-экземпляров.
Рабочие примеры всех форм находятся в BuilderVariants.cs: классический Director с двумя продуктами, методы расширения, Step Builder и вложенный builder неизменяемого объекта.
Когда применять¶
- Объект имеет много полей, значительная часть которых опциональна.
- Нужно гарантировать, что итоговый объект будет создан только в валидном, полностью настроенном состоянии (продукт неизменяем, а промежуточные, "недособранные" состояния наружу не видны).
- Один и тот же процесс сборки должен уметь производить разные представления продукта.
Плюсы¶
- Убирает "телескопические конструкторы" и делает код сборки объекта читаемым.
- Позволяет собирать объект по шагам, в том числе с проверками на каждом шаге.
- Итоговый продукт можно сделать полностью неизменяемым, скрыв изменяемое состояние внутри builder'а.
Минусы¶
- Дополнительный класс и код ради, по сути, альтернативного способа вызвать конструктор - для простых объектов (2-3 обязательных поля) это избыточно.
- В современном C# многие сценарии Builder закрываются именованными и опциональными параметрами конструктора, а также object initializer'ами (
new Report { Title = ..., ... }), особенно если все поля - изменяемые свойства, а неreadonly.
Отличие от Abstract Factory¶
Abstract Factory создаёт объект за один вызов одного из своих методов и обычно работает с семейством разных продуктов. Builder собирает один продукт пошагово, за несколько вызовов, и его основная ценность - именно в поэтапной настройке сложного объекта.
Пример в .NET Framework / BCL¶
StringBuilder- классический пример:Append,AppendLine,Insertпошагово формируют строку,ToString()- финальная сборка продукта.HostBuilder/WebApplicationBuilderв ASP.NET Core - пошаговая настройка (ConfigureServices,ConfigureLogging, ...) с финальнымBuild().- LINQ-выражения вида
IQueryable(например, в EF Core) - каждый вызов (Where,OrderBy,Select) добавляет часть будущего запроса, который "собирается" и выполняется в момент перечисления.
Источники для углубления¶
- Тепляков С. «Паттерны проектирования на платформе .NET», глава 11: fluent interface, методы расширения, строго типизированный строитель и immutable-объекты.
- Refactoring.Guru - Builder: классическая структура с несколькими строителями и опциональным Director.
Пример реализации на C#¶
| BuilderVariants.cs | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | |
Открыть Builder.cs отдельно
Скачать Builder.cs
Открыть BuilderVariants.cs отдельно
Скачать BuilderVariants.cs