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

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

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

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

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

我见过有些类在构造函数中包含20多个参数,使用它们非常痛苦。但是,或者,如果您不需要这些字段,那么您将面临拥有未定义信息的风险,或者更糟糕的是,如果您依赖于调用代码来指定这些属性,那么将面临对象引用错误的风险。

还有其他选择吗? 或者你只需要决定是否 X 数量的构造函数参数对你来说太多了?

92395 次浏览

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

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

只要使用默认参数。在支持默认方法参数(例如 PHP)的语言中,可以在方法签名中实现这一点:

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

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

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

它实际上只是归结为是否适合您设置这些默认值,或者您的对象是否应该在构造过程中一直被指定出来。这真的只有你能做决定。

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

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

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

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

Java 实现:

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


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

这也是保证全局变量安全的良好实践。

史蒂夫•麦康奈尔(Steve McConnell)在《代码完成》(Code Complete)一书中写道,人们在同一时间内很难在脑子里记住更多的7件事情,所以我试图记住的就是这个数字。

我同意 Boojiboy 提到的7项限制。除此之外,还可以查看匿名(或专门的)类型、 IDictionary 或通过主键间接访问其他数据源。

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

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

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

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

需要考虑的两种设计方法

本质模式

接口流畅模式

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

流畅界面的一个实际例子是:

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

在您的情况下,坚持使用构造函数。信息属于 Customer,4个字段就可以了。

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

建议使用默认模式和可选属性的 setter。这要求字段是可变的,但这是一个小问题。

Joshua Block 在有效 Java2中说,在这种情况下,您应该考虑构建器。书中的一个例子:

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

上面的例子取自 有效的 Java2

这并不仅仅适用于构造函数:

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

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

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

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

举个例子,如果你有

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

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

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

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

函数的理想参数数目为零(nilatic)。接下来是一个(一元)紧接着是两个(二元)。在可能的情况下,应该避免三个参数(三进制)。超过三个(多元)需要非常特殊的对齐 & mash; ,然后不应该使用无论如何。

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

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

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

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

现在,如果通过构造函数注入依赖关系,Bob Martin 关于调用构造函数有多么容易的观点就不太适用了(因为通常情况下,应用程序中只有一个地方需要连接它,或者甚至有一个框架可以为您做到这一点)。然而,单一责任原则仍然是相关的: 一旦一个类有四个依赖项,我认为这是一种味道,它正在做大量的工作。

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

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

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