この記事は、
Joshua BlochによるEffective Java第2版の章の無料翻訳です。
この記事では、多くのパラメーターを持つコンストラクターを使用して、クラスの使用を簡素化する3つの代替アプローチについて説明します。
食品の包装に関する碑文を提供するクラスがある場合を考えてみましょう。 この碑文は、製品の化学組成に責任があります。 このラベルには、いくつかの必須フィールドがあります:サービングサイズ、コンテナ内のサービング数、1サービングのカロリー量、および約20の追加フィールド:総脂肪、飽和脂肪、トランス脂肪、コレステロール、ナトリウムなど。 ほとんどの製品では、これらの追加フィールドのごく一部にゼロ以外の値が設定されています。
最初の選択肢(テレスコープコンストラクターパターン)
従来、プログラマーはTelescoping Constructorパターンを使用していました。 このパターンの本質は、いくつかのコンストラクターを提供することです:必須パラメーターを持つコンストラクター、1つの追加パラメーターを持つコンストラクター、2つの追加パラメーターを持つコンストラクターなど。 実際にどのように見えるかを示します。 簡潔にするために、4つの追加パラメーターのみを使用します。
// Telescoping constructor pattern - !
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 NutritionFacts( int servingSize, int servings) {
this (servingSize, servings, 0);
}
public NutritionFacts( int servingSize, int servings, int calories) {
this (servingSize, servings, calories, 0);
}
public NutritionFacts( int servingSize, int servings, int calories, int fat) {
this (servingSize, servings, calories, fat, 0);
}
public NutritionFacts( int servingSize, int servings, int calories, int fat,
int sodium) {
this (servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts( int servingSize, int servings, int calories, int fat,
int sodium, int carbohydrate) {
this .servingSize = servingSize;
this .servings = servings;
this .calories = calories;
this .fat = fat;
this .sodium = sodium;
this .carbohydrate = carbohydrate;
}
}
このクラスのオブジェクトを作成する場合は、必要なパラメーターのリストを使用してコンストラクターを使用します。
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
通常、コンストラクターを呼び出すには、設定したくない多くのパラメーターを渡す必要がありますが、いずれにしても、それらの値を渡す必要があります。 この場合、値0を
fat
フィールドに設定します。 パラメーターが6つしかないため、これはそれほど悪くないように思えるかもしれません。 しかし、パラメータの数が増えると、これは大きな問題を引き起こし始めます。
つまり、Telescoping Constructorパターンを使用すると、多くのパラメーターがある場合にクライアントコードを記述することが難しくなり、このコードを読み取ることがさらに難しくなります。 読者はこれらすべての値が何を意味するかを推測することしかできません。どのフィールドが参照されているかを知るには、パラメーターの位置を慎重に計算する必要があります。 同じタイプのパラメータの長いシーケンスは、微妙なエラーを引き起こす可能性があります。 クライアントがこれらのパラメーターの2つを誤って混同した場合、コンパイルは成功しますが、プログラムは正しく動作しません。
2番目の選択肢(JavaBeansパターン)
2番目のオプションは、多くのパラメーターを持つコンストラクターに遭遇したときのJavaBeansパターンです。 パラメーターなしでコンストラクターを呼び出してオブジェクトを作成し、セッターを呼び出して必要な追加パラメーターを設定します。
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
//
private int servingSize = -1; //
private int servings = -1; //
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() {
}
//
public void setServingSize( int val) {
servingSize = val;
}
public void setServings( int val) {
servings = val;
}
public void setCalories( int val) {
calories = val;
}
public void setFat( int val) {
fat = val;
}
public void setSodium( int val) {
sodium = val;
}
public void setCarbohydrate( int val) {
carbohydrate = val;
}
}
このアプローチには、Telescoping Constructorパターンの欠点はありません(オブジェクトは簡単に作成でき、結果のコードは読みやすくなります)。
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
残念ながら、JavaBeansパターンには重大な欠陥がないわけではありません。 構築は複数の呼び出しに分割されるため、JavaBeanが不安定な状態になり、部分的に構築が行われる場合があります。 不安定な状態にあるオブジェクトを使用しようとすると、エラーを含むコードから遠く離れたエラーが発生する可能性があり、そのためデバッグが困難になります。 また、JavaBeansパターンは、クラスを不変にする可能性を排除します。これは、マルチスレッド環境でのセキュリティを確保するためにプログラマー側の追加の努力を必要とします。
3番目の選択肢(ビルダーパターン)
幸いなことに、Telescoping ConstructorパターンのセキュリティとJavaBeansパターンの可読性を組み合わせた3番目の選択肢があります。 これは、
Builderパターンの形式です。 目的のオブジェクトを直接作成する代わりに、クライアントは必要なすべてのパラメーターを使用してコンストラクター(または静的ファクトリー)を呼び出し、ビルダーオブジェクトを受け取ります。 次に、クライアントはビルダーオブジェクトで
セッターのようなメソッドを呼び出して、追加の各パラメーターを設定します。 最後に、クライアントは
build()
メソッドを呼び出して、不変のオブジェクトを生成します。 ビルダーは、構築しているクラス内の静的な内部クラスです。 実際には次のようになります。
// Builder
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 {
//
private final int servingSize;
private final int servings;
// -
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;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
NutritionFactsは不変であり、デフォルトのパラメーター値はすべて1か所にあることに注意してください。 ビルダーのセッターメソッドは、このビルダーによって返されます。 したがって、呼び出しは一緒に連鎖できます。 クライアントコードは次のようになります。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
このクライアントコードは書きやすく、さらに重要なことには読みやすいです。 Builderパターンは、AdaおよびPythonで使用されるオプションの名前パラメーターを模倣します。
更新コメントによると
、それは提案されています:
問題のNutritionFactsクラスの場合、不変フィールドにゲッターを提供することは論理的です。 それ以外の場合、オブジェクトを作成しますが、使用することはできません。
JavaBeansの規則、つまりsetXXX()に従ってBuilderのフィールドに名前を付ける方が論理的です。 このメソッドはすでにJavaの事実上の標準であるため、このようなアプローチはコードの可読性を向上させます。
PS ハブラに関する私の最初の出版物 あまり蹴らないでください;)