- 過去に執筆した記事を日本語に翻訳しました
- 翻訳機を使用しているため、誤字や不自然な表現があるかもしれません
テレスコピックコンストラクタパターン (TelescopingConstructor Pattern)
まず、飲食注文システムを例に挙げさせていただきます
public class FoodOrder { // テレスコピックコンストラクタパターン
private final String mainDish; // 必須
private final String sideDish;
private final String drink;
private final String dessert;
private final String specialRequest;
public FoodOrder(String mainDish) {
this(mainDish, null);
}
public FoodOrder(String mainDish, String sideDish) {
this(mainDish, sideDish, null);
}
public FoodOrder(String mainDish, String sideDish, String drink) {
this(mainDish, sideDish, drink, null);
}
public FoodOrder(String mainDish, String sideDish, String drink, String dessert) {
this(mainDish, sideDish, drink, dessert, null);
}
public FoodOrder(String mainDish, String sideDish, String drink, String dessert, String specialRequest) {
this.mainDish = mainDish;
this.sideDish = sideDish;
this.drink = drink;
this.dessert = dessert;
this.specialRequest = specialRequest;
}
}
テレスコピックコンストラクタパターンの長所
- すべてのフィールドがfinalであるため、不変性が保証され、状態変更に対して安全でございます
- 不変オブジェクトであるため、複数のスレッドからアクセスしても安全でございます
- オブジェクト生成時にすべての有効性検査が行われるため、オブジェクトは常に一貫した状態を保持いたします
テレスコピックコンストラクタパターンの短所
1.可読性の低下
FoodOrder order = new FoodOrder("ステーキ", "サラダ", "ワイン", "ティラミス");
// これらの引数が何を意味しているのか把握するのは困難でございます
- 引数の数が増えると、コードの記述や読み取りが困難になります
- コードを読む際、各値の意味が何であるか混乱いたします
- 引数の個数を数えなければなりません
- 同じ型の引数が連続して並んでいる場合、発見が難しいバグに繋がる可能性がございます
2.使用の難しさ
FoodOrder order = new FoodOrder("ステーキ", null, null, "ティラミス");
- メインディッシュとデザートのみを注文したい場合、新しいコンストラクタを追加しなければなりません
- つまり、拡張性に欠けるということになります
3.保守の難しさ
// 新しいフィールド(例:食事時間)を追加する際、すべてのコンストラクタを修正しなければなりません
private final String mainDish;
private final String sideDish;
private final String drink;
private final String dessert;
private final String specialRequest;
private final String diningTime; // 追加が発生すると
// ...
public FoodOrder(String mainDish, String sideDish, String drink, String dessert, String specialRequest) {
this(mainDish, sideDish, drink, dessert, specialRequest, null);
}
// 新しいフィールドの追加に伴い、新しいコンストラクタの追加が必要となります
public FoodOrder(String mainDish, String sideDish, String drink, String dessert,
String specialRequest, String diningTime) {
this.mainDish = mainDish;
this.sideDish = sideDish;
this.drink = drink;
this.dessert = dessert;
this.specialRequest = specialRequest;
this.diningTime = diningTime;
}
- ドメインの変更により新しいフィールドが追加される場合、すべてのコンストラクタを修正しなければならず、つまり副作用が大きくなり、保守が困難となります
JavaBeansパターン (JavaBeans pattern)
public class FoodOrder { // JavaBeansパターン
private String mainDish = null; // 必須
private String sideDish = null;
private String drink = null;
private String dessert = null;
private String specialRequest = null;
public FoodOrder() {}
public void setMainDish(String mainDish) {
this.mainDish = mainDish;
}
public void setSideDish(String sideDish) {
this.sideDish = sideDish;
}
public void setDrink(String drink) {
this.drink = drink;
}
public void setDessert(String dessert) {
this.dessert = dessert;
}
public void setSpecialRequest(String specialRequest) {
this.specialRequest = specialRequest;
}
}
- テレスコピックコンストラクタパターンの短所が、JavaBeansパターンでは解消されます
FoodOrder order = new FoodOrder();
order.setMainDish("ステーキ");
order.setDessert("ティラミス");
// 必要な属性のみを設定可能でございます
上記のコードのように、必要な属性のみを設定することができます。
JavaBeansパターンの長所
- 各setterメソッドで設定する属性とその名前が明確に示されるため、適切な長さの場合、可読性と明確性を同時に確保できます
- 必要な属性のみを設定可能でございます
- オブジェクトを一度に生成する必要がなく、複数の段階に分けて構成することができます
- リフレクションを通じて動的に属性を設定することが可能でございます(finalではないため)
- 新しい属性の追加または削除が容易でございます
JavaBeansパターンの短所
1.オブジェクトの状態の一貫性を保証するのが困難でございます
FoodOrder order = new FoodOrder();
order.setMainDish("ステーキ");
// mainDishは設定されていますが、他のフィールドはnullの状態です
// この時点でorderオブジェクトは「不完全な」状態である可能性がございます
- テレスコピックコンストラクタパターンでは、引数の妥当性チェックがコンストラクタ内でのみ行われ、一貫性が保証されておりましたが、
JavaBeansパターンではその保証ができません - オブジェクトを生成するために複数のメソッドを呼び出さなければならず、オブジェクトが完全に生成される前は
一貫性(consistency)
が損なわれた状態になってしまう可能性がございます
2.スレッドの安全性が不足しております
FoodOrder order = new FoodOrder();
Thread thread1 = new Thread(() -> order.setMainDish("ステーキ"));
Thread thread2 = new Thread(() -> order.setMainDish("パスタ"));
- 2つのスレッドが同時に同じオブジェクトを修正することが可能でございます
- つまり、スレッドの安全性が不足しております
Freezeパターンとその限界
- JavaBeansパターンでは一貫性が損なわれる問題により、クラスを不変にすることができません
- これらの問題を解決するために、Freezeパターンを使用することが可能でございます
public class FoodOrder {
private String mainDish = null;
private String sideDish = null;
// その他のフィールド…
private boolean frozen = false;
public void setMainDish(String mainDish) {
if (frozen) throw new IllegalStateException("Object is freeze");
this.mainDish = mainDish;
}
// 他のsetterメソッド…
public void freeze() {
// 妥当性検査
if (mainDish == null) throw new IllegalStateException("The main dish is a must");
this.frozen = true;
}
}
freeze使用の長所
- freeze使用後は不変性が保証されます
- freeze使用後はオブジェクトの一貫性が保証されます
- freezeを使用するとスレッドの安全性が確保されます
freeze使用の短所
FoodOrder order = new FoodOrder();
order.setMainDish("ステーキ");
order.freeze(); // freezeを呼び出したかどうか、コンパイル時点では分かりません
-
freeze();
を呼び出したかどうか、コンパイラーが保証する方法がないため、ランタイムエラーに弱いです - 前述の短所と連なる短所として、状態管理が困難となり、保守性の低下を意味いたします
- フリーズ前に初期化が強制されます
ビルダーパターン (Builder Pattern)
public class FoodOrder {
private final String mainDish; // 必須
private final String sideDish;
private final String drink;
private final String dessert;
private final String specialRequest;
private FoodOrder(Builder builder) {
this.mainDish = builder.mainDish;
this.sideDish = builder.sideDish;
this.drink = builder.drink;
this.dessert = builder.dessert;
this.specialRequest = builder.specialRequest;
}
public static class Builder {
// 必須パラメータ
private final String mainDish;
// 任意パラメータ - デフォルト値で初期化
private String sideDish = null;
private String drink = null;
private String dessert = null;
private String specialRequest = null;
public Builder(String mainDish) {
this.mainDish = mainDish;
}
public Builder sideDish(String sideDish) {
this.sideDish = sideDish;
return this;
}
public Builder drink(String drink) {
this.drink = drink;
return this;
}
public Builder dessert(String dessert) {
this.dessert = dessert;
return this;
}
public Builder specialRequest(String specialRequest) {
this.specialRequest = specialRequest;
return this;
}
public FoodOrder build() {
// 妥当性検証
if (mainDish == null || mainDish.isEmpty()) {
throw new IllegalStateException("The main dish is a must");
}
return new FoodOrder(this);
}
}
}
ビルダーパターンの特徴
- テレスコピックコンストラクタパターンの安全性とJavaBeansパターンの可読性の両方を備えております
- クライアントは、必要なオブジェクトを直接作成するのではなく、必須パラメータのみを用いてコンストラクタを呼び出し、ビルダーオブジェクトを取得いたします
- その後、ビルダーオブジェクトが提供する一種のsetterメソッドで希望する任意パラメータを設定いたします
- 引数のないbuildメソッドを呼び出すことで、必要な(不変の)オブジェクトを取得いたします
- ビルダーは通常、生成するクラス内に静的メンバー・クラスとして定義されます
- FoodOrderクラスは不変であり、すべてのパラメータのデフォルト値が一か所に集約されております
- ビルダーのsetterメソッドは自身を返すため、連鎖的な呼び出しが可能でございます
ビルダーパターンの長所
FoodOrder order = new FoodOrder.Builder("ステーキ")
.sideDish("サラダ")
.dessert("ティラミス")
.build();
- 可読性と明確性を同時に満たすことができます
- 不変のオブジェクト生成が可能なため、不変性と一貫性が保証されます
- 任意の必要なパラメータのみを設定できるため、柔軟性が高いです
ビルダーパターンは、テレスコピックコンストラクタよりもクライアントコードの読み書きが非常に簡潔であり、JavaBeansパターンよりも安全でございます。
ビルダーパターンの短所
- 多くのデザインパターンと同様に、コードの複雑性が増します
- すべてのフィールドに対してsetterメソッドを生成する必要があるため、ボイラープレートコードが多くなります
- もし目的が単純なコードであるならば、不必要に多くのコードを要求してしまい、複雑になる可能性がございます
参考文献
- Effective Java Item 2