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

  • 用户名
  • 电子邮件

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

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

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

有没有其他选择,或者您只需决定 X 数量的构造函数参数是否对您来说太多而无法忍受?

有帮助吗?

解决方案

需要考虑的两种设计方法

本质 图案

流畅的界面 图案

这些在意图上都很相似,因为我们慢慢地构建一个中间对象,然后一步创建我们的目标对象。

流畅界面的实际应用示例如下:

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

其他提示

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

干净的代码, ,Bob 叔叔主张将 3 作为参数数量的一般上限。他提出了激进的主张 (40):

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

他这样说是为了可读性;还因为可测试性:

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

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

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

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

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

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

上面的例子取自 有效的Java 2

这不仅适用于构造函数。引用肯特·贝克的话 实施模式:

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

将矩形明确作为对象可以更好地解释代码:

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

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

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

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

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

史蒂夫·麦康奈尔 (Steve McConnell) 在《Code Complete》中写道,人们很难一次在脑子里记住 7 件以上的事情,所以我尽量控制在这个数字以下。

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

我认为这一切都取决于具体情况。对于像您的示例(客户类别)这样的内容,我不会冒在需要时未定义该数据的风险。另一方面,传递结构会清除参数列表,但您仍然需要在结构中定义很多内容。

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

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

Java实现:

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

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

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

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

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

我将类似的字段封装到它自己的对象中,并具有自己的构造/验证逻辑。

举例来说,如果你有

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

我将创建一个类,将电话和地址与指定其是“家庭”还是“公司”电话/地址的标签一起存储。然后将 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 的解决方案也很有趣......

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top