Я создал то, что для меня является большим улучшением по сравнению с «Образцом строителя» Джоша Блоха. Ни в коем случае не сказать, что он «лучше», просто в очень специфической ситуации он дает некоторые преимущества - самое большое, что он отделяет строителя от класса, который будет построен.
Я подробно задокументировал эту альтернативу ниже, которую я называю «Слепой шаблон».
Шаблон дизайна: слепой строитель
В качестве альтернативы шаблону Джошуа Блоха (пункт 2 в Effective Java, 2-е издание) я создал то, что я называю «Шаблон слепого строителя», который разделяет многие из преимуществ Bloch Builder и, помимо одного символа, используется точно так же. Слепые строители имеют преимущество
- отделяя конструктор от его окружающего класса, устраняя круговую зависимость,
- значительно уменьшает размер исходного кода (который больше не является ) включающего класса, и
- позволяет
ToBeBuilt
расширять класс без необходимости расширять его конструктор .
В этой документации я буду ссылаться на создаваемый класс как " ToBeBuilt
" класс.
Класс, реализованный с помощью Bloch Builder
Блох Строитель является public static class
содержимое внутри создаваемого им класса. Пример:
открытый класс UserConfig {
приватный финал String sName;
закрытый финал int iAge;
приватный финал String sFavColor;
public UserConfig (UserConfig.Cfg uc_c) {// КОНСТРУКТОР
//перечислить
пытаться {
sName = uc_c.sName;
} catch (NullPointerException rx) {
бросить новое NullPointerException ("uc_c");
}
iAge = uc_c.iAge;
sFavColor = uc_c.sFavColor;
// ПРОВЕРКА ВСЕХ ПОЛЕЙ ЗДЕСЬ
}
public String toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
//builder...START
открытый статический класс Cfg {
private String sName;
private int iAge;
приватная строка sFavColor;
public Cfg (String s_name) {
sName = s_name;
}
// самовозвратные сеттеры ... START
public cfg age (int i_age) {
iAge = i_age;
верни это;
}
public Cfg favouriteColor (String s_color) {
sFavColor = s_color;
верни это;
}
// самовозвратные сеттеры ... END
public UserConfig build () {
return (новый UserConfig (this));
}
}
//builder...END
}
Создание класса с помощью Bloch Builder
UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();
Тот же класс, реализованный как Blind Builder
Blind Builder состоит из трех частей, каждая из которых находится в отдельном файле исходного кода:
ToBeBuilt
Класс (в данном примере: UserConfig
)
- Его "
Fieldable
" интерфейс
- Строитель
1. Строящийся класс
Класс, который будет построен, принимает свой Fieldable
интерфейс как единственный параметр конструктора. Конструктор устанавливает все внутренние поля и проверяет каждое из них. Самое главное, этот ToBeBuilt
класс не знает своего строителя.
открытый класс UserConfig {
приватный финал String sName;
закрытый финал int iAge;
приватный финал String sFavColor;
public UserConfig (UserConfig_Fieldable uc_f) {// КОНСТРУКТОР
//перечислить
пытаться {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
бросить новое NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// ПРОВЕРКА ВСЕХ ПОЛЕЙ ЗДЕСЬ
}
public String toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
}
Как заметил один умный комментатор (который необъяснимым образом удалил свой ответ), если ToBeBuilt
класс также реализует его Fieldable
, его единственный конструктор может использоваться как в качестве основного, так и копирующего конструктора (недостатком является то, что поля всегда проверяются, даже если известно, что поля в оригинале ToBeBuilt
действительны).
2. Интерфейс " Fieldable
"
Полевой интерфейс - это «мост» между ToBeBuilt
классом и его создателем, определяющий все поля, необходимые для построения объекта. Этот интерфейс требуется ToBeBuilt
конструктором классов и реализуется сборщиком. Поскольку этот интерфейс может быть реализован другими классами, кроме компоновщика, любой класс может легко создать экземпляр ToBeBuilt
класса без необходимости использовать его компоновщик. Это также облегчает расширение ToBeBuilt
класса, когда расширение его компоновщика нежелательно или необходимо.
Как описано в следующем разделе, я не документирую функции этого интерфейса вообще.
открытый интерфейс UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
3. Строитель
Строитель реализует Fieldable
класс. Он вообще не проверяет, и, чтобы подчеркнуть этот факт, все его поля являются открытыми и изменчивыми. Хотя эта общедоступная доступность не является обязательной, я предпочитаю и рекомендую ее, поскольку она усиливает тот факт, что проверка не происходит до тех пор, пока не ToBeBuilt
будет вызван конструктор. Это важно, потому что это возможно для другого потока манипулировать построитель дальше, прежде чем он будет передан в ToBeBuilt
конструктор «S. Единственный способ гарантировать, что поля действительны - при условии, что сборщик не может каким-либо образом «заблокировать» свое состояние - это сделать для ToBeBuilt
класса окончательную проверку.
Наконец, как и с Fieldable
интерфейсом, я не документирую ни одного из его получателей.
открытый класс UserConfig_Cfg реализует UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// самовозвратные сеттеры ... START
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
верни это;
}
public UserConfig_Cfg favouriteColor (String s_color) {
sFavColor = s_color;
верни это;
}
// самовозвратные сеттеры ... END
//getters...START
public String getName () {
вернуть sName;
}
public int getAge () {
вернуть iAge;
}
public String getFavoriteColor () {
вернуть sFavColor;
}
//getters...END
public UserConfig build () {
return (новый UserConfig (this));
}
}
Создание класса с помощью слепого строителя
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();
Единственная разница - UserConfig_Cfg
«вместо UserConfig.Cfg
»
Заметки
Недостатки:
- Слепые строители не могут получить доступ к закрытым членам своего
ToBeBuilt
класса,
- Они более многословны, поскольку геттеры теперь требуются как в сборщике, так и в интерфейсе.
- Все для одного класса больше не в одном месте .
Компилировать Blind Builder просто:
ToBeBuilt_Fieldable
ToBeBuilt
ToBeBuilt_Cfg
Fieldable
Интерфейс совершенно не обязательно
Для ToBeBuilt
класса с несколькими обязательными полями - такого как этот UserConfig
пример класса, конструктор может быть просто
public UserConfig (String s_name, int i_age, String s_favColor) {
И позвонил в строителя с
public UserConfig build () {
return (новый UserConfig (getName (), getAge (), getFavoriteColor ()));
}
Или даже исключив геттеры (в сборщике):
return (новый UserConfig (sName, iAge, sFavoriteColor));
Передавая поля напрямую, ToBeBuilt
класс становится таким же «слепым» (не подозревая о его построителе), как и с Fieldable
интерфейсом. Тем не менее, для ToBeBuilt
классов, которые и предназначены для «многократного и расширенного многократного расширения» (что есть в названии этого поста), любые изменения в любом поле требуют изменений в каждом подклассе, в каждом сборщике и ToBeBuilt
конструкторе. Поскольку число полей и подклассов увеличивается, это становится нецелесообразным для обслуживания.
(Действительно, с небольшим количеством обязательных полей использование компоновщика может оказаться излишним. Для тех, кто интересуется, приведу выборку некоторых из более крупных интерфейсов Fieldable в моей личной библиотеке.)
Вторичные классы в подпакете
Я хочу, чтобы все строители и Fieldable
классы для всех слепых строителей были в подпакете их ToBeBuilt
класса. Подпакет всегда называется " z
". Это предотвращает загромождение этих вторичных классов списком пакетов JavaDoc. Например
library.class.my.UserConfig
library.class.my.z.UserConfig_Fieldable
library.class.my.z.UserConfig_Cfg
Пример валидации
Как упомянуто выше, вся проверка происходит в ToBeBuilt
конструкторе. Вот снова конструктор с примером кода проверки:
public UserConfig (UserConfig_Fieldable uc_f) {
//перечислить
пытаться {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
бросить новое NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// проверяем (должен действительно предварительно скомпилировать шаблоны ...)
пытаться {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") не может быть пустым и должен содержать только буквы, цифры и подчеркивания.");
}
} catch (NullPointerException rx) {
бросить новое исключение NullPointerException ("uc_f.getName ()");
}
if (iAge <0) {
бросить новое IllegalArgumentException ("uc_f.getAge () (" + iAge + ") меньше нуля.");
}
пытаться {
if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
бросить новое IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") не является красным, синим, зеленым или ярко-розовым.");
}
} catch (NullPointerException rx) {
бросить новое исключение NullPointerException ("uc_f.getFavoriteColor ()");
}
}
Документирование строителей
Этот раздел применим как к Bloch Builders, так и к Blind Builders. Он демонстрирует, как я документирую классы в этом проекте, делая сеттеры (в компоновщике) и их геттеры (в ToBeBuilt
классе), имеющие прямые ссылки друг на друга - одним щелчком мыши, и пользователю не нужно знать, где эти функции на самом деле находятся - и без необходимости разработчика ничего документировать.
Добытчики: ToBeBuilt
только в классах
Геттеры документированы только в ToBeBuilt
классе. Эквивалентные добытчики как в условиях _Fieldable
и
_Cfg
классов игнорируются. Я не документирую их вообще.
/ **
<P> Возраст пользователя. </ P>
@return Инт, представляющий возраст пользователя.
@see UserConfig_Cfg # age (int)
@ смотри getName ()
** /
public int getAge () {
вернуть iAge;
}
Первая @see
- это ссылка на его установщик, который находится в классе построителя.
Сеттеры: в классе строителей
Сеттер задокументирован так, как если бы он находился в ToBeBuilt
классе , а также как если бы он выполнял валидацию (что на самом деле выполняется ToBeBuilt
конструктором). Звездочка (" *
") - это визуальная подсказка, указывающая, что цель ссылки находится в другом классе.
/ **
<P> Установите возраст пользователя. </ P>
@param i_age Не может быть меньше нуля. Получить с {@code UserConfig # getName () getName ()} *.
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
верни это;
}
Дальнейшая информация
Собираем все вместе: полный исходный код примера Blind Builder с полной документацией
UserConfig.java
import java.util.regex.Pattern;
/ **
<P> Информация о пользователе - <I> [builder: UserConfig_Cfg] </ I> </ P>
<P> Проверка всех полей происходит в этом конструкторе классов. Однако каждое требование проверки является документом только в установочных функциях компоновщика. </ P>
<P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
** /
открытый класс UserConfig {
public static final void main (String [] igno_red) {
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("зеленый"). Build ();
System.out.println (ЯК);
}
приватный финал String sName;
закрытый финал int iAge;
приватный финал String sFavColor;
/ **
<P> Создайте новый экземпляр. Это устанавливает и проверяет все поля. </ P>
@param uc_f Не может быть {@code null}.
** /
public UserConfig (UserConfig_Fieldable uc_f) {
//перечислить
пытаться {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
бросить новое NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// Проверка
пытаться {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") не может быть пустым и должен содержать только буквы, цифры и подчеркивания.");
}
} catch (NullPointerException rx) {
бросить новое исключение NullPointerException ("uc_f.getName ()");
}
if (iAge <0) {
бросить новое IllegalArgumentException ("uc_f.getAge () (" + iAge + ") меньше нуля.");
}
пытаться {
if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
бросить новое IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") не является красным, синим, зеленым или ярко-розовым.");
}
} catch (NullPointerException rx) {
бросить новое исключение NullPointerException ("uc_f.getFavoriteColor ()");
}
}
//getters...START
/ **
<P> Имя пользователя. </ P>
@return Не - {@ code null}, непустая строка.
@see UserConfig_Cfg # UserConfig_Cfg (String)
@see #getAge ()
@see #getFavoriteColor ()
** /
public String getName () {
вернуть sName;
}
/ **
<P> Возраст пользователя. </ P>
@return Число больше или равно нулю.
@see UserConfig_Cfg # age (int)
@see #getName ()
** /
public int getAge () {
вернуть iAge;
}
/ **
<P> Любимый цвет пользователя. </ P>
@return Не - {@ code null}, непустая строка.
@see UserConfig_Cfg # age (int)
@see #getName ()
** /
public String getFavoriteColor () {
вернуть sFavColor;
}
//getters...END
public String toString () {
return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
}
}
UserConfig_Fieldable.java
/ **
<P> Требуется конструктором {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </ P>
** /
открытый интерфейс UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
UserConfig_Cfg.java
import java.util.regex.Pattern;
/ **
<P> Конструктор для {@link UserConfig}. </ P>
<P> Проверка всех полей происходит в конструкторе <CODE> UserConfig </ CODE>. Однако каждое требование проверки является документом только в этих функциях установки классов. </ P>
** /
открытый класс UserConfig_Cfg реализует UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
/ **
<P> Создайте новый экземпляр с именем пользователя. </ P>
@param s_name Не может быть {@code null} или пустым, и должен содержать только буквы, цифры и символы подчеркивания. Получить с {@code UserConfig # getName () getName ()} {@ code ()} .
** /
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// самовозвратные сеттеры ... START
/ **
<P> Установите возраст пользователя. </ P>
@param i_age Не может быть меньше нуля. Получить с {@code UserConfig # getName () getName ()} {@ code ()} .
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
верни это;
}
/ **
<P> Установите любимый цвет пользователя. </ P>
@param s_color Должно быть {@code "red"}, {@code "blue"}, {@code green} или {@code "hot pink"}. Получить с {@code UserConfig # getName () getName ()} {@ code ()} *.
@see #age (int)
** /
public UserConfig_Cfg favouriteColor (String s_color) {
sFavColor = s_color;
верни это;
}
// самовозвратные сеттеры ... END
//getters...START
public String getName () {
вернуть sName;
}
public int getAge () {
вернуть iAge;
}
public String getFavoriteColor () {
вернуть sFavColor;
}
//getters...END
/ **
<P> Создайте UserConfig, как настроено. </ P>
@return <CODE> (новый {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (это)) </ CODE>
** /
public UserConfig build () {
return (новый UserConfig (this));
}
}
asImmutable
и включите вReadableFoo
интерфейс [используя эту философию, вызовbuild
неизменяемого объекта просто вернет ссылку на тот же объект].*_Fieldable
и добавлять новые методы получения, расширять*_Cfg
и добавлять новые установки, но я не понимаю, зачем вам нужно воспроизводить существующие методы получения и установки. Они наследуются, и если они не нуждаются в другой функциональности, нет необходимости создавать их заново.Я думаю, что вопрос здесь предполагает что-то с самого начала, не пытаясь доказать это, что модель строителя по своей сути хороша.
Я думаю, что модель строителя редко, если вообще, хорошая идея.
Назначение шаблона Builder
Целью шаблона компоновщика является поддержание двух правил, которые облегчат использование вашего класса:
Объекты не должны иметь возможность быть построенными в несовместимых / непригодных / недопустимых состояниях.
Person
объект может быть построен без онId
заполняется, в то время как все куски кода , которые используют этот объект может потребоваться вId
только для правильной работы сPerson
.Конструкторы объектов не должны требовать слишком много параметров .
Таким образом, цель шаблона построения не вызывает сомнений. Я думаю, что большая часть желания и его использования основана на анализе, который зашел так далеко: мы хотим эти два правила, это дает эти два правила - хотя я думаю, что стоит изучить другие способы выполнения этих двух правил.
Зачем смотреть на другие подходы?
Я думаю, что причина хорошо показана фактом этого вопроса; Существует сложность и много церемоний, добавленных к структурам в применении шаблона строителя к ним. Этот вопрос задает вопрос, как решить некоторые из этих сложностей, потому что, как это часто бывает, сложность делает сценарий странным (наследование). Эта сложность также увеличивает накладные расходы на обслуживание (добавление, изменение или удаление свойств намного сложнее, чем в других случаях).
Другие подходы
Так что для правила номер один выше, какие подходы есть? Ключ, на который ссылается это правило, заключается в том, что при построении объект обладает всей информацией, необходимой ему для правильного функционирования, а после построения эта информация не может быть изменена извне (поэтому это неизменная информация).
Один из способов предоставить всю необходимую информацию объекту при создании - просто добавить параметры в конструктор. Если эта информация запрашивается конструктором, вы не сможете создать этот объект без всей этой информации, поэтому он будет преобразован в допустимое состояние. Но что, если объект требует много информации, чтобы быть действительным? О черт, если это так, этот подход нарушит правило № 2 выше .
Хорошо, что еще там? Ну, вы можете просто взять всю ту информацию, которая необходима для того, чтобы ваш объект находился в согласованном состоянии, и связать ее в другой объект, который берется во время строительства. Ваш код выше, вместо того, чтобы иметь шаблон строителя, будет:
Это не сильно отличается от шаблона компоновщика, хотя он немного проще, и что наиболее важно, мы выполняем правило № 1 и правило № 2 сейчас .
Итак, почему бы не пойти немного больше и сделать его полным на строителя? Это просто не нужно . В этом подходе я удовлетворил обе цели шаблона компоновщика, применив кое-что немного более простое, более простое в обслуживании и многократно используемое . Этот последний бит является ключевым, этот используемый пример является воображаемым и не пригоден для реальной семантической цели, поэтому давайте покажем, как этот подход приводит к многократному использованию DTO, а не к одному целевому классу .
Таким образом, когда вы создаете связные DTO, подобные этим, они могут удовлетворять цели шаблона компоновщика, проще и с более широкой ценностью / полезностью. Кроме того, этот подход решает сложность наследования, результатом которой является шаблон компоновщика:
Вы можете обнаружить, что DTO не всегда сплочен, или для того, чтобы сделать группы свойств сплоченными, они должны быть разбиты по нескольким DTO - это на самом деле не проблема. Если вашему объекту требуется 18 свойств, и вы можете создать 3 связанных DTO с этими свойствами, у вас есть простая конструкция, которая отвечает целям строителей, а затем и некоторые. Если вы не можете придумать связные группировки, это может быть признаком того, что ваши объекты не являются связными, если они имеют свойства, которые совершенно не связаны, но даже тогда создание единого несвязного DTO все еще предпочтительнее из-за более простой реализации плюс решение вашей проблемы наследования.
Как улучшить шаблон строителя
Итак, все в стороне, у вас есть проблема и вы ищете подход к дизайну, чтобы решить ее. Мое предложение: наследующие классы могут просто иметь вложенный класс, который наследуется от класса построителя суперкласса, поэтому наследующий класс имеет в основном ту же структуру, что и суперкласс, и имеет шаблон построителя, который должен точно функционировать с дополнительными функциями. для дополнительных свойств подкласса ..
Когда это хорошая идея
Отбросив в сторону, у застройщика есть ниша . Мы все знаем это, потому что мы все узнали этого конкретного строителя в тот или иной момент:
StringBuilder
- здесь цель не в простом построении, потому что строки не могут быть проще создавать и объединять и т. Д. Это отличный компоновщик, потому что он имеет выигрыш в производительности ,Таким образом, выигрыш в производительности: у вас есть куча объектов, они неизменного типа, вам нужно свернуть их до одного объекта неизменного типа. Если вы будете делать это постепенно, вы создадите много промежуточных объектов, так что выполнение всего этого за один раз будет гораздо более производительным и идеальным.
Поэтому я думаю, что ключ к хорошей идее находится в проблемной области
StringBuilder
: Необходимость превращать несколько экземпляров неизменяемых типов в один экземпляр неизменяемого типа .источник
fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()
предлагает краткий API для построения foos и может предлагать фактическую проверку ошибок в самом сборщике. Без строителя сам объект должен проверять свои входные данные, что означает, что мы не в лучшем положении, чем раньше.Fieldable
параметр. Я бы вызвал эту функцию проверки изToBeBuilt
конструктора, но она может быть вызвана чем угодно, где угодно. Это исключает возможность избыточного кода, не форсируя конкретную реализацию. (И ничто не мешает вам передавать отдельные поля в функцию проверки, если вам не нравитсяFieldable
концепция - но теперь будет по крайней мере три места, в которых должен поддерживаться список полей.)