153

次のフィールドを含むCustomerというクラスがあるとします。

  • ユーザー名
  • Eメール
  • ファーストネーム
  • 苗字

また、ビジネスロジックに従って、すべてのCustomerオブジェクトにこれらの4つのプロパティを定義する必要があるとしましょう。

これで、コンストラクターにこれらの各プロパティを指定させることで、これを非常に簡単に行うことができます。ただし、Customerオブジェクトに必須フィールドを追加する必要がある場合に、これが制御不能になる可能性があることは簡単にわかります。

20以上の引数をコンストラクターに取り込むクラスを見てきましたが、それらを使用するのは面倒です。ただし、代わりに、これらのフィールドが必要ない場合は、未定義の情報が発生するリスクがあります。さらに悪いことに、これらのプロパティを指定するために呼び出し元のコードに依存している場合は、オブジェクト参照エラーが発生します。

これに代わるものはありますか、それともコンストラクター引数のX量が多すぎて、一緒に暮らすことができないかどうかを判断する必要がありますか?

4

15 に答える 15

135

考慮すべき 2 つの設計アプローチ

エッセンスのパターン

流れるようなインターフェイスパターン

これらはどちらも意図が似ており、中間オブジェクトをゆっくりと構築してから、1 つのステップでターゲット オブジェクトを作成します。

実際の流暢なインターフェイスの例は次のとおりです。

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();
    }
}
于 2008-09-02T19:08:20.910 に答える
39

上限として 7 つを推奨している方もいらっしゃるようです。明らかに、人々が一度に 7 つのことを頭に入れられるというのは事実ではありません。彼らは 4 つしか覚えていません (スーザン・ワインシェンク、デザイナーが人について知る必要がある 100 のこと、48)。それでも、私は4つが地球の高い軌道の何かだと考えています. しかし、それは私の考え方がボブ・マーティンによって変えられたからです。

Clean Codeで、ボブおじさんは、パラメーター数の一般的な上限として 3 つを主張しています。彼は過激な主張をしている (40):

関数の引数の理想的な数はゼロ (ニラディック) です。次に 1 つ (モナディック) が続き、すぐ後に 2 つ (ダイアディック) が続きます。3 つの引数 (triadic) は、可能な限り避ける必要があります。3 を超える (ポリアディック) には、非常に特別な正当化が必要です。

彼は読みやすさのためにこれを言っています。だけでなく、テスト容易性のために:

さまざまな引数の組み合わせがすべて適切に機能することを確認するために、すべてのテスト ケースを作成することの難しさを想像してみてください。

彼の本のコピーを見つけて、関数の引数に関する彼の完全な議論 (40-43) を読むことをお勧めします。

私は、単一責任の原則に言及した人々に同意します。適切なデフォルトを持たずに 2 つまたは 3 つ以上の値/オブジェクトを必要とするクラスが実際に 1 つの責任しか持たず、別のクラスを抽出した方がよいとは思えません。

ここで、コンストラクターを介して依存関係を注入する場合、コンストラクターを呼び出すのがいかに簡単であるかについてのボブ・マーティンの議論はあまり当てはまりません (通常、アプリケーションにはそれを接続するポイントが 1 つしかないためです。あなたのためにそれを行うフレームワークがあります)。ただし、単一責任の原則は依然として関連しています。クラスに 4 つの依存関係があると、クラスが大量の作業を行っているという匂いがすると思います。

ただし、コンピューター サイエンスのすべてのことと同様に、多数のコンストラクター パラメーターを持つ有効なケースが間違いなく存在します。多数のパラメーターを使用しないようにコードをゆがめないでください。ただし、多数のパラメーターを使用する場合は、コードが既にゆがんでいることを意味する可能性があるため、停止して少し考えてください。

于 2013-03-20T18:17:12.417 に答える
15

あなたの場合、コンストラクターに固執してください。情報は顧客に属し、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();

上記の例は、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));
于 2008-09-02T19:26:11.210 に答える
9

「純粋なOOP」の答えは、特定のメンバーが初期化されていないときにクラスの操作が無効である場合、これらのメンバーはコンストラクターによって設定される必要があるということだと思います。デフォルト値を使用できる場合は常にありますが、その場合は考慮していないと思います。APIが公開された後に単一の許可されるコンストラクターを変更することは、あなたとあなたのコードのすべてのユーザーにとって悪夢になるため、これはAPIが修正された場合の良いアプローチです。

