Builder パターン
オブジェクトの生成、初期化を簡潔にするためのデザインパターン。
オブジェクトを生成(インスタンス化、new
)するためには、必ずコンストラクタを利用する必要がある。
ただし、「引数のバリエーションが多い」、「オプショナルな引数がある」などの場合、コンストラクタ内の処理が複雑化し、それを利用するクライアント側は引数が必須なのか、オプショナルなのかを理解して使用しなければならない。そのため、クライアントとコンストラクタ間の依存度が高くなる。
Builderパターンはこのようなシチュエーションで利用することで、以下の利点を得ることができる。
- コンストラクタ内の複雑な初期化を簡潔にできる
- クライアントにとってオブジェクトの初期化が行いやすくなる
Builderパターンではクライアントコードが直接コンストラクタを利用しない為、オブジェクト作成のロジックが隠蔽されることになる。この点ではFactoryパターンと似ているように思えるが、BuilderパターンはFactoryパターンと比較して、クライアントが必要なオブジェクトへの知識量が多い。
デザインパターン | オブジェクトに対して必要な知識量 |
---|---|
Builderパターン | 多い |
Factoryパターン | 少ない |
Builderパターンはオブジェクトの生成・初期化を簡潔にするためのデザインパターン
構成要素
Target
生成したいオブジェクト。
設定値を保持するデータオブジェクトとしての役割を持つ。
コンストラクタがprivate
で定義されることで、クライアントに対して直接インスタンス化する方法を提供しない。
ただし以下の説明ではprivate
でないところから説明を始め、段階的にprivate
化を行う。
基本的にTargetオブジェクトの生成はBuilderを介して行う。
Builder
オブジェクトの生成を担当する。
メソッドチェーンによって簡潔な記述方法を提供する。
Director
オブジェクトの作成過程を決定する。
Builderパターンにとって必須の要素ではないが、Directorの存在によって複雑な初期化ロジックがある場合にクライアントコードから複雑さを切り離す事ができる。
オプショナルな引数が多くある場合に、クライアント側にそれら引数の組み合わせを、より簡潔に選択する方法を提供する。
内部でBuilderを保持している。
全体図
パターン 〜その1〜
コンストラクタのオプショナルな引数に対する処理がBuilder
クラスの提供するメソッドチェーンによって簡潔な記述方法になる。
またDirector
がオブジェクトの生成方法を提供することで、クライアントがTarget
に依存しなくなる。
ただし、以下のコードにはクライアントが直接Target
クラスを生成できるという課題がある。これについてはパターン〜その2〜で対応することとしたい。
// 生成対象のオブジェクト
public class Target {
private int a;
private int b;
private int c;
// できればprivate化したいところ
Target(Builder builder) {
// Builderの持つプロパティをコピーする
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
int getA() {
return a;
}
int getB() {
return b;
}
int getC() {
return c;
}
}
// オブジェクトを生成する
class Builder {
int a;
int b;
int c;
Builder setA(int a) {
this.a = a;
return this;
}
Builder setB(int b) {
this.b = b;
return this;
}
Builder setC(int c) {
this.c = c;
return this;
}
Target build() {
return new Target(this);
}
}
// オブジェクトの生成手順を管理する
class Director {
private Builder builder;
Director(Builder builder) {
this.builder = builder;
}
// 生成方法1
Target constructA() {
return builder
.setA(1)
.build();
}
// 生成方法2
Target constructB() {
return builder
.setB(2)
.build();
}
// 生成方法3
Target constructC() {
return builder
.setC(3)
.build();
}
// 生成方法4
Target constructD() {
return builder
.setA(1)
.setB(2)
.setC(3)
.build();
}
}
// クライアント
public class Main {
public static void main(String[] args){
Builder builder = new Builder();
Director director = new Director(builder);
// オブジェクトがどのように初期化されているのかを知らなくて良い(疎結合)
Target targetA = director.constructA();
Target targetB = director.constructB();
Target targetC = director.constructC();
Target targetD = director.constructD();
// ただしコンストラクタがprivateではないので、クライアントが直接Targetを生成できる課題がある
Target target = new Target(builder);
}
}
パターン 〜その2〜
パターン〜その1〜には、クライアントコードから直接Target
クラスがインスタンス化できてしまう課題があったため、これを解消する。
まずBuilder
クラスをTarget
クラスの インナークラス として定義する。
Builder
は、Target
クラス(インナークラスの外側クラスは Enclosing Class と呼ばれる)のインスタンスが存在しない段階で機能する必要があるため、Target
クラスのstatic
メンバとして定義する。
static
インナークラスは Enclosing Class のインスタンスではなく、 Enclosing Class 自体に属する。
これによりTarget
クラスのコンストラクタをprivate
化する事ができ、クライアントから直接Target
がインスタンス化できなくなる。つまり、クライアントとTarget
が疎結合になる。
また、Director
はBuilderパターンにとって必須の要素ではないため、ここからは削除してしまい、Builderパターンに焦点を当てていくことにする。(Director
が持っていたオブジェクトに関わる知識はクライアント側に移動する。クライアント側はより多くの知識が必要になってしまうので、これが嫌な場合にはやはりDirector
が必要。)
public class Target {
private int a;
private int b;
private int c;
// privateなコンストラクタ
private Target(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
int getA() {
return a;
}
int getB() {
return b;
}
int getC() {
return c;
}
// インナークラス化したBuilder
static class Builder {
private int a;
private int b;
private int c;
Builder setA(int a) {
this.a = a;
return this;
}
Builder setB(int b) {
this.b = b;
return this;
}
Builder setC(int c) {
this.c = c;
return this;
}
Target build() {
return new Target(this);
}
}
}
// クライアント
public class Main {
public static void main(String[] args) {
Target.Builder builder = new Target.Builder();
// Directorの役割はクライアント側に移動
Target target1 = builder
.setA(1)
.build();
Target target2 = builder
.setB(2)
.build();
Target target3 = builder
.setC(3)
.build();
Target target4 = builder
.setA(1)
.setB(2)
.setC(3)
.build();
// 直接Targetをインスタンス化できなくなった
Target target = new Target(builder); // コンパイルエラー
}
}
パターン 〜その3〜
パターン3では、ラムダ式を利用してさらに記述を簡略化する。
関数型インターフェースの一つであるjava.util.function.Consumer<T>
を利用する。
Consumer<T>
には引数を一つ受け取り戻り値を返さないメソッドaccept()
が1つだけ定義されている。
@FunctionalInterface public interface Consumer<T>{ void accept(T t); }
この部分をラムダ式によって置き換える。
Builder setA(int a) {
this.a = a;
return this;
}
builder
.setA(1)
.setB(2)
.setC(3)
.build();
特にセッター部分の定義が不要になるため、フィールドが増える度にセッターを追加する必要がない。
import java.util.function.Consumer;
public class Target {
private int a;
private int b;
private int c;
private Target(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
int getA() {
return a;
}
int getB() {
return b;
}
int getC() {
return c;
}
// セッターの定義が必要なくなったBuilder
static class Builder {
public int a;
public int b;
public int c;
Builder with(Consumer<Builder> consumer) { // consumer: 関数型インターフェースが実装されたクラスのインスタンス
// 受け取ったインスタンスで実装されているメソッドを実行する
consumer.accept(this);
return this;
}
Target build() {
return new Target(this);
}
}
}
public class Main {
public static void main(String[] args) {
Target.Builder targetBuilder = new Target.Builder();
// ラムダ式によって Consumer<T> を実装した匿名クラスを生成
Target target1 = targetBuilder.with(builder -> {
builder.a = 1;
}).build();
Target target2 = targetBuilder.with(builder -> {
builder.b = 2;
}).build();
Target target3 = targetBuilder.with(builder -> {
builder.c = 3;
}).build();
Target target4 = targetBuilder.with(builder -> {
builder.a = 1;
builder.b = 2;
builder.c = 3;
}).build();
}
}