Шаблон будівельника: коли вийти з ладу?


45

Реалізуючи схему Builder, я часто плутаюсь із тим, коли не допустити будівництва, і навіть мені вдається зайняти різні позиції з цього приводу кожні кілька днів.

Спочатку пояснення:

  • Якщо не вдалося зробити раннє, я маю на увазі, що побудова об'єкта повинна вийти з ладу, як тільки передається недійсний параметр. Отже, всередині SomeObjectBuilder.
  • З затримкою запізнення я маю на увазі, що будувати об’єкт може лише на build()виклик, який неявно викликає конструктор об'єкта, який буде побудований.

Потім кілька аргументів:

  • На користь невдачі із запізненням: Клас будівельника повинен бути не більше класу, який просто містить значення. Більше того, це призводить до меншого дублювання коду.
  • На користь невдалого ранку: загальний підхід у програмуванні програмного забезпечення полягає в тому, що ви хочете виявити проблеми якомога раніше, і тому найбільш логічним місцем для перевірки буде місце в конструкторі, конструкторі, сеттерах і в кінцевому підсумку в методі збірки.

Який загальний консенсус щодо цього?


8
Я не бачу жодної переваги відмовитись пізно. Те, що хтось каже, що "слід" класу будівельників, не має переваги над хорошим дизайном, а ловити помилок на ранніх стадіях завжди краще, ніж пізніше ловити помилки.
Довал

3
Ще один спосіб подивитися на це - це те, що будівельник може не знати, що є дійсними даними. Невдача на ранній стадії в цьому випадку - це більше про помилку, як тільки ви дізнаєтесь про помилку. Якщо раніше не вийде з ладу, будівельник поверне nullоб'єкт, коли в ньому виникли проблеми build().
Кріс

Якщо ви не додасте спосіб надіслати попередження і запропонувати засоби виправити в програмі будівельника, немає сенсу запізнюватися.
Марк

Відповіді:


34

Давайте розглянемо варіанти, де ми можемо розмістити код перевірки:

  1. Всередині сетерів у будівельнику.
  2. Всередині build()методу.
  3. Всередині сконструйованої сутності: вона буде викликана build()методом під час створення об'єкта.

Варіант 1 дозволяє виявляти проблеми раніше, але можуть бути складні випадки, коли ми можемо перевірити вхід лише з повним контекстом, таким чином, зробивши принаймні частину перевірки build()методу. Таким чином, вибір варіанту 1 призведе до непослідовного коду, частина перевірки виконується в одному місці, а інша - в іншому місці.

Варіант 2 не є значно гіршим, ніж варіант 1, тому що, як правило, сеттери в програмі запускаються безпосередньо перед build(), особливо, у вільних інтерфейсах. Таким чином, в більшості випадків проблему все-таки можливо виявити досить рано. Однак якщо конструктор - це не єдиний спосіб створити об’єкт, це призведе до дублювання коду перевірки, тому що вам потрібно мати його скрізь, де ви створюєте об'єкт. Найбільш логічним рішенням у цьому випадку буде поставити перевірку якомога ближче до створеного об’єкта, тобто всередині нього. І це варіант 3 .

З точки зору SOLID, введення валідації в builder також порушує SRP: клас будівельника вже несе відповідальність за агрегацію даних для побудови об'єкта. Валідація - це встановлення договорів про власний внутрішній стан, це нова відповідальність перевірити стан іншого об’єкта.

Таким чином, з моєї точки зору, не тільки краще пізно провалитись з точки зору проектування, але і краще провалитися всередині побудованої сутності, а не в самому будівельнику.

UPD: цей коментар нагадав мені ще одну можливість, коли валідація всередині будівельника (варіант 1 або 2) має сенс. Це має сенс, якщо у будівельника є власні договори на об’єкти, які він створює. Наприклад, припустимо, що у нас є конструктор, який будує рядок із певним вмістом, скажімо, списком діапазонів чисел 1-2,3-4,5-6. Цей будівельник може мати такий метод addRange(int min, int max). Отриманий рядок нічого не знає про ці числа, а також не повинен знати. Сам конструктор визначає формат рядка та обмеження чисел. Таким чином, метод addRange(int,int)повинен підтвердити вхідні числа та винести виняток, якщо max менше, ніж min.

