parameters - 有多少構造函數參數太多?




refactoring constructor (10)

兩種設計方法需要考慮

essence模式

流暢的界面模式

這些都是意圖相似的,因為我們慢慢建立一個中間對象,然後在一個步驟中創建我們的目標對象。

流暢的界面的一個例子是:

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的類,其中包含以下字段:

  • 用戶名
  • 電子郵件
  • 名字

我們還要說,根據您的業務邏輯,所有Customer對像都必須定義這四個屬性。

現在,我們可以通過強制構造函數指定每個屬性來輕鬆完成此操作。 但是,當您被迫向Customer對象添加更多必需字段時,很容易看出這會如何失控。

我見過有20多個參數存在於其構造函數中的類,使用它們只是一個痛苦。 但是,或者,如果您不需要這些字段,那麼如果您依賴調用代碼來指定這些屬性,就會面臨未定義信息的風險,或者更糟糕的是,會導致對象引用錯誤。

有沒有其他的選擇呢,或者你是否需要決定是否有太多的構造函數參數可供您使用?


只需使用默認參數。 在支持默認方法參數(例如PHP)的語言中,您可以在方法簽名中執行此操作:

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

還有其他方法可以創建默認值,例如支持方法重載的語言。

當然,如果您認為適當,您也可以在聲明字段時設置默認值。

這實際上只是歸結為您是否適合設置這些默認值,或者是否應始終在施工中指定您的對象。 這是一個只有你自己才能做出的決定。


在你的情況下,堅持構造函數。 這些信息屬於客戶和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:

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

將矩形作為對象進行顯式更好地解釋代碼:

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

如果你有很多參數,那麼把它們打包到結構體/ POD類中,最好聲明為你正在構建的類的內部類。 這樣,您仍然可以在調用構造器的代碼合理可讀時要求這些字段。


我將它自己的構造/驗證邏輯封裝到自己的對像中。

舉例來說,如果你有

  • 商家電話
  • BusinessAddress
  • 家庭電話
  • 家庭地址

我會創建一個存儲電話和地址的類,並附上一個標記,指明其“家庭”或“商業”電話/地址。 然後將這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的解決方案也很有趣...

我看到一些人推薦七個作為上限。 很明顯,人們可以一次把七件事情放在腦海中, 他們只能記得四個(蘇珊Weinschenk, 每個設計師需要知道的100件事情 ,48人)。 即便如此,我認為四者是高地球軌道。 但那是因為我的想法被鮑勃馬丁改變了。

Clean Code中 ,Bob叔叔認為三是參數數量的一般上限。 他提出了激進的主張(40):

函數的理想參數個數為零(niladic)。 接下來是一個(monadic),緊跟著兩個(二元)。 應盡可能避免三個論點(三元)。 超過三個(多邊形)需要非常特殊的理由 - 然後不應該被使用。

他說這是因為可讀性; 而且還因為可測試性:

想像一下編寫所有測試用例的困難,以確保所有參數的各種組合都能正常工作。

我鼓勵你找到他的書的副本,並閱讀他對函數論點的全面討論(40-43)。

我同意那些提到單一責任原則的人。 我很難相信,一個需要超過兩三個價值/對象而沒有合理缺省的類實際上只有一個責任,並且在提取另一個類時不會更好。

現在,如果你通過構造函數注入你的依賴關係,Bob Martin關於調用構造函數是多麼容易的論點並不太適用(因為通常情況下,應用程序中只有一個點將你連接起來,或者你甚至有一個框架,為你做)。 然而,單一責任原則仍然是相關的:一旦一個班級有四個依賴關係,我認為這是一種大量工作的氣味。

但是,與計算機科學中的所有東西一樣,無疑有大量構造函數參數的有效情況。 不要扭曲你的代碼以避免使用大量的參數; 但是如果你確實使用了大量的參數,請停下來思考一下,因為這可能意味著你的代碼已經被扭曲了。


我認為你的問題更多的是關於你的類的設計,而不是關於構造函數中參數的數量。 如果我需要20條數據(參數)才能成功初始化一個對象,那麼我可能會考慮分解這個類。


我認為最簡單的方法是找到每個值可接受的默認值。 在這種情況下,每個字段看起來都需要構建,因此可能會重載函數調用,以便在調用中未定義某些內容時將其設置為默認值。

然後,為每個屬性設置getter和setter函數,以便可以更改默認值。

Java實現:

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

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

這也是保持全局變量安全的好習慣。


除非它多於1個參數,否則我總是使用數組或對像作為構造函數參數,並依靠錯誤檢查來確保所需的參數在那裡。


風格很重要,在我看來,如果有一個有20多個參數的構造函數,那麼設計應該改變。 提供合理的默認值。