Я видел много реализаций шаблона Builder (в основном на Java). Все они имеют класс сущности (скажем, Person
класс) и класс строителя PersonBuilder
. Конструктор "складывает" различные поля и возвращает a new Person
с переданными аргументами. Зачем нам явно нужен класс построителя вместо того, чтобы помещать все методы построителя в сам Person
класс?
Например:
class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
Я могу просто сказать Person john = new Person().withName("John");
Зачем нужен PersonBuilder
класс?
Единственное преимущество, которое я вижу, это то, что мы можем объявить Person
поля как final
, обеспечивая тем самым неизменность.
java
design-patterns
builder-pattern
Боян Кушлев
источник
источник
chainable setters
: DwithName
вернуть копию Человека только с измененным полем имени. Другими словами,Person john = new Person().withName("John");
может работать, даже еслиPerson
является неизменным (и это распространенная модель в функциональном программировании).void
методов. Так, например, еслиPerson
есть метод, который печатает их имя, вы все равно можете связать его с помощью интерфейса Fluentperson.setName("Alice").sayName().setName("Bob").sayName()
. Кстати, я аннотирую их в JavaDoc именно вашим предложением@return Fluent interface
- оно достаточно общее и понятное, когда оно применяется к любому методу, который делаетreturn this
в конце своего выполнения, и это довольно ясно. Таким образом, Builder также будет свободно работать с интерфейсом.Ответы:
Это значит, что вы можете быть неизменными И моделировать именованные параметры одновременно.
Это удерживает ваши руки от человека до тех пор, пока не будет установлено его состояние, и, как только оно будет установлено, не позволит вам изменить его, хотя каждое поле четко помечено. Вы не можете сделать это только с одним классом в Java.
Похоже, вы говорите о Josh Blochs Builder Pattern . Это не должно быть перепутано с Образцом Банды Четырех Строителей . Это разные звери. Они оба решают строительные проблемы, но по-разному.
Конечно, вы можете создать свой объект, не используя другой класс. Но тогда вы должны выбрать. Вы теряете возможность имитировать именованные параметры в языках, в которых они отсутствуют (например, Java), или вы теряете возможность оставаться неизменными в течение всего времени существования объектов.
Неизменяемый пример, не имеет имен для параметров
Здесь вы строите все с помощью одного простого конструктора. Это позволит вам оставаться неизменным, но вы потеряете симуляцию именованных параметров. Это становится трудно читать со многими параметрами. Компьютерам все равно, но это тяжело для людей.
Пример имитированного именованного параметра с традиционными установщиками. Не неизменен.
Здесь вы строите все с помощью сеттеров и имитируете именованные параметры, но вы уже не неизменны. Каждое использование сеттера меняет состояние объекта.
Итак, добавив класс, вы можете сделать и то, и другое.
Проверка может быть выполнена в том
build()
случае, если вам достаточно ошибки времени выполнения для отсутствующего поля возраста. Вы можете обновить его и применить к немуage()
вызов ошибки компилятора. Только не с шаблоном строителя Джоша Блоха.Для этого вам нужен внутренний домен-специфический язык (iDSL).
Это позволяет вам требовать, чтобы они звонили
age()
иname()
до звонкаbuild()
. Но вы не можете сделать это, просто возвращаясьthis
каждый раз. Каждая вещь, которая возвращается, возвращает другую вещь, которая заставляет вас называть следующую вещь.Использование может выглядеть так:
Но это:
вызывает ошибку компилятора, потому что
age()
допустимо вызывать только тип, возвращенныйname()
.Эти iDSL являются чрезвычайно мощными (например, JOOQ или Java8 Streams ) и очень удобны в использовании, особенно если вы используете IDE с дополнением кода, но их довольно сложно настроить. Я бы порекомендовал сохранить их для вещей, для которых будет написано немало исходного кода.
источник
AnonymousPersonBuilder
собственный набор правил.Зачем использовать / предоставлять класс строителя:
источник
PersonBuilder
не имеет получателей, и единственный способ проверить текущие значения - это вызвать.Build()
returnPerson
. Таким образом .Build может проверить правильно построенный объект, правильно? Т.е. этот механизм предназначен для предотвращения использования «строящегося» объекта?Build
операцию, чтобы предотвратить плохие и / или недостаточно построенные объекты.getAge
строителя может вернуться,null
если это свойство еще не указано. Напротив,Person
класс может применять инвариант, которым никогда не может быть возрастnull
, что невозможно при смешивании строителя и фактического объекта, как вnew Person() .withName("John")
примере с OP , который успешно создаетPerson
без возраста. Это относится даже к изменяемым классам, так как сеттеры могут применять инварианты, но не могут применять начальные значения.Одна из причин заключается в том, чтобы гарантировать, что все передаваемые данные соответствуют бизнес-правилам.
Ваш пример не принимает это во внимание, но допустим, что кто-то передал пустую строку или строку, состоящую из специальных символов. Вы хотели бы сделать какую-то логику, основанную на том, чтобы убедиться, что их имя действительно является допустимым (что на самом деле очень сложная задача).
Вы можете поместить это все в свой класс Person, особенно если логика очень мала (например, просто убедитесь, что возраст неотрицателен), но по мере роста логики имеет смысл разделить ее.
источник
john
Немного другой взгляд на это от того, что я вижу в других ответах.
withFoo
Подход здесь является проблематичным , так как они ведут себя как сеттера , но определены таким образом , чтобы создать впечатление опоры класса неизменности. В классах Java, если метод изменяет свойство, обычно запускают метод с помощью 'set'. Я никогда не любил это как стандарт, но если ты сделаешь что-то еще, это удивит людей, и это не хорошо. Есть еще один способ, которым вы можете поддерживать неизменяемость с помощью базового API, который у вас есть. Например:Это не дает возможности предотвратить неправильную печать частично построенных объектов, но предотвращает изменения любых существующих объектов. Это, вероятно, глупо для такого рода вещей (как и JB Builder). Да, вы создадите больше объектов, но это не так дорого, как вы думаете.
В основном вы увидите такой подход, используемый с параллельными структурами данных, такими как CopyOnWriteArrayList . И это намекает на то, почему неизменность важна. Если вы хотите сделать ваш код безопасным для потоков, почти всегда следует учитывать неизменность. В Java каждому потоку разрешено хранить локальный кэш переменного состояния. Чтобы один поток мог видеть изменения, сделанные в других потоках, необходимо использовать синхронизированный блок или другую функцию параллелизма. Любой из них добавит некоторые накладные расходы в код. Но если ваши переменные являются окончательными, ничего не поделаешь. Значение всегда будет тем, к чему оно было инициализировано, и поэтому все потоки видят одно и то же, несмотря ни на что.
источник
Другая причина, которая здесь явно не упоминалась, заключается в том, что
build()
метод может проверить, что все поля являются 'полями, содержащими допустимые значения (заданные напрямую или полученные из других значений других полей), что, вероятно, является наиболее вероятным режимом сбоя что иначе произошло бы.Другое преимущество заключается в том, что ваш
Person
объект будет иметь более простое время жизни и простой набор инвариантов. Вы знаете, что если у вас естьPerson p
, у вас естьp.name
и действительныйp.age
. Ни один из ваших методов не должен быть рассчитан на такие ситуации, как «ну, если задан возраст, но не имя, или что, если задано имя, но не возраст?» Это уменьшает общую сложность класса.источник
URI
, aFile
, либо aFileInputStream
и использует все, что было предоставлено, чтобы получить a,FileInputStream
который в конечном итоге переходит в вызов конструктора как argТакже можно определить конструктор для возврата интерфейса или абстрактного класса. Вы можете использовать конструктор для определения объекта, и он может определить, какой конкретный подкласс должен возвращаться, основываясь на том, какие свойства установлены или для чего они установлены, например.
источник
Шаблон Builder используется для поэтапного построения / создания объекта путем установки свойств, а когда все обязательные поля установлены, возвращают конечный объект с помощью метода сборки. Вновь созданный объект является неизменным. Главное, на что следует обратить внимание, это то, что объект возвращается только при вызове окончательного метода сборки. Это гарантирует, что все свойства установлены для объекта и, таким образом, объект не находится в несогласованном состоянии, когда он возвращается классом компоновщика.
Если мы не используем класс построителя и напрямую помещаем все методы класса построителя в сам класс Person, то сначала нужно создать объект, а затем вызвать методы установки для созданного объекта, что приведет к противоречивому состоянию объекта между созданием объекта и установка свойств.
Таким образом, используя класс построителя (т.е. некоторую внешнюю сущность, отличную от самого класса Person), мы гарантируем, что объект никогда не будет в противоречивом состоянии.
источник
Повторно использовать объект строителя
Как уже упоминалось, неизменность и проверка бизнес-логики всех полей для проверки объекта являются основными причинами отдельного объекта компоновщика.
Однако повторное использование является еще одним преимуществом. Если я хочу создать множество похожих объектов, я могу внести небольшие изменения в объект построителя и продолжить создание экземпляра. Не нужно воссоздавать объект застройщика. Это повторное использование позволяет разработчику действовать как шаблон для создания многих неизменяемых объектов. Это небольшое преимущество, но оно может быть полезным.
источник
На самом деле, вы можете иметь методы builder в вашем классе, и при этом иметь неизменяемость. Это просто означает, что методы компоновщика будут возвращать новые объекты вместо изменения существующего.
Это работает, только если есть способ получить начальный (действительный / полезный) объект (например, из конструктора, который устанавливает все обязательные поля, или из метода фабрики, который устанавливает значения по умолчанию), а дополнительные методы компоновщика затем возвращают модифицированные объекты на основе на существующий. Эти методы конструктора должны гарантировать, что вы не получите недопустимые / противоречивые объекты в пути.
Конечно, это означает, что у вас будет много новых объектов, и вам не следует делать это, если ваши объекты стоят дорого.
Я использовал это в тестовом коде для создания совпадений Hamcrest для одного из моих бизнес-объектов. Я не помню точный код, но он выглядел примерно так (упрощенно):
Затем я бы использовал это в своих модульных тестах (с подходящим статическим импортом):
источник
name
но нетage
), то концепция не работает. Ну, на самом деле вы могли бы возвращать что-то вроде этого,PartiallyBuiltPerson
пока оно недействительно, но это похоже на хак, чтобы замаскировать Строителя.