Для чого нам потрібен клас Builder при реалізації схеми Builder?


31

Я бачив багато реалізацій шаблону Builder (головним чином на Java). Усі вони мають клас сутності (скажімо, Personклас) та клас будівельника PersonBuilder. Конструктор "укладає" різні поля та повертає a new Personіз переданими аргументами. Чому нам явно потрібен клас будівельника, а не розміщення всіх методів builder у самому 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, забезпечуючи таким чином незмінність.


4
Примітка: Звичайна назва цього шаблону chainable setters: D
Mooing Duck

4
Принцип єдиної відповідальності Якщо ви
вкладете

1
Ваша вигода може мати будь-який спосіб: я можу withNameповернути копію Особи зі зміненим лише полем імені. Іншими словами, це Person john = new Person().withName("John");може працювати навіть за умови, що Personце непорушно (і це звичайна модель у функціональному програмуванні).
Брайан Маккотчон

9
@MooingDuck: ще один поширений термін для нього - вільний інтерфейс .
Mac

2
@Mac Я думаю, що вільний інтерфейс трохи більш загальний - ви уникаєте використання voidметодів. Так, наприклад, якщо Personє метод, який друкує їх ім'я, ви все одно можете пов'язати його з Fluent Interface person.setName("Alice").sayName().setName("Bob").sayName(). До речі, я коментую їх у JavaDoc саме за вашою пропозицією @return Fluent interface- це загальний і досить зрозумілий характер, коли він застосовується до будь-якого методу, який return thisє в кінці його виконання, і це досить зрозуміло. Отже, Builder також буде робити вільний інтерфейс.
ВЛАЗ

Відповіді:


27

Це так, що ви можете бути незмінним І змоделювати названі параметри одночасно.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

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

Схоже, ви говорите про шаблон Josh Blochs Builder . Це не слід плутати з малюнком банди чотирьох будівельників . Це різні звірі. Вони обидва вирішують будівельні проблеми, але досить різними способами.

Звичайно, ви можете сконструювати об'єкт без використання іншого класу. Але тоді доведеться вибирати. Ви втрачаєте або можливість імітувати названі параметри мовами, які їх не мають (наприклад, Java), або ви втрачаєте можливість залишатися незмінними протягом усього життя об'єктів.

Незмінний приклад, не має імен параметрів

Person p = new Person("Arthur Dent", 42);

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

Імітаційний приклад названого параметра з традиційними сетерами. Незмінна.

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

Тут ви будуєте все за допомогою сетерів і моделюєте названі параметри, але ви більше не змінюєтесь. Кожне використання сетера змінює стан об'єкта.

Отже, ви отримаєте додавання класу - це ви можете зробити і те, і інше.

Перевірка може бути виконана в тому build()випадку, якщо помилка виконання для вікового поля, що відсутнє, вам достатньо. Ви можете оновити та застосувати те, що age()викликається помилкою компілятора. Тільки не з малюнком конструктора Josh Bloch.

Для цього вам потрібна внутрішня мова для домену (iDSL).

Це дозволяє вам вимагати, щоб вони дзвонили age()і name()перед тим, як дзвонити build(). Але ви не можете це зробити, просто повертаючись thisкожен раз. Кожна річ, яка повертається, повертає іншу річ, яка змушує вас називати наступне.

Використання може виглядати так:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Але це:

Person p = personBuilder
    .age(42)
    .build()
;

викликає помилку компілятора, оскільки age()діє лише для виклику того типу, який повертає name().

Ці iDSL є надзвичайно потужними (наприклад, JOOQ або Java8 Streams ) і дуже приємні у використанні, особливо якщо ви використовуєте IDE із заповненням коду, але вони досить дорого налаштовані. Я рекомендую зберегти їх для речей, на яких буде написано трохи вихідного коду.


3
А потім хтось запитує, чому ви повинні вказати ім'я до віку, і єдина відповідь - «бо комбінаторний вибух». Я впевнений, що можна уникнути цього в джерелі, якщо у вас є відповідні шаблони, такі як C ++, або повертається до використання іншого типу для всього.
Дедуплікатор

1
Негерметичних абстракція вимагає знань нижчого складності , щоб мати можливість знати , як використовувати його. Ось що я тут бачу. Будь-який клас, який не знає, як будувати себе, обов'язково поєднується з Builder, який має подальші залежності (такі є мета та природа концепції Builder). Один раз (не в таборі групи) проста потреба в екземплярі об'єкта вимагала 3 місяців, щоб скасувати саме таку кластерну групу. Я малюк тебе не.
radarbob

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

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

1
@radarbob Клас, який будується, все ще вміє будувати себе. Його конструктор відповідає за забезпечення цілісності об’єкта. Клас Builder - це лише помічник для конструктора, оскільки конструктор часто приймає велику кількість параметрів, що не робить дуже приємним API.
Ні U

57

