Шаблон 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);
}
}
В зависимости от приложения это может быть переломный момент, когда у фабричных методов практически нет аргументов, поскольку все они могут быть предоставлены менеджером зависимостей, или это может быть большой объем кода, который усложняет создание экземпляров без видимой выгоды. Такие фабрики более полезны для отображения интерфейсов на конкретные типы, чем для управления параметрами. Тем не менее, этот подход пытается решить коренную проблему слишком большого количества параметров, а не просто скрыть его с помощью достаточно свободного интерфейса.