Конструктор з тоннами параметрів проти малюнка будівельника


21

Добре відомо, що якщо у вашому класі є конструктор з багатьма параметрами, скажімо, більше 4, то це, швидше за все, кодовий запах . Вам потрібно переглянути, якщо клас задовольняє SRP .

Але що робити, якщо ми побудуємо і об'єктуємо, що залежить від 10 або більше параметрів, і, врешті-решт, встановимо всі ці параметри за допомогою шаблону Builder? Уявіть собі, ви будуєте об’єкт Personтипу з його особистою інформацією, інформацією про роботу, інформацією про друзів, інформацією про інтереси, інформацією про освіту тощо. Це вже добре, але ви якимось чином задали ті самі більше 4 параметрів, правда? Чому ці два випадки не вважаються однаковими?

Відповіді:


25

Шаблон Builder не вирішує "проблему" багатьох аргументів. Але чому багато аргументів проблематичні?

  • Вони вказують, що ваш клас може робити занадто багато . Однак існує багато типів, які легітимно містять багато членів, які неможливо скласти з групою.
  • Тестування та розуміння функції з багатьма входами стає експоненціально складніше - буквально!
  • Якщо мова не пропонує названих параметрів, виклик функції не є самодокументуванням . Читати виклик функції з багатьма аргументами досить складно, оскільки ви поняття не маєте, що повинен робити 7-й параметр. Ви навіть не помітите, якщо 5-й та 6-й аргументи були замінені випадково, особливо якщо ви є динамічно набраною мовою або все відбувається як рядок, або коли останній параметр trueз якихось причин.

Підробка названих параметрів

У шаблоні Builder адресу тільки одна з цих проблем, а саме ремонтопридатність проблем викликів функцій з багатьма аргументами * . Отже, виклик функції, як

MyClass o = new MyClass(a, b, c, d, e, f, g);

може стати

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

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

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

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

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Очевидно, що імена типів A, B, C, ... повинні бути іменами самодокументірованни, що ілюструє значення параметра часто тим же ім'я , як ви б дати змінний параметр. У порівнянні з ідіомою конструктора для іменних аргументів необхідна реалізація набагато простіша, і, таким чином, менше ймовірність містити помилки. Наприклад (із синтаксисом Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

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

Існує ще один загальний підхід до імітації названих аргументів: єдиний абстрактний параметричний об'єкт, який використовує синтаксис вбудованого класу для ініціалізації всіх полів. На Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Однак можна забути поля, і це досить специфічне для мови рішення (я бачив використання в JavaScript, C # і C).

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

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

Підхід до кореневої проблеми

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

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

Іноді замість цього використовуються (абстрактні) фабрики або системи повного введення залежності , хоча вони можуть бути надмірними в більшості випадків використання. Зокрема, вони зменшують кількість аргументів лише у тому випадку, якщо багато з цих аргументів є квазіглобальними об'єктами або значеннями конфігурації, які не змінюються між інстанцією об'єктів. Наприклад , якщо параметри aі dбули глобальні-Робочі, ми отримаємо

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

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


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

В Android Studio 3.0 назва параметра в конструкторі відображається поряд із значенням, яке передається під час виклику конструктора. наприклад: новий A (operand1: 34, operand2: 56); opend1 і operand2 - це назви параметрів у конструкторі. Вони показані IDE, щоб зробити код більш читабельним. Отже, не потрібно переходити до визначення, щоб з’ясувати, що таке параметр.
гранат

9

Шаблон Builder нічого не вирішує для вас і не виправляє помилки дизайну.

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

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


1

Кожну частину, наприклад, особисту інформацію, інформацію про роботу (кожне заняття "затягує"), кожного друга тощо, слід перетворити на власні об'єкти.

Поміркуйте, як би ви реалізували функцію оновлення. Користувач хоче додати нову історію роботи. Користувач не хоче змінювати ані інші частини інформації, ані попередню історію роботи - просто зберігайте їх такими ж.

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

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

Настанова 4 - це лише правило. Є ситуації, які вимагають більше 4 аргументів, і немає жодного розумного або логічного способу їх групування. У цих випадках може бути доцільним створити запис, structякий можна заповнити безпосередньо або за допомогою налаштувань властивостей. Зауважте, що structі в цьому випадку будівельник виконує дуже схожу мету.

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

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