При реализации паттерна «Строитель» я часто путаюсь с тем, когда строить не удается, и мне даже удается каждые несколько дней занимать разные позиции по этому вопросу.
Сначала несколько объяснений:
- Под ранним сбоем я подразумеваю, что создание объекта должно завершиться неудачей, как только будет передан недопустимый параметр. Так что внутри
SomeObjectBuilder
. - С поздним провалом я имею в виду, что построение только объекта может потерпеть неудачу при
build()
вызове, который косвенно вызывает конструктор объекта, который будет построен.
Тогда несколько аргументов:
- В пользу позднего отказа: класс строителя должен быть не более чем классом, который просто содержит значения. Более того, это приводит к меньшему дублированию кода.
- В пользу раннего сбоя: общий подход в программировании программного обеспечения заключается в том, что вы хотите обнаруживать проблемы как можно раньше, и поэтому наиболее логичным местом для проверки будет класс конструктора «конструктор», «сеттеры» и, в конечном счете, метод сборки.
Каково общее мнение об этом?
java
design-patterns
skiwi
источник
источник
null
объект, когда возникла проблема вbuild()
.Ответы:
Давайте посмотрим на варианты, где мы можем разместить код проверки:
build()
метода.build()
созданного объекта: он будет вызываться в методе при создании объекта.Вариант 1 позволяет нам обнаруживать проблемы раньше, но могут быть сложные случаи, когда мы можем проверять ввод только с полным контекстом, таким образом, делая по крайней мере часть проверки в
build()
методе. Таким образом, выбор варианта 1 приведет к несогласованности кода, при котором часть проверки выполняется в одном месте, а другая часть - в другом.Вариант 2 не намного хуже, чем вариант 1, потому что, как правило, сеттеры в компоновщике вызываются непосредственно перед
build()
, особенно в свободно используемых интерфейсах. Таким образом, в большинстве случаев все еще возможно обнаружить проблему достаточно рано. Однако, если построитель - не единственный способ создания объекта, это приведет к дублированию проверочного кода, поскольку он должен быть везде, где вы создаете объект. Наиболее логичным решением в этом случае будет поставить проверку как можно ближе к созданному объекту, то есть внутри него. И это вариант 3 .С твердой точки зрения, размещение проверки в компоновщике также нарушает SRP: класс компоновщика уже несет ответственность за агрегирование данных для построения объекта. Валидация - это заключение договоров по собственному внутреннему состоянию, это новая обязанность проверять состояние другого объекта.
Таким образом, с моей точки зрения, не только лучше потерпеть неудачу поздно с точки зрения проектирования, но также лучше потерпеть неудачу внутри созданного объекта, а не в самом сборщике.
UPD: этот комментарий напомнил мне еще одну возможность, когда проверка внутри компоновщика (вариант 1 или 2) имеет смысл. Это имеет смысл, если у застройщика есть свои собственные контракты на объекты, которые он создает. Например, предположим, что у нас есть конструктор, который создает строку с определенным содержимым, скажем, список диапазонов номеров
1-2,3-4,5-6
. Этот строитель может иметь метод, какaddRange(int min, int max)
. Результирующая строка ничего не знает об этих числах, и не должна знать. Сам строитель определяет формат строки и ограничения на числа. Таким образом, методaddRange(int,int)
должен проверять введенные числа и генерировать исключение, если max меньше min.Тем не менее, общее правило будет заключаться в проверке только контрактов, определенных самим застройщиком.
источник
Учитывая, что вы используете Java, рассмотрите авторитетное и подробное руководство, предоставленное Джошуа Блохом в статье « Создание и уничтожение объектов Java» (жирный шрифт в приведенной ниже цитате - мой):
Обратите внимание, что согласно пояснениям редактора этой статьи, «элементы» в приведенной выше цитате относятся к правилам, представленным в Effective Java, Second Edition .
В статье подробно не объясняется, почему это рекомендуется, но если подумать, причины этого достаточно очевидны. Общий совет по пониманию этого предоставляется прямо в статье, в объяснении, как концепция компоновщика связана с концепцией конструктора - и ожидается, что инварианты класса будут проверяться в конструкторе, а не в любом другом коде, который может предшествовать / подготовить его вызов.
Для более конкретного понимания того, почему проверка инвариантов до вызова сборки была бы неправильной, рассмотрим популярный пример CarBuilder . Методы построителя могут быть вызваны в произвольном порядке, и в результате невозможно действительно знать, является ли конкретный параметр действительным до сборки.
Подумайте, что спортивный автомобиль не может иметь более двух мест, как можно узнать,
setSeats(4)
хорошо это или нет? Это только при сборке, когда можно точно знать,setSportsCar()
был ли вызван или нет, то есть, броситьTooManySeatsException
или нет.источник
Неверные значения, которые являются недопустимыми, потому что они недопустимы, должны быть немедленно объявлены, по моему мнению. Другими словами, если вы принимаете только положительные числа и передается отрицательное число, нет необходимости ждать вызова
build()
. Я бы не стал рассматривать эти типы проблем, которые вы «ожидаете», поскольку это является обязательным условием для вызова метода для начала. Другими словами, вы вряд ли будете зависеть от сбоя установки определенных параметров. Скорее всего, вы предполагаете, что параметры верны, или проведете некоторую проверку самостоятельно.Тем не менее, для более сложных вопросов, которые не так легко проверить, может быть лучше сообщить о них при звонке
build()
. Хорошим примером этого может быть использование предоставленной вами информации о соединении для установления соединения с базой данных. В этом случае, хотя вы технически можете проверить наличие таких условий, это больше не является интуитивно понятным и только усложняет ваш код. На мой взгляд, это также типы проблем, которые могут действительно возникнуть, и которые вы не можете предвидеть, пока не попробуете. Это своего рода разница между соответствие строки с регулярным выражением , чтобы увидеть , если она может быть распознана как междунар и просто пытается разобрать его, обработки любых потенциальных исключений , которые могут возникнуть как следствие.Обычно я не люблю генерировать исключения при установке параметров, поскольку это означает необходимость перехвата любого генерируемого исключения, поэтому я склоняюсь к проверке в
build()
. Поэтому по этой причине я предпочитаю использовать RuntimeException, поскольку, опять же, ошибки в передаваемых параметрах обычно не должны возникать.Тем не менее, это лучшая практика, чем все остальное. Я надеюсь, что это отвечает на ваш вопрос.
источник
Насколько я знаю, общая практика (не уверенная в том, что существует консенсус) состоит в том, чтобы потерпеть неудачу как можно раньше, чтобы обнаружить ошибку. Это также затрудняет непреднамеренное неправильное использование вашего API.
Если это тривиальный атрибут, который можно проверить на входе, например, емкость или длина, которые должны быть неотрицательными, то лучше всего немедленно потерпеть неудачу. Удержание ошибки увеличивает расстояние между ошибкой и обратной связью, что затрудняет поиск источника проблемы.
Если вам не повезло оказаться в ситуации, когда действительность атрибута зависит от других, у вас есть два варианта:
build()
или около того вызывается.Как и в большинстве случаев, это решение принимается в контексте. Если из-за контекста становится неловким или сложным преждевременный сбой, можно сделать компромисс, чтобы отложить проверки на более позднее время, но по умолчанию должен использоваться отказоустойчивый.
источник
unsigned
,@NonNull
и т.д.X
на значение, которое недопустимо, учитывая текущее значениеY
, но перед вызовомbuild()
установитеY
значение, которое сделаетX
действительным.Shape
а у строителя естьWithLeft
иWithRight
свойства, и кто-то хочет настроить построителя так, чтобы он строил объект в другом месте, требуя, чтобыWithRight
он вызывался первым при перемещении объекта вправо иWithLeft
при перемещении его влево, добавило бы ненужную сложность по сравнению с разрешениемWithLeft
установить левый край справа от старого правого края при условии, чтоWithRight
правый край фиксируется доbuild
вызова.Основное правило - «рано провалиться».
Чуть более продвинутое правило - «провалиться как можно раньше».
Если свойство изначально недействительно ...
... тогда вы немедленно отвергаете это.
В других случаях может потребоваться проверка значений в комбинации, и их лучше разместить в методе build ():
источник