Навіщо використовувати / надавати клас будівельника:

  • Зробити незмінні предмети - вигода, яку ви вже визначили. Корисно, якщо будівництво здійснює кілька кроків. FWIW, незмінність слід вважати важливим інструментом у нашому прагненні до написання програм, що потребують ремонту, та без помилок.
  • Якщо представлення кінцевого (можливо, незмінного) об’єкту виконання оптимізовано для читання та / або використання простору, але не для оновлення. Тут є хорошими прикладами String і StringBuilder. Неодноразово об'єднуючі рядки не дуже ефективні, тому StringBuilder використовує інше внутрішнє представлення, яке добре для додавання - але не настільки добре для використання місця, і не настільки добре для читання та використання, як звичайний клас String.
  • Чітко відокремити побудовані об’єкти від об'єктів, що будуються. Цей підхід вимагає чіткого переходу від недобудови до спорудженого. Для споживача немає способу плутати об'єкт недобудови з побудованим об'єктом: система типів буде це виконувати. Це означає, що іноді ми можемо використовувати такий підхід для того, щоб «потрапити до ями успіху», і, робивши абстракцію для використання іншими (або самими собою) (наприклад, API або шаром), це може бути дуже добре річ.

4
Щодо останньої точки кулі. Таким чином, ідея полягає в тому, що у PersonBuilderатестата немає, і єдиний спосіб перевірити поточні значення - закликати .Build()повернути a Person. Таким чином .Build може перевірити правильно побудований об’єкт, правильно? Тобто це механізм, призначений для запобігання використанню об'єкта "будується"?
AaronLS

@AaronLS, так, звичайно; складна перевірка може бути введена в Buildоперацію для запобігання поганим та / або недостатньо побудованим об'єктам.
Ерік Ейдт

2
Ви також можете перевірити об'єкт у своєму конструкторі, уподобання може бути особистим або залежно від складності. Що б ви не вибрали, головний момент полягає в тому, що коли ви маєте свій непорушний об’єкт, ви знаєте, що він правильно побудований і не має несподіваного значення. Незмінний об'єкт з неправильним станом не повинен відбуватися.
Вальфрат

2
@AaronLS у будівельника можуть бути геттери; це не суть. Цього не потрібно, але якщо у будівельника є геттери, ви повинні мати на увазі, що виклик getAgeбудівельника може повернутися, nullколи цей ресурс ще не вказаний. На противагу цьому, Personклас може примусити інваріанта, що вік ніколи не може бути null, що неможливо, коли будівельник і фактичний об'єкт змішуються, як з new Person() .withName("John")прикладом ОП , який щасливо створює Personбез віку. Це стосується навіть змінних класів, оскільки сетери можуть примусово застосовувати інваріанти, але не можуть застосувати початкові значення.
Холгер

1
@Holger ваші пункти вірні, але в основному підтримують аргумент, що Builder повинен уникати геттерів.
user949300

21

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

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

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


7
Приклад у запитанні містить просте порушення правил: не вказано вік дляjohn
Кейлет

6
+1 І дуже важко робити правила для двох полів одночасно без класу builder (поля X + Y не можуть бути більшими за 10).
Регінальд Блю

7
@ReginaldBlue ще складніше, якщо вони пов'язані "x + y є рівним". Наша початкова умова з (0,0) - це нормально, стан (1,1) теж добре, але немає послідовності оновлень однієї змінної, яка б підтримувала стан дійсним і отримує нас від (0,0) до (1) , 1).
Майкл Андерсон

Я вважаю, що це найбільша перевага. Всі інші - приємні.
Джакомо Альзетта

5

Трохи інший кут на це від того, що я бачу в інших відповідях.

withFooПідхід тут є проблематичним , так як вони поводяться як сетера , але визначені таким чином , щоб створити враження опори класу незмінності. У Java-класах, якщо метод модифікує властивість, звичайно запускати метод із "set". Я ніколи не любив це як стандарт, але якщо ти робиш щось інше, це здивує людей, і це не добре. Є ще один спосіб, який можна підтримати незмінністю за допомогою базового API, який ви маєте тут. Наприклад:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

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

Ви в основному побачите такий тип підходу, який використовується з паралельними структурами даних, такими як CopyOnWriteArrayList . І це натякає на те, чому незмінність важлива. Якщо ви хочете зробити свій код безпечним, майже завжди слід враховувати незмінність. У Java кожному потоці дозволено зберігати локальний кеш змінного стану. Для того, щоб один потік побачив зміни, внесені в інші потоки, потрібно застосувати синхронізований блок або іншу функцію сумісності. Будь-яке з них додасть до коду трохи накладних витрат. Але якщо ваші змінні є остаточними, то нічого робити. Значенням завжди буде те, для чого воно було ініціалізовано, і тому всі потоки бачать одне і те ж, незалежно від того.


2

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

Ще одна перевага полягає в тому, що ваш Personоб’єкт в кінцевому підсумку має більш простий життєвий цикл і простий набір інваріантів. Ви знаєте, що як тільки у вас є Person p, ви маєте p.nameі дійсне p.age. Жоден з ваших методів не повинен бути розроблений для вирішення таких ситуацій, як "добре що, якщо встановлено вік, але не ім'я, або що, якщо ім'я встановлено, але не вік?" Це зменшує загальну складність класу.


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