Однак, загальним правилом буде затвердження лише тих договорів, визначених самим будівельником.


Я думаю, що варто відзначити, що хоча варіант 1 може призвести до "непослідовних" моментів перевірки, він все ще може розглядатися як послідовний, якщо все "як можна раніше". Трохи простіше зробити "якомога раніше" більш визначеним, якщо замість цього буде використаний варіант будівельника, StepBuilder.
Джошуа Тейлор

Якщо конструктор URI видає виняток, якщо нульовий рядок передано, це порушення SOLID? Сміття
Гусдор

@Gusdor так, якщо він кидає виняток сам. Однак, з точки зору користувача, всі параметри виглядають як виняток, який викидає будівельник.
Іван Гаммель

То чому б не мати валідат (), який викликається build ()? Таким чином, є невелике дублювання, послідовність і порушення SRP. Це також дозволяє перевірити дані без спроби створення, а перевірка близька до створення.
StellarVortex

@StellarVortex в цьому випадку він буде перевірений двічі - один раз у builder.build (), і, якщо дані дійсні, і ми переходимо до конструктора об'єкта, у цьому конструкторі.
Іван Гаммель

34

Зважаючи на те, що ви використовуєте Java, врахуйте авторитетні та детальні вказівки, надані Джошуа Блохом у статті Створення та знищення об’єктів Java (жирний шрифт нижче цитата - мій):

Як і конструктор, будівельник може нав'язувати інваріанти за його параметрами. Метод побудови може перевірити цих інваріантів. Важливо, щоб вони перевірялися після копіювання параметрів від будівельника до об’єкта, і щоб вони перевірялися на полях об'єктів, а не на полях конструктора (Пункт 39). Якщо будь-які інваріанти порушені, метод збирання повинен кинути IllegalStateException(Пункт 60). Метод деталізації винятку повинен вказувати, який інваріант порушений (п. 63).

Інший спосіб нав'язувати інваріанти, що включають декілька параметрів, полягає у тому, щоб методи сеттера взяли цілі групи параметрів, на яких повинен міститись інваріант. Якщо інваріант не задоволений, метод сеттера кидає an IllegalArgumentException. Це має перевагу в виявленні інваріантного відмови, як тільки недійсні параметри будуть передані, замість того, щоб чекати виклику збірки.

Зауважте, згідно з поясненнями редактора цієї статті, "елементи", наведені вище, цитують правила, представлені в " Ефективна Java", Друге видання .

Стаття не заглиблюється в пояснення, чому це рекомендується, але якщо ви думаєте про це, причини досить очевидні. Загальна порада щодо розуміння цього наведена саме в статті, в поясненні того, як концепція конструктора пов'язана з концепцією конструктора - і очікується, що інваріанти класів будуть перевірені в конструкторі, а не в будь-якому іншому коді, який може передувати / підготувати його виклик.

Для більш конкретного розуміння того, чому перевірка інваріантів перед викликом складання було б неправильним, розглянемо популярний приклад CarBuilder . Методи Builder можуть використовуватися в довільному порядку, і, як наслідок, не можна точно знати, чи є певний параметр дійсним до збірки.

Подумайте, що спортивний автомобіль не може мати більше 2-х сидінь, як можна було знати setSeats(4), нормально чи ні? Це лише на складанні, коли можна точно знати, чи setSportsCar()викликали це чи ні, тобто кинути TooManySeatsExceptionчи ні.


3
+1 - рекомендувати, які типи винятків кинути, саме те, що я шукав.
Ксантікс

Не впевнений, що я отримую альтернативу. Здається, йдеться лише про те, коли інваріанти можуть бути затверджені лише групами. Конструктор приймає окремі атрибути, коли вони не включають жодних інших, і приймає групи атрибутів лише тоді, коли група інваріантна. У цьому випадку, чи повинен один атрибут кидати виняток перед збіркою?
Дідьє А.

19

На мій погляд, негайно слід повідомити про недійсні значення, які не є допустимими, оскільки вони не переносяться. Іншими словами, якщо ви приймаєте лише позитивні цифри, а негативне число передається, вам не потрібно чекати, поки build()викликається. Я б не вважав ці типи проблем, які ви «очікуєте», що трапляться, оскільки це необхідна умова викликати метод для початку. Іншими словами, ви, швидше за все, не залежатимете від невстановлення певних параметрів. Більш ймовірно, ви б припустили, що параметри є правильними, або ви самі зробите певну перевірку.

