Чому тип поєднується зі своїм конструктором?


20

Нещодавно я видалив мою відповідь на Code Review , яка почалася так:

private Person(PersonBuilder builder) {

Стій. Червоний прапор. PersonBuilder побудував би Person; це знає про Особу. Клас Person нічого не повинен знати про PersonBuilder - це просто незмінний тип. Ви створили тут кругле з'єднання, де A залежить від B, що залежить від A.

Людина повинна просто приймати її параметри; клієнт, який готовий створити особу, не будуючи її, повинен це робити.

Мене вдарили по полю, і сказав, що (цитуючи) Червоний прапор, чому? Реалізація тут має таку ж форму, яку продемонстрував Джошуа Блох у своїй книзі "Ефективна Java" (пункт №2).

Отже, виявляється, що єдиний правильний спосіб реалізації шаблону Builder в Java - це зробити будівельника вкладеним типом (хоча це не питання, хоча це питання), а потім зробити продукт (клас об'єкта, що будується ) взяти залежність від будівельника , як це:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

Ця ж сторінка Вікіпедії для того ж шаблону Builder має дещо іншу (і набагато більш гнучку) реалізацію для C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

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

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

То яка угода? Навіщо придумувати надмірно розроблені рішення проблеми, яка навіть не повинна існувати в першу чергу? Якщо ми знімемо Джошуа Блоха з його п’єдесталу на хвилину, чи можемо ми знайти одну єдину добру, дійсну причину з’єднання двох конкретних типів і назвати це найкращою практикою?

Це все нагадує для мене вантажне культове програмування .


12
І це "питання" нагадує просто бути оманчиком ...
Філіп Кендалл

2
@PhilipKendall, можливо, небагато. Але я щиро зацікавлений у розумінні того, чому кожна реалізація Java-розробника має таку міцну зв'язок.
Матьє Гіндон

19
@PhilipKendall Мене викликає цікавість.
Щогли

8
@PhilipKendall Це так, як зграя, так, але в основі цього є дійсне тематичне питання. Можливо, мізер може бути відредагований?
Андрес Ф.

Мене не вражає аргумент "занадто багато конструкторських аргументів". Але мене ще менше вражають обидва ці приклади будівельника. Можливо, це тому, що приклади занадто прості, щоб продемонструвати нетривіальну поведінку, але приклад Java читає, як переплетений вільний інтерфейс, а приклад C # - це просто обхідний спосіб реалізації властивостей. Жоден приклад не робить жодної форми перевірки параметрів; конструктор хоча б підтвердив тип та кількість наданих аргументів, а це означає, що конструктор із занадто великою кількістю аргументів виграє.
Роберт Харві

Відповіді:


22

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

Для мене є одне питання: чи Builder є необхідною частиною API типу? Ось, чи є PersonBuilder необхідною частиною Person API?

Цілком обґрунтовано, що перевірений на дійсність, незмінний та / або щільно зафіксований клас Person обов’язково буде створений через наданий ним Builder (незалежно від того, вбудований він чи сусідній). Роблячи це, Особа може зберігати всі свої поля приватними або пакетними-приватними та остаточними, а також залишати клас відкритим для модифікації, якщо мова йде про незавершені роботи. (Це може бути закритим для модифікації та відкритим для розширення, але це зараз не важливо, і особливо суперечливо для незмінних об'єктів даних.)

Якщо у випадку, якщо особа обов'язково створюється через наданий PersonBuilder як частину єдиного дизайну API рівня пакету, то кругла муфта просто чудова, і червоний прапор тут не гарантований. Зокрема, ви заявляли це як факт, що не може бути суперечливим, а не думкою чи вибором проекту API, так що, можливо, це сприяло поганій реакції. Я б не прихильнив вас до уваги, але я не звинувачую когось іншого в цьому, і решту дискусій про те, "що вимагає потворна заява" я залишу в довідковому центрі та мета . Відновлюються випадки; це не "ляпас".

Звичайно, якщо ви хочете залишити багато способів відкрити об’єкт Person, тоді PersonBuilder може стати споживачем або утилітоюкласу Person, від якого Особа не повинна безпосередньо залежати. Цей вибір здається більш гнучким - хто не хоче більше варіантів створення об’єктів? - але для того, щоб дотримуватися гарантій (таких як незмінність або серіалізативність) раптом Особі доводиться розкривати інший вид публічної техніки створення, такий як дуже довгий конструктор . Якщо Builder призначений для представлення або заміни конструктора з великою кількістю необов'язкових полів або конструктора, відкритого для модифікації, то довгий конструктор класу може бути деталлю реалізації, яка краще залишена прихованою. (Також майте на увазі, що офіційний та потрібний Builder не перешкоджає вам писати власну будівельну утиліту, тільки що ваша будівельна утиліта може споживати Builder як API, як у моєму первісному запитанні вище.)


ps: Зауважу, що згаданий вами зразок огляду коду, який ви перераховували, має непорушну особу, але в контрприкладі з Вікіпедії перелічено змінний автомобіль із геттерами та сетерами. Можливо, буде легше побачити необхідну техніку, пропущену з Автомобіля, якщо ви будете дотримуватися мови та інваріантів між ними.


Я думаю, я бачу ваш погляд на трактування конструктора як деталі реалізації. Це не здається, що Ефективна Java належним чином передає це повідомлення тоді (я не / не читав цю книгу), залишаючи тонну молодших (?) Java-розробників припускаючи, що "це так робиться" незалежно від здорового глузду . Я погоджуюсь, що приклади Вікіпедії погані для порівняння .. але ей, це Вікіпедія .. і FWIW я насправді не дбаю про голос; видалена відповідь все одно має позитивний чистий бал (і я маю шанс нарешті набрати [значок: тиск однолітків]).
Матьє Гіндон

1
Однак я не можу позбавитись думки про те, що тип із занадто великою кількістю аргументів конструктора - це проблема дизайну (пахне розбиттям SRP), що модель будівельника швидко засувається під килим.
Матьє Гіндон

3
@ Mat'sMug Моя публікація вище сприймає це як те, що класу потрібно стільки аргументів конструктора . Я б також ставився до цього як до дизайнерського запаху, якби ми говорили про довільну складову бізнес-логіки, але для об'єкта даних це не питання для того, щоб тип мав десять-двадцять прямих атрибутів. Це не є порушенням SRP для об'єкта, наприклад, для представлення однієї сильно типізованої записи в журналі .
Джефф Боуман підтримує Моніку

1
Справедливо. Я все ще не отримую String(StringBuilder)конструктор, який, здається, підкоряється тому "принципу", коли нормально тип має залежність від його конструктора. Мене було приголомшено, коли я побачив, що конструктор перевантажений. Це не так, як у Stringнього 42 параметри конструктора ...
Матьє Гіндон

3
@Luaan Незнайко. Я буквально ніколи не бачив, щоб він використовувався і не знав, що він існує; toString()є універсальним.
chrylis -на страйк-

7

Я розумію, наскільки це може бути прикро, коли в дебатах використовується "звернення до влади". Аргументи повинні стояти на власному ІМО, і хоча це неправильно вказувати на пораду такої шанованої людини, це насправді не може вважатися повноцінним аргументом сам по собі ала ми знаємо, що Сонце подорожує по землі, тому що Арістотель сказав так .

Сказавши це, я думаю, що передумова вашого аргументу начебто однакова. Ми ніколи не повинні зв'язувати два конкретні типи і називати це найкращою практикою, тому що ... Хтось так сказав?

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

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


Отже ... вища зв'язаність, менша згуртованість => щасливий кінець? хм ... Я бачу сенс у тому, що будівельник "розширює конструктор", але, щоб підняти інший приклад, я не бачу, що Stringробиться з перевантаженням конструктора, приймаючи StringBuilderпараметр (хоча це дискусійно, чи StringBuilderє будівельник , але це інша історія).
Матьє Гіндон

4
@ Mat'sMug Щоб бути зрозумілим, у мене дійсно немає сильних почуттів з цього приводу. Я не схильний використовувати схему конструктора, тому що я не створюю класів, у яких є багато державних вкладів. Я б, як правило, розклав такий клас з причин, які ви натякаєте на інше місце. Все, що я говорю, - якщо ви зможете показати, як такий підхід призводить до проблеми, не має значення, чи Бог зійшов і передав цей зразок будівельника Мойсею на кам'яному планшеті. Ти виграв'. З іншого боку, якщо ви не можете ...
JimmyJames

Одне легальне використання шаблону програми для побудови, який я отримав у власній базі коду, - це для глузування з VBIDE API; в основному ви надаєте вміст рядків кодового модуля, і конструктор надає вам VBEмакет у комплекті з Windows, активною панеллю коду, проектом та компонентом з модулем, який містить наданий вами рядок. Без цього я не знаю, як мені вдалося б написати 80% тестів у Rubberduck , принаймні, не забивши себе в очі кожен раз, коли я дивлюся на них.
Матьє Гіндон

@ Mat'sMug Це може бути дійсно корисним для знеструмлення даних у незмінні об’єкти. Ці види об'єктів, як правило, є мішками властивостей, тобто насправді не є ОО. Увесь простір проблем є таким ПДФО, що все, що допомагає зробити, звикнути, чи слідкувати вони за хорошою практикою чи ні.
JimmyJames

4

Щоб відповісти, чому тип поєднується з будівельником, необхідно зрозуміти, чому хтось використовує будівельник в першу чергу. Зокрема: Bloch рекомендує використовувати конструктор, коли у вас є велика кількість параметрів конструктора. Це не єдина причина використовувати будівельника, але я припускаю, що це найпоширеніша. Коротше кажучи, будівельник є заміною для цих параметрів конструктора, і часто будівельник оголошується в тому ж класі, який він будує. Таким чином, клас, що додає, вже має знання будівельника і передача його як аргумент конструктора цього не змінює. Ось чому тип поєднується з його builder.

Чому наявність великої кількості параметрів означає, що ви повинні використовувати будівельник? Ну а наявність великої кількості параметрів не є рідкістю у реальному світі, а також не порушує принцип єдиної відповідальності. Він також відстійний, коли у вас є десять параметрів String і намагаєтесь запам'ятати, які з них є, чи є п'ята чи шоста струна, чи не повинні бути нульовими. Будівельник полегшує керування параметрами, а також може робити інші цікаві речі, про які ви повинні прочитати книгу.

Коли ви використовуєте конструктор, який не поєднується з його типом? Як правило, коли компоненти об'єкта недоступні відразу, а об’єкти, які мають ці фрагменти, не потрібно знати про тип, який ви намагаєтеся побудувати, або ви додаєте конструктор для класу, над яким ви не маєте контролю .


Як не дивно, єдиний раз, коли мені потрібен був будівельник, коли я мав тип, який не має остаточної форми, як, наприклад, цей MockVbeBuilder , який створює макет API IDE; для одного тесту може знадобитися простий модуль коду, інший тест може потребувати двох форм і модуля класу - IMO , для чого розробник GoF. З цього приводу я можу почати використовувати його для іншого класу з божевільним конструктором ... але зчеплення просто не відчуває себе правильно.
Матьє Гіндон

1
@ Mat's Mug Клас, який ви представили, здається більш представницьким зразком дизайну Builder (від Design Patterns від Gamma та ін.), Не обов'язково простим класом для будівельників. Вони обидва будують речі, але це не одне і те ж. Крім того, вам ніколи не потрібен "будівельник", він просто полегшує інші речі.
Old Fat Ned

1

продукт творчого малюнка ніколи не повинен знати нічого про те, що його створює.

PersonКлас не знає , що його створення. У ньому є лише конструктор з параметром, і що? Назва типу параметра закінчується на ... Builder, який насправді нічого не примушує. Це лише назва.

Конструктор - це прославлений об'єкт конфігурації 1 . А об’єкт конфігурації насправді просто групує властивості разом, які належать один одному. Якщо вони належать один одному лише з метою передачі конструктору класу, так і буде! Як originі destinationв StreetMapприкладі мають тип Point. Чи можна було б передати окремі координати кожної точки на карту? Зрозуміло, але властивості Pointналежать разом, плюс ви можете мати на собі всілякі методи, що допомагають у їх побудові:

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

Тепер давайте додамо до суміші трохи командного шаблону , тому що якщо уважно придивитися, то будівельник - це фактично команда:

  1. Він інкапсулює дію: виклик методу іншого класу
  2. Він містить необхідну інформацію для виконання цієї дії: значення параметрів методу

Особлива частина для будівельника полягає в тому, що:

  1. Цей метод, який слід назвати, насправді є конструктором класу. Краще назвіть це креативним малюнком зараз.
  2. Значення для параметра - сам будівельник

Те, що передається Personконструктору, може створити його, але це не обов'язково. Це може бути звичайний старий об'єкт конфігурації або конструктор.


0

Ні, вони абсолютно помиляються, і ви абсолютно праві. Ця модель конструктора просто нерозумна. Це жодна справа особи, звідки беруться аргументи конструктора. Конструктор - це просто зручність для користувачів і нічого більше.


4
Дякую ... Я впевнений, що ця відповідь набере більше голосів, якщо вона збільшиться трохи більше, це виглядає як незакінчена відповідь =)
Матьє Гіндон

Так, я б +1, альтернативний підхід до будівельника, який забезпечує ті самі гарантії та гарантії без з’єднання
матовий фрік

3
Для контрагенту цьому див. Прийняту відповідь , в якій згадуються випадки, коли PersonBuilderнасправді є важливою частиною PersonAPI. На даний момент ця відповідь не аргументує свою справу.
Андрес Ф.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.