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

eenim 发布于 2018-04-25 constructor 最后更新 2018-04-25 15:00 293 浏览

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

  • 用户名
  • 电子邮件
  • 名字
  • 我们还要说,根据您的业务逻辑,所有Customer对象都必须定义这四个属性。 现在,我们可以通过强制构造函数指定每个属性来轻松完成此操作。但是,当您被迫向Customer对象添加更多必需字段时,很容易看出这会如何失控。 我见过有20多个参数存在于其构造函数中的类,使用它们只是一个痛苦。但是,或者,如果您不需要这些字段,那么如果您依赖调用代码来指定这些属性,就会面临未定义信息的风险,或者更糟糕的是,会导致对象引用错误。 有没有其他的选择呢,或者你是否需要决定是否有太多的构造函数参数可供您使用?
已邀请:

lvel

赞同来自:

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

zsed

赞同来自:

如果你有很多参数,那么把它们一起打包到结构体/ POD类中,最好声明为正在构建的类的内部类。这样,您仍然可以在调用构造器的代码合理可读时要求这些字段。

jculpa

赞同来自:

只需使用默认参数。在支持默认方法参数(例如PHP)的语言中,您可以在方法签名中执行此操作: public function doSomethingWith($this = val1, $this = val2, $this = val3) 还有其他方法可以创建默认值,例如支持方法重载的语言。 当然,如果您认为适当,您也可以在声明字段时设置默认值。 这实际上只是归结为您是否适合设置这些默认值,或者是否应始终在构建时指定您的对象。这是一个只有你自己才能做出的决定。

iest

赞同来自:

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

baut

赞同来自:

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

jdolor

赞同来自:

我认为最简单的方法是找到每个值可接受的默认值。在这种情况下,每个字段看起来都需要构建,所以可能会重载函数调用,以便在调用中未定义某些内容时将其设置为默认值。 然后,为每个属性设置getter和setter函数,以便可以更改默认值。 Java实现:

public static void setEmail(String newEmail){
    this.email = newEmail;
}
public static String getEmail(){
    return this.email;
}
这也是保持全局变量安全的好习惯。

zfugit

赞同来自:

史蒂夫·麦克康纳在“代码完成”中写道,人们一次只能在脑海中保存更多的东西,所以这是我试图留下的数字。

cet

赞同来自:

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

aid

赞同来自:

我认为“纯粹的OOP”的答案是,如果某些成员未初始化时,对该类的操作无效,那么这些成员必须由构造函数设置。总是可以使用默认值的情况,但我会假设我们没有考虑这种情况。当API被修复时,这是一个很好的方法,因为在API公开之后更改单个允许的构造函数对于您和代码的所有用户来说都是一场噩梦。 在C#中,我对设计准则的理解是,这不一定是处理这种情况的唯一方法。特别是对于WPF对象,您会发现.NET类倾向于支持无参数构造函数,并且在调用方法之前数据尚未初始化为期望状态时会抛出异常。这可能主要针对基于组件的设计;我无法想出一个以这种方式表现的.NET类的具体示例。在你的情况下,它确实会增加测试的负担,以确保该类从未保存到数据存储区,除非该属性已经过验证。老实说,因为这个,如果你的API要么被设置为石头或者不公开,那么我更喜欢“构造函数设置所需的属性”方法。 我确信的一件事就是可能有无数的方法可以解决这个问题,并且每个方法都会引入自己的问题。最好的做法是尽可能多地学习模式,并选择最适合的工作。 (这不是一个答案吗?)

non_et

赞同来自:

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

oqui

赞同来自:

两种设计方法需要考虑 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();
    }
}

fet

赞同来自:

在你的情况下,坚持构造函数。这些信息属于客户和4个领域。 如果您有许多必需和可选字段,构造函数不是最佳解决方案。正如@boojiboy所说,这很难阅读,而且编写客户端代码也很困难。 @contagious建议使用可选属性的默认模式和设置器。这要求这些字段是可变的,但这是一个小问题。 有效的Java 2的约书亚块说,在这种情况下,你应该考虑一个建设者。书中的一个例子是:

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

jquia

赞同来自:

我将它自己的构造/验证逻辑封装到自己的对象中。 举例来说,如果你有

  • 商家电话
  • 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的解决方案也很有趣......

zearum

赞同来自:

我看到一些人推荐七个作为上限。很明显,人们可以一次把七件事情放在脑海中,他们只能记得四个(苏珊Weinschenk,每个设计师需要知道的100件事情,48人)。即便如此,我认为四者是高地球轨道。但那是因为我的想法被鲍勃马丁改变了。 在Clean Code中,Bob叔叔认为三是参数数量的一般上限。他提出了激进的主张(40):

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic) followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn't be used anyway.
他说这是因为可读性;而且还因为可测试性:
Imagine the difficulty of writing all the test cases to ensure that all various combinations of arguments work properly.
我鼓励你找到他的书的副本,并阅读他对函数论点的全面讨论(40-43)。 我同意那些提到单一责任原则的人。我很难相信,一个需要超过两三个价值/对象而没有合理缺省的类实际上只有一个责任,并且在提取另一个类时不会更好。 现在,如果你通过构造函数注入你的依赖关系,Bob Martin关于调用构造函数是多么容易的论点并不太适用(因为通常在你的应用程序中只有一个点将你连接起来,或者你甚至有一个框架,为你做)。然而,单一职责原则仍然相关:一旦一个班级有四个依赖关系,我认为这是一种大量工作的气味。 但是,与计算机科学中的所有东西一样,无疑有大量构造函数参数的有效情况。不要扭曲你的代码以避免使用大量的参数;但是如果你确实使用了大量的参数,请停下来思考一下,因为这可能意味着你的代码已经被扭曲了。