@VincentSavard Дійсно, але, на мою оцінку, це найчастіше використання. Для перевірки встановлення деякого "основного" набору значень, а також виконайте фантазійну обробку навколо деяких необов'язкових (встановлення за замовчуванням, виведення їх значення з інших значень тощо).
Олександр - Відновіть Моніку

Замість того, щоб сказати "перевірити, чи всі поля встановлені", я змінив би це на "перевірку того, що всі поля містять дійсні значення (іноді залежать від значень інших полів]" (тобто поле може допускати лише певні значення залежно від значення іншого поля). Це можна зробити в кожному сеттері, але комусь простіше встановити об'єкт, не маючи винятку, поки об'єкт не буде завершений і готовий до створення.
yitzih

Конструктор класу, що будується, несе кінцеву відповідальність за обґрунтованість своїх аргументів. Перевірка в конструкторі в кращому випадку зайва і може легко вийти з синхронізації з вимогами конструктора.
Ні U

@TKK Дійсно. Але вони підтверджують різні речі. У багатьох випадках, в яких я використовував Builder, завданням будівельника було побудувати вхідні дані, які в кінцевому підсумку отримують від конструктора. Наприклад, конструктор налаштований або з a URI, a File, або a FileInputStream, і використовує все, що було передбачено для отримання a, FileInputStreamщо в кінцевому підсумку переходить у виклик конструктора як аргумент
Олександр - Поновіть Моніку

2

Будівельник також може бути визначений для повернення інтерфейсу або абстрактного класу. Ви можете використовувати будівельник для визначення об'єкта, а конструктор може визначити, який конкретний підклас повернути, виходячи з того, які властивості встановлені, або для чого вони встановлені, наприклад.


2

Шаблон будівельника використовується для створення / створення об'єкта поетапно шляхом встановлення властивостей, і коли всі необхідні поля встановлені, потім повертайте кінцевий об'єкт за допомогою методу збирання. Новостворений об’єкт непорушний. Головне, що тут слід зазначити, це те, що об'єкт повертається лише тоді, коли викликається остаточний метод збірки. Це гарантує, що всі властивості встановлені на об'єкт, і, таким чином, об'єкт не знаходиться в непослідовному стані, коли він повертається класом builder.

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

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


@DawidO Ви зрозуміли мою думку. Основний акцент, який я намагаюся тут зробити, - це непослідовний стан. І це одна з головних переваг використання шаблону Builder.
Шубхем Бондарде

2

Повторне використання об’єкта builder

Як уже згадували інші, незмінність та перевірка ділової логіки всіх полів для перевірки об'єкта є головними причинами окремого об’єкта-будівельника.

Однак повторне використання - ще одна перевага. Якщо я хочу створити багато подібних об'єктів, я можу внести невеликі зміни до об'єкта будівельника та продовжити інстанціювання. Не потрібно відтворювати об’єкт будівельника. Таке повторне використання дозволяє будівельнику діяти як шаблон для створення багатьох незмінних об'єктів. Це невелика користь, але може бути корисною.


1

Насправді, ви можете мати методи будівельника на своєму класі і все одно мати незмінність. Це просто означає, що методи будівельника будуть повертати нові об'єкти, а не змінювати існуючі.

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

Звичайно, це означає, що у вас буде багато нових об’єктів, і ви не повинні цього робити, якщо ваші об’єкти дорого створювати.

Я використовував це в тестовому коді для створення відповідників Hamcrest для одного з моїх бізнес-об'єктів. Я не пам'ятаю точного коду, але він виглядав приблизно так (спрощено):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

Тоді я б використовував його так у своїх одиничних тестах (з відповідним статичним імпортом):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));

1
Це правда - і я думаю, що це дуже важливий момент, але він не вирішує останньої проблеми - це не дає вам шанс перевірити, що об’єкт перебуває у послідовному стані під час його побудови. Після побудови вам знадобиться така хитрість, як виклик валідатора перед будь-яким із ваших бізнес-методів, щоб переконатися, що абонент встановив усі методи (що було б жахливою практикою). Я думаю, що добре роздумувати через подібні речі, щоб зрозуміти, чому ми використовуємо шаблон Builder так, як ми робимо.
Білл К

@BillK true - об’єкт із, по суті, незмінними сеттерами (які щоразу повертають новий об’єкт) означає, що кожен повернутий об'єкт повинен бути дійсним. Якщо ви повертаєте недійсні об'єкти (наприклад, особа, яка nameне має age), концепція не працює. Ну, ви насправді можете повернути щось на кшталт, PartiallyBuiltPersonхоча це не дійсно, але здається, що хак маскує Builder.
ВЛАЗ

@BillK це те, що я мав на увазі з "якщо є спосіб отримати початковий (дійсний / корисний) об'єкт" - я припускаю, що і початковий об'єкт, і об'єкт, повернутий з кожної функції будівельника, є дійсними / послідовними. Ваше завдання як творця такого класу полягає в тому, щоб надати лише методики для побудови, які вносять ці послідовні зміни.
Paŭlo Ebermann
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.