Однак про складніші питання, які не так легко підтверджені, може бути краще повідомити, коли ви телефонуєте build(). Хорошим прикладом цього може бути використання наданої вами інформації про з'єднання для встановлення з'єднання з базою даних. У цьому випадку, хоча ви технічно могли перевірити наявність таких умов, це вже не інтуїтивно зрозуміло і це лише ускладнює ваш код. Як я бачу, це також типи питань, які можуть насправді трапитися, і яких ви не можете реально передбачити, поки не спробуєте. Це своєрідна різниця між узгодженням рядка з регулярним виразом, щоб побачити, чи можна його розібрати як int, і просто намагатися розібрати його, обробляючи будь-які можливі винятки, які можуть виникнути як наслідок.

Я, як правило, не люблю викидання викидів при встановленні параметрів, оскільки це означає, що потрібно виловлювати будь-який викинутий виняток, тому я схильний підтримувати перевірку в build(). Тому з цієї причини я вважаю за краще використовувати RuntimeException, оскільки помилки в переданих параметрах зазвичай не трапляються.

Однак це більше, ніж найкраща практика. Я сподіваюся, що це відповість на ваше запитання.


11

Наскільки я знаю, загальна практика (не впевнена, чи є консенсус) полягає в тому, щоби провалитись якнайшвидше, оскільки можна виявити помилку. Це також ускладнює ненавмисне неправильне використання API.

Якщо це тривіальний атрибут, який можна перевірити на вході, наприклад, ємність або довжина, яка повинна бути негативною, то вам найкраще негайно вийти з ладу. Утримування помилки збільшує відстань між помилкою та зворотним зв'язком, що ускладнює пошук джерела проблеми.

Якщо у вас є нещастя опинитися в ситуації, коли термін дії атрибута залежить від інших, то у вас є два варіанти:

  • Потрібно, щоб обидва (або більше) атрибутів надходили одночасно (тобто виклик одного методу).
  • Перевірте дійсність, як тільки ви дізнаєтесь, що більше змін не надходить: коли build()або так називається.

Як і в більшості речей, це рішення, прийняте в контексті. Якщо контекст робить незручним або складним невдачу достроково, можна зробити компроміс, щоб відкласти чеки на більш пізній час, але за замовчуванням має бути невдалий.


Отже, підсумовуючи, ви говорите, що розумно якомога раніше перевірити все, що могло бути охоплено об'єктом / примітивним типом? Як unsignedі @NonNullт. Д.
skiwi

2
@skiwi Досить, так. Перевірки доменів, нульові перевірки, такі речі. Я б не виступав за те, щоб вносити в нього набагато більше, ніж це: будівельники, як правило, прості речі.
JvR

1
Можливо, варто відзначити, що якщо обґрунтованість одного параметра залежить від значення іншого, можна лише законно відхилити значення параметра, якщо відомо, що інший "дійсно" встановлений . Якщо допустимо встановити значення параметра кілька разів [з останнім параметром, що має перевагу], то в деяких випадках найбільш природним способом налаштування об'єкта може бути встановлення параметра Xна значення, яке недійсне, враховуючи теперішнє значення Y, але перед викликом build()встановлюється Yзначення, яке було б Xдійсним.
supercat

Якщо, наприклад, хтось будує, Shapeа будівельник має WithLeftі WithRightвластивості, і хоче налаштувати будівельника, щоб сконструювати об’єкт в іншому місці, вимагаючи, щоб WithRightйого викликали спочатку при переміщенні об'єкта праворуч, а WithLeftпри переміщенні його вліво додавали б зайвих складностей порівняно з тим, що дозволяють WithLeftвстановити лівий край праворуч від старого правого краю, за умови, що WithRightправий край фіксується раніше build.
supercat

0

Основне правило - "невдало рано".

Трохи вдосконалене правило - "невдача як можна раніше".

Якщо властивість суттєво недійсна ...

CarBuilder.numberOfWheels( -1 ). ...  

... тоді ти негайно відкидаєш.

В інших випадках може знадобитися, щоб значення перевірялися в комбінації, і вони могли бути краще розміщені в методі build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.