153

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

  • 用户名
  • 电子邮件

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

现在,我们可以通过强制构造函数指定这些属性中的每一个来轻松地做到这一点。但是,当您被迫向 Customer 对象添加更多必填字段时,很容易看出这会如何失控。

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

是否有任何替代方案,或者您是否只需要决定 X 数量的构造函数参数是否太多而无法忍受?

4

15 回答 15

135

需要考虑的两种设计方法

本质图案_

流畅的界面模式

它们的意图相似,因为我们慢慢地构建一个中间对象,然后一步创建我们的目标对象。

流利界面的一个例子是:

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();
    }
}
于 2008-09-02T19:08:20.910 回答
39

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

Clean Code中,鲍勃叔叔主张将三个作为参数数量的一般上限。他提出了激进的主张(40):

函数的理想参数数量为零(niladic)。接下来是一个(单子),紧随其后的是两个(二元)。应尽可能避免使用三个参数(三元)。超过三个(多元)需要非常特殊的证明——无论如何都不应该使用。

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

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

我鼓励您找到他的书的副本并阅读他对函数参数的完整讨论(40-43)。

我同意那些提到单一职责原则的人的观点。我很难相信一个需要两个或三个以上的值/对象而没有合理默认值的类真的只有一个责任,并且提取另一个类不会更好。

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

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

于 2013-03-20T18:17:12.417 回答
15

在您的情况下,请坚持使用构造函数。该信息属于客户,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));
于 2008-09-02T19:26:11.210 回答
9

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

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

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

于 2008-09-02T18:55:05.467 回答
5

我认为这一切都取决于情况。对于您的示例,客户类,我不会冒险在需要时使该数据未定义。另一方面,传递一个结构会清除参数列表,但你仍然需要在结构中定义很多东西。

于 2008-09-02T18:50:50.873 回答
5

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

于 2008-09-02T18:55:35.653 回答
4

如果您有很多令人不快的参数,那么只需将它们一起打包成结构/POD 类,最好声明为您正在构建的类的内部类。这样,您仍然可以要求这些字段,同时使调用构造函数的代码具有合理的可读性。

于 2008-09-02T18:48:09.040 回答
4

Steve McConnell 在 Code Complete 中写道,人们很难一次在脑海中保留 7 件以上的东西,所以我尽量保持在这个数字之下。

于 2008-09-02T18:52:00.480 回答
4

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

比如说,如果你有

  • 商务电话
  • 营业地址
  • 家庭电话
  • 家庭地址

我会创建一个类,将电话和地址与一个标签一起存储,该标签指定它是“家庭”还是“企业”电话/地址。然后将 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可以帮助我构建Customers的课程
  • @marcio 的解决方案也很有趣……
于 2008-09-02T19:31:20.700 回答
2

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

于 2008-09-02T18:50:45.523 回答
2

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

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

Java实现:

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

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

这也是保持全局变量安全的好习惯。

于 2008-09-02T18:51:22.247 回答
1

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

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

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

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

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

于 2008-09-02T18:49:29.957 回答
1

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

于 2008-09-02T18:54:49.290 回答
1

在问题更面向对象的情况下,您可以使用 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";
}

在我看来,这是更简洁的代码,在这种情况下不需要承包商,这为其他必要的参数节省了空间。

于 2019-11-01T14:39:10.923 回答
-3

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

于 2008-09-02T18:47:40.757 回答