有多少个构造函数参数太多?
假设我们有一个名为Customer的类,其中包含以下字段:
- 用户名
- 电子邮件
- 名
- 姓
还要说,根据业务逻辑,所有Customer对象都必须定义了这四个属性。
现在,我们可以通过强制构造函数指定这些属性中的每一个,来轻松实现此目的。但是,当我们被迫向Customer对象添加更多必填字段时,很容易看出这会如何失控。
我见过在类的构造函数中包含20多个参数的类,使用它们很痛苦。但是,或者,如果我们不需要这些字段,则会遇到具有未定义信息的风险,或者,如果我们依靠调用代码来指定这些属性,则对象引用错误会更加危险。
有其他选择吗?还是我们只需要确定X数量的构造函数参数对于我们来说是不是太多了?
解决方案
回答
除非它的参数不止1个,否则我总是将数组或者对象用作构造函数参数,并依靠错误检查来确保所需的参数在那里。
回答
如果我们有太多令人讨厌的参数,那么只需将它们打包到structs / POD类中,最好声明为我们正在构造的类的内部类。这样,在使调用构造函数的代码合理可读的同时,我们仍然可以要求使用字段。
回答
只需使用默认参数即可。在支持默认方法参数的语言(例如PHP)中,我们可以在方法签名中执行此操作:
公共函数doSomethingWith($ this = val1,$ this = val2,$ this = val3)
还有其他创建默认值的方法,例如支持方法重载的语言。
当然,如果我们认为合适的话,也可以在声明字段时设置默认值。
实际上,这取决于我们是否适合设置这些默认值,或者是否应该始终在构造时指定对象。这确实是只有我们才能做出的决定。
回答
样式非常重要,在我看来,如果有一个带有20个以上参数的构造函数,则应该更改设计。提供合理的默认值。
回答
我认为这完全取决于情况。对于示例(例如客户类),我不会冒险在需要时使数据未定义。另一方面,传递一个结构会清除参数列表,但是我们仍然需要在结构中定义很多东西。
回答
我认为最简单的方法是为每个值找到可接受的默认值。在这种情况下,每个字段看起来都是需要构造的,因此可能会使函数调用过载,以便在调用中未定义某些内容时将其设置为默认值。
然后,为每个属性创建getter和setter函数,以便可以更改默认值。
Java实现:
public static void setEmail(String newEmail){ this.email = newEmail; } public static String getEmail(){ return this.email; }
这也是确保全局变量安全的好习惯。
回答
史蒂夫·麦康奈尔(Steve Mcconnell)在《代码完成》中写道,人们很难一次将7件事保持在头脑中,所以这就是我试图保留的数字。
回答
我同意Boojiboy提到的7项限制。除此之外,可能值得研究匿名(或者专用)类型,IDictionary或者通过主键间接指向另一个数据源。
回答
我认为"纯粹的OOP"答案是,如果在未初始化某些成员时对类的操作无效,则这些成员必须由构造函数设置。总是可以使用默认值的情况,但我假设我们不考虑这种情况。固定API时,这是一种好方法,因为在API公开后更改单个可允许的构造函数对于我们和代码的所有用户来说都是一场噩梦。
在C#中,我对设计准则的了解是,这不一定是处理这种情况的唯一方法。特别是对于WPF对象,我们会发现.NET类倾向于使用无参数构造函数,并且如果在调用该方法之前未将数据初始化为所需的状态,则将引发异常。但是,这可能主要是特定于基于组件的设计。我无法提出以这种方式运行的.NET类的具体示例。对于我们而言,这肯定会增加测试负担,以确保除非已验证属性,否则绝不会将类保存到数据存储中。坦率地说,因此,如果API是一成不变的或者非公开设置的,我宁愿使用"构造函数设置所需的属性"方法。
我可以确定的一件事是,可能有无数种方法可以解决此问题,并且每种方法都会引入自己的一系列问题。最好的办法是学习尽可能多的模式,并为工作选择最佳模式。 (不是这样的答案吗?)
回答
我认为问题更多是关于类的设计,而不是关于构造函数中参数的数量。如果我需要20条数据(参数)来成功初始化一个对象,我可能会考虑分拆该类。
回答
两种设计方法要考虑
本质模式
流畅的界面模式
两者的意图相似,在于我们缓慢地构建一个中间对象,然后在一个步骤中创建目标对象。
实际使用的流畅界面的示例如下:
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(); } }
回答
在情况下,请坚持使用构造函数。该信息属于"客户",并且四个字段都可以。
如果我们有许多必填字段和可选字段,则构造方法不是最佳解决方案。正如@boojiboy所说,很难阅读,也很难编写客户端代码。
@contagious建议对默认属性使用默认模式和设置器。这要求字段是可变的,但这是一个小问题。
关于有效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));
回答
我将使用自己的构造/验证逻辑将类似的字段封装到自己的对象中。
举例来说,如果我们有
- 商务电话
- 公司地址
- 家庭电话
- 家庭地址
我将创建一个存储电话和地址以及指定其"住所"或者"公司"电话/地址的标记的类。然后将4个字段简化为一个数组。
ContactInfo cinfos = new ContactInfo[] { new ContactInfo("home", "+123456789", "123 ABC Avenue"), new ContactInfo("biz", "+987654321", "789 ZYX Avenue") }; Customer c = new Customer("john", "doe", cinfos);
那应该使它看起来不像意大利面条。
当然,如果我们有很多字段,那么必须有一些可以提取出来的模式,它可以很好地发挥其功能。并且也使代码更具可读性。
以下是可能的解决方案:
- 展开验证逻辑,而不是将其存储在单个类中。用户输入时进行验证,然后在数据库层再次验证等。
- 制作一个" CustomerFactory"类,以帮助我构建" Customer`s"
- @marcio的解决方案也很有趣...