Для начала стоит обратиться к какому-нибудь источнику информации об этом шаблоне. Например,
к этому.
Представьте сложный объект, требующий кропотливой пошаговой инициализации множества полей и вложенных объектов. Код инициализации таких объектов обычно спрятан внутри монструозного конструктора с десятком параметров. Либо ещё хуже — распылён по всему клиентскому коду.
Иными словами, под монструозным конструктором понимается намеренное создание
монолита. Монолит создается, как правило, один раз и потом тухнет до полного разложения его кода, когда пользоваться им уже невозможно в следствии объема его ошибок, но и отрефакторить тоже невозможно в следствии его монолитной организации.
А под
ещё хуже понимается именно вот этот метод с интерфейсом полным сеттеров, которые призваны инициализировать объект до его правильного состояния. Только вот в чем проблема: в каком состоянии будет находиться объект до вызова всех нужных сеттеров и завершения его инициализации? В неправильном состоянии, но объект уже будет и воспользоваться им в таком состоянии кто-то обязательно захочет. А ведь еще кто-то должен знать эту самую последовательность(последовательности?) вызова сеттеров чтобы завершить инициализацию такого объекта. Не многовато ли избыточной ответственности на клиентский код возлагается?
Давай представим что нам надо выполнить рейс на МИ-8. Но чтобы автоматизированная система современного аэродрома дала нам разрешение на взлет, нам надо передать ей специальный хеш.
Как это могло бы быть...МИ-8 - старая штука, на ее борту механический бортовой компьютер - что-то по аналогии с
машиной Ч. Беббиджа, только поменьше. Современные машины формируют этот хеш сами из своей телеметрии процесса запуска и маршрута полета. У нас же с собой только мобильник с ПО для формирования этого хеша вручную и обширная приборная панель МИ-8.
И вот, мы приступаем к
запуску МИ-8: запускаем каждый механизм и вписываем показания приборов в ПО на мобильнике. Когда вертолет запущен и план полета установлен, мы жмем в ПО на мобильнике кнопку генерации хеша и получаем строчку этого хеша.
В связи с тем, что старой техники со своими условностями море, а формат хеша единый для всех, ПО ручной генерации хеша для каждой модели техники сделали отдельно от ПО передачи хеша в диспетчерскую аэродрома. Да и вообще, ПО генерации хеша для старой техники пишется в КБ производителя этой техники. Поэтому нам выдали строковое представление хеша и теперь его надо скопировать из одного ПО и вставить в нужное поле для отправки в другом ПО...
Так, стоп. Кажется хеш - это интерфейс, оператор ПО внезапно оказался посредником, а ПО для ручной генерации хеша - это билдер что ли? Все именно так.
Билдер хорошо знает устройство того типа, который он строит. Но ни о самом этом типе, ни о его устройстве посреднику можно не знать. Посредник может знать только интерфейс (это важно, т.е. даже не сам тип билдера, а только его интерфейс) билдера и интерфейс создаваемого объекта. Посредник имеет право обойтись малыми знаниями, которых достаточно для того чтобы передать артефакт работы билдера потребителю.
Билдер может строить объект совсем другого типа данных, отдает он всегда некоторый обобщенный интерфейс.
Вдобавок, создаваемый нашим билдером из примера хеш является DTO - т.е. Data Transfer Object, среди прочих свойств которого можно обозначить иммутабильность.
Иммутабильный объект можно получить через идиому
RAII или
фабричного метода. Но что если для создания иммутабильного объекта требуется очень много параметров? Например - 16 параметров. Или 23, как в
одном случае. Или - 42 параметра.
Что если часть этих 42 параметров можно вычислить из остальных, но вот иногда их нужно указывать явно?
Что если всего параметров 42, но для конструирования требуется использовать лишь произвольное подмножество этих параметров? Припоминаются SQL-запросы, правда ведь?
RAII в этом случае захлебывается и становится непонятным, а фабричных методов требуется столько, что ими становится тяжело управлять. Идиома фабричного метода в этом случае начинает проявлять свои негативные качества и тормозить разработку.
А билдер со всеми такими случаями легко справляется. Напомню, что Immutable в принципе невозможно снабдить сеттерами, т.к. это нарушит иммутабильность. А если выйти из ситуации через преобразование одного Immutable в другой, то достичь таким способом получится лишь
комбинаторного взрыва иммутабильных типов, похоронив тем самым дальнейшую разработку.
Но давай представим другой пример. Допустим, система диспетчеризации аэродрома принимает сигнал о готовности взлета со стороны борта. Разрешать взлет или нет? Кажется, сперва надо то-то проверить перед резолюцией.
Как это могло бы быть...Нужно осмотреть журналы движения судов, сверить полетные расписания, сделать сверку документов пилота на допуск к полетам сегодня и еще что-нибудь малозначительное, вроде проверки расписания уборки взлетного полотна, чтобы какой-нибудь уборщик не взлетел вместе с бортом на воздух.
Все это - запросы к куче разных сервисов. Какой-то сервис может Быть слабонагруженным, а у какого-то могут быть перебои в работе. Система же распределенная и отказоустойчивая, в ней есть дублирующие узлы, на которые стоит отправить повтор запроса в случае отказа по запросу с основного узла.
Все данные нужно собрать в экземпляре сложной аналитической системы, которая состоит из множества довольно сложных стратегий работы со своими данными. Это все значит, что объект, с которым должна работать система диспетчеризации, будет иметь крайне сложный
инвариант своего типа. Такой инвариант невозможно собрать на сеттерах, объект просто не склеится. К тому же, зачем системе диспетчеризации знать все тонкости типа такой сложной аналитической системы, когда ей нужен просто ответ - можно вот этому борту взлетать или нет?
Тут стоит отметить и то, что решение такой задачи в процедурном стиле приведет к все тому же комбинаторному взрыву и перегрузке по логике, потому что сервисный код запросов будет тесно переплетен с кодом логики принятия решений.
Но система диспетчеризации может просто взять билдер и сформировать на его базе граф асинхронных задач запросов к внешним системам. Именно в топологии этого графа и отражаются зависимости запросов к резервным системам, а результаты запросов складываются в билдер в произвольном, полностью асинхронном порядке.
Когда билдер возвращается к системе диспетчеризации из асинхронной среды, система диспетчеризации просто создает из него объект аналитической системы, с которой дальше и ведет все свои беседы по душам.
Билдер призван решать вопросы перегрузки по знаниям типов, перегрузки по сохранению отказоустойчивости и перегрузки по сложности создания экземпляров. И использовать его стоит именно для решения таких задач.
Еще билдер позволяет развести логику сеттеров отдельно от логики результирующего типа, это бывает очень полезно.
И, да, важно отметить еще и то, что если инвариант типа допускает наличие сеттеров, если в том, что пользовательская система знает этот тип в лицо, нет проблемы, если сами сеттеры не перегружают логику типа, то билдер уже явно не нужен.