Конструктор с множеством параметров в сравнении с шаблоном


21

Хорошо известно, что если в вашем классе есть конструктор с множеством параметров, скажем, больше 4, то это, скорее всего, запах кода . Вам нужно пересмотреть, удовлетворяет ли класс SRP .

Но что если мы создадим объект, который зависит от 10 или более параметров, и в конечном итоге закончим настройкой всех этих параметров через шаблон Builder? Представьте, вы создаете Personобъект типа с его личной информацией, информацией о работе, информацией о друзьях, информацией об интересах, информацией об образовании и так далее. Это уже хорошо, но вы как-то задали те же более 4 параметров, верно? Почему эти два случая не считаются одинаковыми?

Ответы:


25

Шаблон Builder не решает «проблему» многих аргументов. Но почему много аргументов проблематично?

  • Они указывают, что ваш класс может делать слишком много . Однако есть много типов, которые на законных основаниях содержат много членов, которые не могут быть разумно сгруппированы.
  • Тестирование и понимание функции с множеством входов значительно усложняется - буквально!
  • Когда язык не предлагает именованных параметров, вызов функции не самодокументируется . Чтение вызова функции со многими аргументами довольно сложно, потому что вы понятия не имеете, что должен делать седьмой параметр. Вы даже не заметите, если 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();

* Паттерн 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 (операнд 1: 34, операнд 2: 56); операнд1 и операнд2 являются именами параметров в конструкторе. Они показаны в IDE, чтобы сделать код более читабельным. Таким образом, нет необходимости переходить к определению, чтобы узнать, что это за параметр.
гранат

9

Шаблон Builder ничего не решает за вас и не исправляет ошибки проектирования.

Если у вас есть класс, для которого нужно построить 10 параметров, то создание конструктора для его создания не сделает ваш проект лучше. Вы должны выбрать рефакторинг рассматриваемого класса.

С другой стороны, если у вас есть класс, возможно, простой DTO, где определенные атрибуты класса являются необязательными, шаблон компоновщика может упростить конструирование указанного объекта.


1

Каждая часть, например, личная информация, информация о работе (каждая занятость "ограничена"), каждый друг и т. Д., Должны быть преобразованы в свои собственные объекты.

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

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

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

Принцип 4 - это просто эмпирическое правило. Существуют ситуации, в которых требуется более 4 аргументов и для которых нет разумного или логичного способа их группировки. В этих случаях может иметь смысл создать объект, structкоторый можно заполнить напрямую или с помощью установщиков свойств. Обратите внимание, что structи в этом случае застройщик выполняет очень похожую задачу.

Однако в своем примере вы уже описали, какой логичный способ их сгруппировать. Если вы столкнулись с ситуацией, когда это не так, возможно, вы можете проиллюстрировать это другим примером.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.