构造函数的参数数量过多会有什么问题?

164

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

  • UserName(用户名)
  • Email(电子邮件)
  • First Name(名字)
  • Last Name(姓氏)

假设根据您的业务逻辑,所有客户对象都必须定义这四个属性。

现在,我们可以通过强制构造函数指定每个属性来轻松地实现此目的。但是,当您被迫向Customer对象添加更多所需字段时,很容易看出这可能会失控。

我见过一些类将20多个参数传入其构造函数,使用起来非常麻烦。但是,如果您不需要这些字段,则存在未定义信息的风险,或者更糟糕的是,如果依赖调用代码指定这些属性,则可能出现对象引用错误。

是否有其他替代方案,还是您只能决定X数量的构造函数参数是否太多并接受它们呢?


很明显的答案是,比你需要的更多。 - Jodrell
15个回答

141

需要考虑的两种设计方法

Essence模式

Fluent Interface模式

这两种方法在意图上相似,即我们逐步构建一个中间对象,然后在单个步骤中创建目标对象。

流畅接口模式的示例:

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

2
我知道这个被称为“命名参数惯用语”:http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.18。相关内容:还有一个被称为“命名构造函数惯用语”:http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.8。 - Frank
@Frank,你的链接现在似乎有误,"Named Parameter Idiom"现在位于http://www.parashift.com/c++-faq-lite/named-parameter-idiom.html,"Named Constructor Idiom"现在在这里:http://www.parashift.com/c++-faq-lite/named-ctor-idiom.html。 - Lennart
8
我确实喜欢客户端代码的流畅性,但我不喜欢在CustomerBuilderCustomer中复制实例变量。此示例如果所有实例变量都是可选的,则可以接受,但如果所有实例变量都是必需的,并且有数十个,则我不确定您是否能够避免使用包含所有这些参数的构造函数。 如果没有带有所有必需属性的构造函数,那么作为客户端编码者,我将无法通过即将创建的类的接口看到该要求,这是我不喜欢的事情。 - dragan.stepanovic
1
客户生成器不更像是数据传输对象(DTO)吗? - Kamil Latosinski
6
建议在检查参数是否为空时,不要抛出NullPointException异常。这并不是NullPointException的用途。最好抛出IllegalArgumentException异常(“表示方法已收到非法或不适当参数而引发的异常。”请参见https://docs.oracle.com/javase/7/docs/api/java/lang/IllegalArgumentException.html)。 - Grmpfhmbl
显示剩余6条评论

41

我看到有些人建议将七作为上限。显然,人们一次只能记住四个东西,而不是七个(Susan Weinschenk,《100 Things Every Designer Needs to Know about People》,48页)。即便如此,我认为四个也是一个很高的地球轨道。但这是因为我的思维已经被Bob Martin改变了。

在《Clean Code》中,Uncle Bob主张将参数数量限制在三个以内。他做出了激进的声明(40):

函数的理想参数数目是零(niladic)。其次是一个(monadic),紧随其后的是两个(dyadic)。应尽量避免使用三个(triadic) 参数。超过三个(polyadic)需要非常特殊的理由,并且那种情况下也不应该使用。

他之所以这样说,是因为可读性;也因为可测试性:

想象一下编写所有测试用例以确保各种参数组合正常工作的困难。

我鼓励你找一本他的书,阅读他关于函数参数的完整讨论(40-43页)。

我同意那些提到单一责任原则的人。对我来说很难相信一个需要超过两个或三个值/对象且没有合理默认值的类只有一种责任,并且不会因为提取另一个类而变得更好。

现在,如果你通过构造函数注入依赖项,Bob Martin关于调用构造函数的简易性的论点并不完全适用(因为通常情况下你的应用程序中只有一个地方进行连接,或者你甚至拥有一个框架可以为你完成它)。然而,单一责任原则仍然相关:一旦一个类具有四个依赖项,我认为这是一种代码异味,说明它正在做大量的工作。

然而,与计算机科学中的所有事物一样,有大量构造函数参数的情况无疑是有效的。不要扭曲你的代码来避免使用大量的参数;但如果你确实使用了很多参数,请停下来仔细思考,因为这可能意味着你的代码已经很扭曲了。


1
我从不将参数传递给构造函数... 我会在一个init函数中传递所有参数,而这个参数是一个包含所有必需参数的对象。但是,我使用的是JavaScript... Java是什么? - andygoestohollywood
6
我一直在想,如果这个概念应用到“数据类”上,会有怎样的影响。因为“数据类”通常只是用来存储相关数据的。针对OP的问题,他的类也只是用来存储顾客信息的数据而已。你有没有考虑过如何在这种情况下减少参数的数量呢? - 0cd
@Puneet,还有一种类似的批评,即构造函数可能只需要3个参数,但所有这些参数都是大型复合类。因此,实质上您正在向构造函数发送60个参数,只是它们被打包起来了。 - LegendLength
7
成为一名函数式编程人员后,我已不再是之前那个 Uncle Bob 的信徒。我不再完全同意这个答案了。 - Keith Pinson
我只看了《代码整洁之道》的几章,但在阅读关于单元、双元和三元函数的内容时,我想知道构造函数是否是一个例外。我不记得它清楚地做出区分。我看到为了避免双元/三元函数(或任何更大的东西),可以创建一个类来包装参数。但是,在创建包装器类时,作者似乎没有给出如何定义包装器类属性的最佳实践。 - eaglei22
显示剩余2条评论

16

对于您的情况,请坚持使用构造函数。信息应放在客户端,4个字段足矣。

如果您有许多必填和可选字段,则构造函数不是最佳解决方案。正如@boojiboy所说,它很难阅读,编写客户端代码也很困难。

@contagious建议使用默认模式和可选属性的设置器。这会使字段变得可变,但这只是一个小问题。

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

8
如果构造函数中需要传入所有参数,那么你只是在将一个巨大的构造函数从一个地方移动到另一个地方。 - Andriy Drozdyuk
1
我知道这篇文章写于一段时间之前,但我确实喜欢这个解决方案。现在有了命名参数,这仍然被认为是一个好的做法吗? - eaglei22

10
我认为“纯面向对象编程”的答案是,如果在未初始化某些成员时,类上的操作无效,那么这些成员必须由构造函数设置。虽然可以使用默认值的情况总是存在,但我假设我们不考虑这种情况。当API固定时,这是一个好方法,因为在API公开后更改单个允许的构造函数将使您和所有用户的代码都变得混乱。
在C#中,我对设计指南的理解是,这不一定是处理该情况的唯一方式。特别是对于WPF对象,你会发现.NET类倾向于使用无参数的构造函数,并且在调用方法之前,如果数据尚未初始化为期望状态,则会抛出异常。尽管如此,这可能主要适用于组件化设计;我无法想出一个具体的.NET类,其行为与此类似。在您的情况下,确保在存储类到数据存储区之前已验证属性,肯定会增加测试的负担。说实话,因为这个原因,如果您的API已经确定或非公开,我更喜欢“构造函数设置所需属性”的方法。
我唯一确定的一件事是,可能有无数的方法可以解决这个问题,每种方法都引入了自己的一套问题。最好的方法是学习尽可能多的模式,并选择最适合任务的模式。(这是一个非常规的答案,不是吗?)

6

我认为你的问题更多地涉及到类的设计而不是构造函数中参数的数量。如果我需要20个数据(参数)才能成功初始化一个对象,我可能会考虑将类进行拆分。


1
有时候这是不可能的。考虑一个需要处理50列的Excel文件。MyExcelFileLine类拥有一个带有50个参数的构造函数的想法相当可怕。 - anar khalilov
@anarkhalilov 为什么你要使用一个类来表示Excel文件的行呢?将每一行存储在一个数组中,作为表示文件的类的成员,听起来更合理。 - Mehdi Charife
@anarkhalilov 为什么你要使用一个类来表示Excel文件的行呢?将每一行存储在一个数组中,作为表示文件的类的成员,听起来更合理。 - undefined

5

如果您有太多难以处理的参数,那么只需将它们打包到结构体/POD类中,最好声明为正在构建的类的内部类。这样一来,您仍然可以要求字段,同时使调用构造函数的代码相对容易阅读。


5

我认为这完全取决于具体情况。对于像您举的例子中的客户类,我不会冒险在需要时数据未定义。另一方面,传递结构体可以清理参数列表,但仍需在结构体中定义许多内容。


4

我会将类似的字段封装成一个具有自己构建/验证逻辑的对象。

例如,如果你有:

  • BusinessPhone
  • BusinessAddress
  • HomePhone
  • HomeAddress

我会创建一个类来存储电话和地址,并使用标签指定它是“家庭”还是“商务”电话/地址。然后将这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
  • @marcio的解决方案也很有趣...

4
史蒂夫·麦康奈尔在《代码大全》中指出,人类一次只能记忆七项左右的内容,因此我尽量将数量控制在这个范围内。

1
但是请参考Weinschenk的《设计师需要了解的100件事情》第48页。显然这已经被证明是错误的:四是更准确的上限。 - Keith Pinson

3

样式很重要,如果有一个带有20多个参数的构造函数,则应该修改设计。提供合理的默认值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接