有多less构造函数参数太多?

假设您有一个名为Customer的类,其中包含以下字段:

  • 用户名
  • 电子邮件
  • 名字

我们还要说,根据您的业务逻辑,所有Customer对象都必须定义这四个属性。

现在,我们可以通过强制构造函数指定每个属性来轻松完成此操作。 但是当你被迫向Customer对象添加更多必需的字段时,很容易看出这会如何失控。

我已经看到有20多个参数被引入构造函数的类,使用它们只是一个痛苦。 但是,或者,如果您不需要这些字段,那么如果您依赖调用代码来指定这些属性,则可能会遇到未定义信息的风险,或者更糟糕的是,会导致对象引用错误。

有没有其他的select,或者你只是要决定X的构造函数参数太多,你可以住?

两种devise方法来考虑

本质模式

stream畅的界面模式

这些都是相似的意图,我们慢慢build立一个中间对象,然后创build我们的目标对象在一个单一的步骤。

stream畅的界面的一个例子是:

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(); } } 

我看到有些人推荐七个作为上限。 显然,人们一下子就能把七件事情掌握在脑海里, 他们只能记得四(苏珊Weinschenk, 每个devise师需要了解的人100件事 ,48)。 即便如此,我认为四是一个高地球轨道的东西。 但那是因为我的想法被鲍勃·马丁改变了。

清洁代码中 ,Bob叔叔认为三个参数的数量是一般的上限。 他提出了激进的要求(40):

一个函数的理想参数个数是零(niladic)。 接下来是一个(monadic)紧跟着两个(二元)。 应尽可能避免三个论点(三元)。 超过三个(多边形)需要非常特殊的理由 – 然后不应该使用。

他说这是因为可读性; 而且还因为可测性:

想象一下编写所有testing用例的困难,以确保所有不同的参数组合都能正常工作。

我鼓励你find他的书的副本,并阅读他对function论点的充分讨论(40-43)。

我同意那些提到单一责任原则的人。 我很难相信,一个需要超过两三个价值/对象,而没有合理的违约的类实际上只有一个责任,而且不会因另一个类被提取而更好。

现在,如果你通过构造函数注入你的依赖关系,那么Bob Martin关于调用构造函数是多么容易的论点并不太适用(因为通常在你的应用程序中只有一个点将你连接起来,或者你甚至有一个框架,为你做)。 然而,单一责任原则仍然是相关的:一旦一个class级有四个依赖关系,我认为这是一个正在做大量工作的气味。

然而,和计算机科学中的所有东西一样,有大量的构造函数参数是有效的。 不要扭曲你的代码,以避免使用大量的参数; 但是如果你使用了大量的参数,请停下来思考一下,因为这可能意味着你的代码已经被扭曲了。

在你的情况下,坚持构造函数。 信息属于客户,4个字段都可以。

如果你有许多必需和可选的字段,构造函数不是最好的解决scheme。 正如@boojiboy所说,很难阅读,而且编写客户端代码也很困难。

@contagiousbuild议使用可选属性的默认模式和设置器。 这要求这些领域是可变的,但这是一个小问题。

有效的Java 2的约书亚块说,在这种情况下,你应该考虑一个build设者。 书中的一个例子是:

  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)); 

我认为你的问题更多地是关于你的类的devise,而不是关于构造函数中的参数的数量。 如果我需要20个数据(参数)来成功地初始化一个对象,我可能会考虑分解这个类。

我认为“纯粹的面向对象”的答案是,如果对某些成员没有初始化的类的操作是无效的,那么这些成员必须由构造函数设置。 总是可以使用默认值的情况,但是我认为我们不考虑这种情况。 当API被修复时,这是一个很好的方法,因为在API公开之后更改单个允许的构造函数对于您和代码的所有用户来说都是一场噩梦。

在C#中,我对devise准则的理解是,这不一定是处理这种情况的唯一方法。 特别是对于WPF对象,您会发现.NET类倾向于使用无参数的构造函数,如果在调用方法之前数据尚未初始化为所需的状态,将会抛出exception。 这可能主要针对基于组件的devise; 我不能拿出一个以这种方式performance的.NET类的具体例子。 在你的情况下,肯定会增加testing的负担,以确保该类从未保存到数据存储区,除非属性已经被validation。 老实说,因为这个,我宁愿“构造函数设置所需的属性”的方法,如果你的API要么是石头或不公开。

我可以肯定的一件事就是可能有无数的方法可以解决这个问题,每个方法都会引入自己的问题。 最好的做法是尽可能多地学习模式,并select最适合的工作。 (这不是一个答案吗?)

如果你有很多参数,那么把它们一起打包到结构体/ POD类中,最好声明为正在构build的类的内部类。 这样,您仍然可以在使调用构造方法的代码合理可读时仍然需要这些字段。

史蒂夫·麦克康纳(Steve Mcconnell)在“代码完整版”(Code Complete)中写道,人们一次只能在脑海中保存更多的东西,所以这是我想要保留的数字。

我想这一切都取决于情况。 对于你的例子,一个客户类,我不会冒这个数据在需要的时候被定义的机会。 另一方面,传递一个结构会清除参数列表,但是在结构中仍然需要定义很多东西。

我想最简单的方法是find每个值的可接受的默认值。 在这种情况下,每个字段看起来像是需要构build的,所以可能会重载函数调用,以便如果在调用中没有定义某些内容,请将其设置为默认值。

然后,为每个属性设置getter和setter函数,以便可以更改默认值。

Java实现:

 public static void setEmail(String newEmail){ this.email = newEmail; } public static String getEmail(){ return this.email; } 

保持全局variables的安全也是一个很好的做法。

风格很重要,在我看来,如果有一个有20多个参数的构造函数,那么devise就应该改变。 提供合理的默认值。

我将它自己的构build/validation逻辑封装到自己的对象类似的领域。

举例来说,如果你有

  • 商家电话
  • BusinessAddress
  • 家庭电话
  • 家庭地址

我会做一个类,存储电话和地址连同一个标签指定其“家”或“业务”的电话/地址。 然后将这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); 

这应该使它看起来不像意大利面。

当然,如果你有很多的领域,必须有一些你可以提取出来的模式,这将会是一个很好的function单元。 并且为了获得更多可读的代码。

以下也是可能的解决scheme:

  • 展开validation逻辑,而不是将其存储在单个类中。 validation用户input他们,然后在数据库层等再次validation…
  • build立一个CustomerFactory类来帮助我构buildCustomer
  • @ marcio的解决scheme也很有趣…

除非超过1个参数,否则我总是使用数组或对象作为构造函数参数,并依靠错误检查来确保所需的参数在那里。

只需使用默认参数。 在支持默认方法参数(例如PHP)的语言中,您可以在方法签名中执行此操作:

public function doSomethingWith($this = val1, $this = val2, $this = val3)

还有其他方法可以创build默认值,例如支持方法重载的语言。

当然,如果您认为适当,也可以在声明字段时设置默认值。

这实际上只是归结为是否适合您设置这些默认值,或者您的对象应始终在施工中被指定出来。 这是一个只有你自己才能做出的决定。

我同意对Boojiboy提到的7个项目的限制。 除此之外,可能值得查看匿名(或专用)types,IDictionary或间接通过主键到另一个数据源。