假设您有一个名为Customer的类,其中包含以下字段:
用户名
电子邮件
名字
姓
我们还要说,根据您的业务逻辑,所有Customer对象都必须定义这四个属性.
现在,我们可以通过强制构造函数指定每个属性来轻松地完成此操作.但是,当您被迫向Customer对象添加更多必需字段时,很容易看出它会如何失控.
我已经看到了在构造函数中加入20多个参数的类,使用它们只是一种痛苦.但是,或者,如果您不需要这些字段,则可能会遇到未定义信息的风险,或者更糟糕的是,如果您依赖调用代码来指定这些属性,则会引发对象引用错误.
有没有替代方案,或者你只需要决定X的构造函数参数是否太多,你不能忍受?
两种设计方法需要考虑
在本质模式
在流畅的界面模式
这两者在意图上都是相似的,因为我们慢慢建立一个中间对象,然后在一个步骤中创建我们的目标对象.
流畅的界面的一个例子是:
public class CustomerBuilder {
String surname;
String firstName;
String ssn;
public static CustomerBuilder customer() {
return new CustomerBuilder();
}
public CustomerBuilder withSurname(String surname) {
this.surname = surname;
return this;
}
public CustomerBuilder withFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public CustomerBuilder withSsn(String ssn) {
this.ssn = ssn;
return this;
}
// client doesn't get to instantiate Customer directly
public Customer build() {
return new Customer(this);
}
}
public class Customer {
private final String firstName;
private final String surname;
private final String ssn;
Customer(CustomerBuilder builder) {
if (builder.firstName == null) throw new NullPointerException("firstName");
if (builder.surname == null) throw new NullPointerException("surname");
if (builder.ssn == null) throw new NullPointerException("ssn");
this.firstName = builder.firstName;
this.surname = builder.surname;
this.ssn = builder.ssn;
}
public String getFirstName() { return firstName; }
public String getSurname() { return surname; }
public String getSsn() { return ssn; }
}
import static com.acme.CustomerBuilder.customer;
public class Client {
public void doSomething() {
Customer customer = customer()
.withSurname("Smith")
.withFirstName("Fred")
.withSsn("123XS1")
.build();
}
}
我看到有些人建议将7作为上限.显然,人们不能同时掌握七件事; 他们只能记住四个(Susan Weinschenk,每个设计师需要知道的关于人的100件事,48).即便如此,我认为四是高地球轨道.但那是因为鲍勃·马丁改变了我的想法.
在清洁代码中,鲍勃叔叔争论三个作为参数数量的一般上限.他提出了激进的主张(40):
函数的理想参数数量为零(niladic).接下来是一个(monadic)紧随其后的是两个(二元).应尽可能避免三个论点(三元论).超过三个(polyadic)需要非常特殊的理由 - 然后不应该使用.
他说这是因为可读性; 但也因为可测试性:
想象一下编写所有测试用例的难度,以确保所有各种参数组合都能正常工作.
我鼓励你找到他的书的副本,并阅读他对函数论证的全面讨论(40-43).
我同意那些提到单一责任原则的人.我很难相信一个需要超过两个或三个没有合理默认值的对象的类实际上只有一个责任,并且在提取另一个类时不会更好.
现在,如果你是通过构造函数注入你的依赖项,Bob Martin关于调用构造函数是多么容易的论点并没有那么多适用(因为通常在你的应用程序中只有一点你可以连接它,或者你甚至有一个为你做的框架).但是,单一责任原则仍然是相关的:一旦一个类有四个依赖关系,我认为这是一种气味,它正在做大量的工作.
但是,与计算机科学中的所有事物一样,无疑有大量构造函数参数的有效情况.不要扭曲代码以避免使用大量参数; 但是如果你确实使用了大量的参数,请停下来考虑一下,因为这可能意味着你的代码已经被扭曲了.
在你的情况下,坚持使用构造函数.该信息属于客户,4个字段都可以.
如果您有许多必需字段和可选字段,则构造函数不是最佳解决方案.正如@boojiboy所说,它很难阅读,而且编写客户端代码也很困难.
@contagious建议使用可选属性的默认模式和setter.这要求字段是可变的,但这是一个小问题.
有效Java 2上的Joshua Block说在这种情况下你应该考虑一个构建器.从这本书中摘取的一个例子:
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // required parameters private final int servingSize; private final int servings; // optional parameters private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; soduim = builder.sodium; carbohydrate = builder.carbohydrate; } }
然后像这样使用它:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8). calories(100).sodium(35).carbohydrate(27).build();
上面的例子来自Effective Java 2
这不仅适用于构造函数.引用Kent Beck的实施模式:
setOuterBounds(x, y, width, height); setInnerBounds(x + 2, y + 2, width - 4, height - 4);
将矩形显式化为对象可以更好地解释代码:
setOuterBounds(bounds); setInnerBounds(bounds.expand(-2));
我认为“纯粹的OOP”答案是,如果在未初始化某些成员时对类的操作无效,则这些成员必须由构造函数设置。总是可以使用默认值的情况,但我假设我们不考虑这种情况。固定API时,这是一种好方法,因为在API公开后更改单个可允许的构造函数对于您和您的所有代码用户来说都是一场噩梦。
在C#中,我对设计准则的了解是,这不一定是处理这种情况的唯一方法。特别是对于WPF对象,您会发现.NET类倾向于使用无参数构造函数,并且如果在调用该方法之前未将数据初始化为所需的状态,则将引发异常。但是,这可能主要是特定于基于组件的设计。我无法提出以这种方式运行的.NET类的具体示例。对于您而言,这肯定会增加测试负担,以确保除非已验证属性,否则绝不将类保存到数据存储中。坦率地说,因此,如果您的API是一成不变的或未公开的,那么我宁愿使用“构造函数设置所需的属性”方法。
有一两件事我很肯定的是,有可以解决这个问题,可能无数的方法,和他们每个人介绍自己的一套问题。最好的办法是学习尽可能多的模式,并为工作选择最佳模式。(这不是答案吗?)
我认为你的问题更多的是关于类的设计而不是构造函数中的参数数量.如果我需要20个数据(参数)来成功初始化一个对象,我可能会考虑拆分该类.