有多少构造函数参数太多了?

时间:2008-09-02 18:44:32

标签: parameters refactoring constructor coding-style

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

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

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

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

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

有没有其他选择,或者你只需​​要决定X的构造函数参数数量是否太多而无法与你共存?

15 个答案:

答案 0 :(得分:108)

两种考虑的设计方法

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

答案 1 :(得分:26)

我看到有些人建议将7作为上限。显然,人们不能同时掌握七件事;他们只能记住四个(Susan Weinschenk,每件设计师需要知道的100件事情,48)。即便如此,我认为四是高地球轨道。但那是因为鲍勃·马丁改变了我的想法。

清洁代码中,Bob叔叔争辩说三个是参数数量的一般上限。他提出了激进的主张(40):

  

函数的理想参数数量为零(niladic)。接下来是一个(monadic)紧随其后的是两个(二元)。应尽可能避免三个论点(三元论)。超过三个(polyadic)需要非常特殊的理由 - 然后不应该使用。

他说这是因为可读性;但也因为可测试性:

  

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

我鼓励你找到他的书的副本并阅读他对函数论证的全面讨论(40-43)。

我同意那些提到单一责任原则的人。我很难相信一个需要超过两个或三个没有合理默认值的对象的类实际上只有一个责任,并且在提取另一个类时不会更好。

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

然而,就像计算机科学中的所有事情一样,无疑有大量构造函数参数的有效案例。不要扭曲代码以避免使用大量参数;但如果您确实使用了大量参数,请停下来仔细考虑,因为这可能意味着您的代码已经被扭曲。

答案 2 :(得分:13)

在你的情况下,坚持使用构造函数。该信息属于客户,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的Implementation Patterns

setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);

将矩形显式化为对象可以更好地解释代码:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));

答案 3 :(得分:5)

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

在C#中,我对设计指南的理解是,这不一定是处理这种情况的唯一方法。特别是对于WPF对象,您会发现.NET类倾向于支持无参数构造函数,并且如果在调用方法之前数据尚未初始化为所需状态,则会抛出异常。这可能主要针对基于组件的设计;我无法想出一个以这种方式运行的.NET类的具体示例。在您的情况下,它肯定会导致测试负担增加,以确保类永远不会保存到数据存储,除非已经验证了属性。老实说,因为这样,如果您的API要么一成不变,要么不公开,我更倾向于“构造函数设置所需的属性”方法。

确定的一件事是,可能有无数的方法可以解决这个问题,并且每个方法都会引入自己的一系列问题。最好的办法是学习尽可能多的模式,并选择最适合的工作。 (这不是答案吗?)

答案 4 :(得分:5)

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

答案 5 :(得分:4)

Steve Mcconnell在Code Complete中写道,人们无法一次保留更多7件事,所以这就是我试图留下的数字。

答案 6 :(得分:3)

如果你有许多不可说的参数,那么只需将它们打包成struct / POD类,最好声明为你正在构建的类的内部类。这样,您仍然可以在调用构造函数的代码合理可读时使用字段。

答案 7 :(得分:3)

我认为这完全取决于具体情况。对于像您的示例,客户类这样的东西,我不会冒险在需要时将数据定义为未定义的机会。另一方面,传递一个struct会清除参数列表,但是你仍然需要在struct中定义很多东西。

答案 8 :(得分:3)

我认为最简单的方法是为每个值找到可接受的默认值。在这种情况下,每个字段看起来都需要构造,因此可能会重载函数调用,以便在调用中未定义某些内容时将其设置为默认值。

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

Java实现:

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

public static String getEmail(){
    return this.email;
}

这也是保护全局变量安全的好方法。

答案 9 :(得分:2)

样式很重要,在我看来,如果有一个包含20多个参数的构造函数,那么应该改变设计。提供合理的默认值。

答案 10 :(得分:1)

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

答案 11 :(得分:1)

我使用自己的构造/验证逻辑将类似的字段封装到自己的对象中。

比如说,如果你有

  • 商家电话
  • 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 s
  • @ marcio的解决方案也很有意思......

答案 12 :(得分:0)

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

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

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

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

这实际上只取决于您是否适合设置这些默认值,或者是否应始终在构造中指出您的对象。这真的是一个只有你能做出的决定。

答案 13 :(得分:0)

在问题的更加面向对象的情况下,可以在C#中使用属性。如果创建对象的实例并没有多大帮助,但是假设我们有一个父类,该父类的构造函数中需要太多参数。
由于可以拥有抽象属性,因此可以利用它来发挥自己的优势。父类需要定义子类必须重写的抽象属性。
通常,一个类可能看起来像:

class Customer {
    private string name;
    private int age;
    private string email;

    Customer(string name, int age, string email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

class John : Customer {
    John() : base("John", 20, "John@email.com") { 

    }
}

使用太多参数会变得混乱和不可读。
而这种方法:

class Customer {
    protected abstract string name { get; }
    protected abstract int age { get; }
    protected abstract string email { get; }
}

class John : Customer {
    protected override string name => "John";
    protected override int age => 20;
    protected override string email=> "John@email.com";
}

我认为哪个代码更简洁,在这种情况下无需承包商,这为其他必要的参数节省了空间。

答案 14 :(得分:-1)

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