コンストラクター引数の数が多すぎると言えるでしょうか?
-
09-06-2019 - |
質問
Customer というクラスがあり、次のフィールドが含まれているとします。
- ユーザー名
- Eメール
- ファーストネーム
- 苗字
また、ビジネス ロジックに従って、すべての Customer オブジェクトにこれら 4 つのプロパティが定義されている必要があるとします。
さて、コンストラクターにこれらの各プロパティを強制的に指定することで、これを非常に簡単に行うことができます。しかし、Customer オブジェクトにさらに必須フィールドを追加する必要がある場合、これがどのように制御不能になるかは簡単にわかります。
コンストラクターに 20 個以上の引数を取り込むクラスを見てきましたが、それらを使用するのは面倒です。ただし、これらのフィールドが必要ない場合、呼び出しコードに依存してこれらのプロパティを指定すると、情報が未定義になるリスク、またはさらに悪いことに、オブジェクト参照エラーが発生するリスクがあります。
これに代わるものはありますか、それとも X 個のコンストラクター引数が多すぎるかどうかを判断するだけでよいでしょうか?
解決
考慮すべき 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();
}
}
他のヒント
上限として7つを推奨している人もいます。どうやら、人が一度に 7 つのことを頭の中に保持できるというのは真実ではないようです。彼らは4人しか覚えていない(スーザン・ワインシェンク、 すべてのデザイナーが人間について知っておくべき100のこと, 、48)。それでも、私は 4 というのは地球の高い軌道のようなものだと考えています。しかし、それは私の考え方がボブ・マーティンによって変えられたからです。
で クリーンなコード, ボブおじさんはパラメータ数の一般的な上限として3つを主張しています。彼は次のような過激な主張をしています (40)。
関数の引数の理想的な数はゼロ (ニラディック) です。次に 1 (単項) が続き、その次に 2 (二項) が続きます。3 つの引数 (triadic) は可能な限り避けるべきです。3 つを超える (ポリアディック) には非常に特別な理由が必要であり、とにかく使用すべきではありません。
彼は読みやすさのためにこれを言っています。しかし、テスト容易性のためでもあります。
引数のさまざまな組み合わせがすべて適切に機能することを確認するために、すべてのテスト ケースを作成することの難しさを想像してみてください。
彼の本のコピーを見つけて、関数の引数に関する彼の完全な議論 (40-43) を読むことをお勧めします。
私は単一責任原則について言及した人たちに同意します。適切なデフォルトを持たずに 2 つまたは 3 つ以上の値/オブジェクトを必要とするクラスが実際に責任を負うのは 1 つだけであり、別のクラスを抽出した方が良いとは考えられません。
さて、コンストラクターを介して依存関係を注入している場合、コンストラクターを呼び出すのがいかに簡単であるかについての Bob Martin の議論はあまり当てはまりません (通常、アプリケーション内でコンストラクターを接続するポイントは 1 か所だけであるためです。それを行うフレームワークがあります)。ただし、単一責任の原則は依然として有効です。クラスに 4 つの依存関係がある場合、それは大量の作業を実行していると思われます。
ただし、コンピューター サイエンスのすべての事柄と同様、多数のコンストラクター パラメーターを持つ有効なケースが間違いなく存在します。多数のパラメータの使用を避けるためにコードを加工しないでください。ただし、多数のパラメータを使用する場合は、コードがすでに歪んでいる可能性があるため、立ち止まって少し考えてください。
あなたの場合は、コンストラクターを使用してください。情報は Customer に属し、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();
上の例は以下から引用したものです 効果的な Java 2
そして、それはコンストラクターにのみ当てはまります。ケント・ベックの引用 実装パターン:
setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);
四角形をオブジェクトとして明示的にすると、コードがわかりやすくなります。
setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));
「純粋な OOP」の答えは、特定のメンバーが初期化されていないときにクラスに対する操作が無効な場合、これらのメンバーはコンストラクターによって設定する必要があるということだと思います。デフォルト値を使用できるケースは常にありますが、そのケースは考慮されていないものとします。API が公開された後で許可される 1 つのコンストラクターを変更することは、開発者とコードのすべてのユーザーにとって悪夢となるため、API が修正された場合には、これは良いアプローチです。
C# では、設計ガイドラインについて私が理解していることは、これが必ずしも状況に対処する唯一の方法ではないということです。特に WPF オブジェクトの場合、.NET クラスはパラメーターなしのコンストラクターを優先する傾向があり、メソッドを呼び出す前にデータが望ましい状態に初期化されていない場合は例外をスローすることがわかります。ただし、これはおそらく主にコンポーネントベースの設計に特有のものです。このように動作する .NET クラスの具体的な例が思いつきません。あなたの場合、プロパティが検証されない限りクラスがデータ ストアに保存されないようにテストする負担が確実に増加します。正直なところ、このため、API が設定されているか公開されていない場合は、「コンストラクターが必要なプロパティを設定する」アプローチを好みます。
私が一つ言えることは、 午前 確かなのは、この問題を解決できる方法論はおそらく無数にあり、それぞれが独自の問題を引き起こすということです。最善の方法は、できるだけ多くのパターンを学び、その仕事に最適なものを選択することです。(それはあまりにも的外れな答えではないでしょうか?)
あなたの質問は、コンストラクターの引数の数よりもクラスの設計に関するものだと思います。オブジェクトを正常に初期化するために 20 個のデータ (引数) が必要な場合は、おそらくクラスを分割することを検討するでしょう。
Steve Mcconnell は Code Complete の中で、人は一度に 7 つ以上のことを頭の中に留めておくのが難しいと書いています。そのため、私はこの数字を維持するように努めています。
不快なほど多くの引数がある場合は、それらをまとめて構造体/POD クラスにパッケージ化し、できれば構築中のクラスの内部クラスとして宣言します。こうすることで、コンストラクターを呼び出すコードを読みやすくしながら、フィールドを必須にすることができます。
すべては状況次第だと思います。あなたの例のような顧客クラスの場合、必要なときにそのデータが未定義になる危険を冒すつもりはありません。逆に、構造体を渡すと引数リストは整理されますが、構造体にはまだ多くの内容を定義する必要があります。
最も簡単な方法は、各値の許容可能なデフォルトを見つけることだと思います。この場合、各フィールドを構築する必要があるように見えるため、関数呼び出しをオーバーロードして、呼び出しで何かが定義されていない場合にそれをデフォルトに設定することができます。
次に、デフォルト値を変更できるように、プロパティごとにゲッター関数とセッター関数を作成します。
Java 実装:
public static void setEmail(String newEmail){
this.email = newEmail;
}
public static String getEmail(){
return this.email;
}
これは、グローバル変数を安全に保つための良い習慣でもあります。
スタイルは非常に重要であり、20 個以上の引数を持つコンストラクターがある場合は、デザインを変更する必要があるように思えます。適切なデフォルトを提供します。
Boojiboy が言及している 7 項目の制限については同意します。さらに、匿名 (または特殊な) 型、IDictionary、または別のデータ ソースへの主キーを介した間接化にも注目する価値があるかもしれません。
同様のフィールドを、独自の構築/検証ロジックを備えた独自のオブジェクトにカプセル化します。
たとえば、次のようにします。
- ビジネス用電話機
- 職場の住所
- 自宅の電話
- 自宅住所
電話番号と住所を、「自宅」または「会社」の電話番号/住所を指定するタグとともに保存するクラスを作成します。次に、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
s - @marcioの解決策も興味深いです...
デフォルトの引数を使用するだけです。デフォルトのメソッド引数をサポートする言語 (PHP など) では、メソッド シグネチャでこれを行うことができます。
public function doSomethingWith($this = val1, $this = val2, $this = val3)
メソッドのオーバーロードをサポートする言語など、デフォルト値を作成する他の方法もあります。
もちろん、適切だと思われる場合は、フィールドを宣言するときにデフォルト値を設定することもできます。
結局のところ、これらのデフォルト値を設定することが適切かどうか、またはオブジェクトを建設時に常に指定する必要があるかどうかが問題になります。それは本当にあなたにしかできない決断です。
複数の引数でない限り、私は常に配列またはオブジェクトをコンストラクター パラメーターとして使用し、必要なパラメーターが存在するかどうかを確認するためにエラー チェックに頼っています。