Answers:
构建器模式无法解决许多参数的“问题”。但是,为什么许多论点有问题?
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模式最初旨在作为一种与表示形式无关的方法来组装复合对象,这比仅使用参数命名参数要大得多。特别是,构建器模式不需要流畅的界面。
这提供了一些额外的安全性,因为如果您调用不存在的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中的用法)。
幸运的是,构造函数仍可以验证所有参数,而在部分构造状态下创建对象时,情况并非如此,并且要求用户通过setter或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);
}
}
根据应用程序的不同,这可能是一个改变游戏规则的方法,工厂方法最终几乎没有参数,因为所有参数都可以由依赖项管理器提供,或者它可能是大量代码,使实例化变得复杂,没有明显的好处。这样的工厂对于将接口映射到具体类型比对参数进行管理更有用。但是,这种方法试图解决过多参数的根本问题,而不是仅仅使用一个流畅的界面将其隐藏。
每个部分,例如个人信息,工作信息(每个“职位”),每个朋友等,都应转换为自己的对象。
考虑如何实现更新功能。用户想要添加新的工作历史记录。用户不想更改信息的任何其他部分,也不想更改以前的工作历史记录,只需保持不变即可。
当信息很多时,不可避免地一次构建一条信息。您可以根据人类的直觉(使程序员更容易使用)或基于使用方式(通常同时更新哪些信息)在逻辑上对这些片段进行分组。
构建器模式只是捕获类型化参数列表(有序或无序)的一种方法,然后可以将其传递到实际的构造函数中(或构造函数可以读取生成器捕获的参数)。
准则4只是一个经验法则。在某些情况下,需要四个以上的参数,并且没有理智或逻辑的方式将它们分组。在这些情况下,创建一个struct
可以直接填充或使用属性设置器填充的对象可能是有意义的。请注意,struct
在这种情况下,和构建器的用途非常相似。
但是,在您的示例中,您已经描述了将它们分组的合理方法。如果您面临的情况并非如此,也许您可以举一个不同的例子来说明这一点。