C#では、設計ガイドラインについて私が理解しているのは、これが必ずしも状況を処理する唯一の方法ではないということです。特にWPFオブジェクトの場合、.NETクラスはパラメーターのないコンストラクターを優先する傾向があり、メソッドを呼び出す前にデータが望ましい状態に初期化されていない場合は例外をスローすることがわかります。ただし、これはおそらく主にコンポーネントベースの設計に固有のものです。このように動作する.NETクラスの具体的な例を思いつくことはできません。あなたの場合、プロパティが検証されていない限り、クラスがデータストアに保存されないことを確認するためのテストの負担が確実に増加します。正直なところ、このため、APIが石で設定されているか公開されていない場合は、「コンストラクターが必要なプロパティを設定する」アプローチをお勧めします。

私が確信していることの1つ、この問題を解決できる方法論はおそらく無数にあり、それぞれが独自の問題のセットを導入しているということです。最善の方法は、できるだけ多くのパターンを学び、その仕事に最適なパターンを選ぶことです。(それは答えのそのようなコップアウトではありませんか?)

于 2008-09-02T18:55:05.467 に答える
5

状況次第だと思います。あなたの例のような顧客クラスの場合、必要なときにそのデータが未定義になる可能性はありません。反対に、構造体を渡すと引数リストがクリアされますが、それでも構造体で定義することがたくさんあります。

于 2008-09-02T18:50:50.873 に答える
5

あなたの質問は、コンストラクターの引数の数よりも、クラスの設計に関するものだと思います。オブジェクトを正常に初期化するために20個のデータ(引数)が必要な場合は、おそらくクラスを分割することを検討します。

于 2008-09-02T18:55:35.653 に答える
4

口に合わないほど多くの引数がある場合は、それらを構造体/ PODクラスにパッケージ化するだけです。できれば、構築しているクラスの内部クラスとして宣言します。そうすれば、コンストラクターを呼び出すコードを適度に読みやすくしながら、フィールドを要求することができます。

于 2008-09-02T18:48:09.040 に答える
4

スティーブマコネルはCodeCompleteに、一度に7つ以上のものを頭に入れておくのに苦労していると書いています。

于 2008-09-02T18:52:00.480 に答える
4

同様のフィールドを、独自の構築/検証ロジックを使用して独自のオブジェクトにカプセル化します。

たとえば、

  • ビジネス用電話機
  • 職場の住所
  • 自宅の電話
  • 自宅住所

「自宅」または「会社」の電話/住所を指定するタグと一緒に電話と住所を格納するクラスを作成します。そして、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);

これにより、スパゲッティのように見えなくなります。

確かに、多くのフィールドがある場合は、独自の優れた機能単位を作成するために抽出できるパターンがいくつかあるはずです。また、より読みやすいコードも作成します。

また、次の解決策も考えられます。

  • 検証ロジックを 1 つのクラスに格納するのではなく、分散させます。ユーザーが入力したときに検証し、データベース層で再度検証するなど...
  • sのCustomerFactory構築に役立つクラスを作成するCustomer
  • @marcioのソリューションも興味深い...
于 2008-09-02T19:31:20.700 に答える
2

スタイルは非常に重要であり、20以上の引数を持つコンストラクターがある場合は、デザインを変更する必要があるように思われます。妥当なデフォルトを提供します。

于 2008-09-02T18:50:45.523 に答える
2

最も簡単な方法は、各値の許容可能なデフォルトを見つけることだと思います。この場合、各フィールドは作成する必要があるように見えるため、関数呼び出しをオーバーロードして、呼び出しで何かが定義されていない場合は、デフォルトに設定することができます。

次に、プロパティごとにgetter関数とsetter関数を作成して、デフォルト値を変更できるようにします。

Javaの実装:

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

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

これは、グローバル変数を安全に保つための良い習慣でもあります。

于 2008-09-02T18:51:22.247 に答える
1

デフォルトの引数を使用するだけです。デフォルトのメソッド引数をサポートする言語(たとえば、PHP)では、メソッドシグネチャでこれを行うことができます。

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

メソッドのオーバーロードをサポートする言語など、デフォルト値を作成する方法は他にもあります。

もちろん、適切であると判断した場合は、フィールドを宣言するときにデフォルト値を設定することもできます。

実際には、これらのデフォルト値を設定することが適切かどうか、またはオブジェクトを構築時に常に指定する必要があるかどうかにかかっています。それは本当にあなただけが下すことができる決定です。

于 2008-09-02T18:49:29.957 に答える
1

私はBoojiboyが言及する7項目の制限に同意します。それを超えて、匿名(または特殊な)タイプ、IDictionary、または別のデータソースへの主キーを介した間接参照を調べる価値があるかもしれません。

于 2008-09-02T18:54:49.290 に答える
-3

複数の引数がない限り、私は常に配列またはオブジェクトをコンストラクターパラメーターとして使用し、エラーチェックに依存して必要なパラメーターが存在することを確認します。

于 2008-09-02T18:47:40.757 に答える