首页 文章

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

提问于
浏览
118

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

  • 用户名

  • 电子邮件

  • 名字

  • 姓氏

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

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

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

有没有替代方案,或者你只需要决定X的构造函数参数是否太多,你不能忍受?

14 回答

  • 106

    两种设计方法需要考虑

    essence模式

    fluent interface模式

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

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

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

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

    在清洁代码中,鲍勃叔叔争论三个作为参数数量的一般上限 . 他提出了激进的主张(40):

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

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

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

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

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

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

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

  • 0

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

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

  • 2

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

  • 4

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

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

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

  • 1

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

  • 1

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

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

    Java实现:

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

    这也是保持全局变量安全的好方法 .

  • 3

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

  • 2

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

  • -1

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

  • 13

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

    比如说,如果你有的话

    • BusinessPhone

    • 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的解决方案也很有意思......

  • 3

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

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

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

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

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

  • 26

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

相关问题