Builder パターン
オブジェクトの生成、初期化を簡潔にするためのデザインパターン。
オブジェクトの生成(インスタンス化)にはコンストラクタが必要になるが、クライアントコードが直接コンスタントラクタを使用する場合、もしコンストラクタにオプショナルな引数(与えなくても良い引数)があると、コンストラクタ内の処理が複雑化してしまう。
このような場合にBuilderパターンを利用することで、コンストラクタ内の複雑な初期化処理を簡潔にすることができ、またクライアントコードなにとってもオブジェクトの初期化が書きやすくなる。
Builderパターンではクライアントか直接コンストラクタを利用しない為、オブジェクト作成は隠蔽される。
その点ではFactoryパターンと似ているが、Builderパターンはオブジェクトに対する知識量が比較的多く必要な点が違いと言える。
Factoryパターンについてはこちらにまとめています。
構成要素
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 自体に属する。
static
インナークラスについてはこちらにまとめています。
これにより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();
// ラムダ式によってaccept(T 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();
}
}