0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

デザインパターン Builderパターン

Last updated at Posted at 2024-06-12

Builder パターン

オブジェクトの生成、初期化を簡潔にするためのデザインパターン。

オブジェクトを生成(インスタンス化、new)するためには、必ずコンストラクタを利用する必要がある。

ただし、「引数のバリエーションが多い」、「オプショナルな引数がある」などの場合、コンストラクタ内の処理が複雑化し、それを利用するクライアント側は引数が必須なのか、オプショナルなのかを理解して使用しなければならない。そのため、クライアントとコンストラクタ間の依存度が高くなる。

Builderパターンはこのようなシチュエーションで利用することで、以下の利点を得ることができる。

  • コンストラクタ内の複雑な初期化を簡潔にできる
  • クライアントにとってオブジェクトの初期化が行いやすくなる

Builderパターンではクライアントコードが直接コンストラクタを利用しない為、オブジェクト作成のロジックが隠蔽されることになる。この点ではFactoryパターンと似ているように思えるが、BuilderパターンはFactoryパターンと比較して、クライアントが必要なオブジェクトへの知識量が多い。

デザインパターン オブジェクトに対して必要な知識量
Builderパターン 多い
Factoryパターン 少ない

Builderパターンはオブジェクトの生成・初期化を簡潔にするためのデザインパターン

構成要素

Target

生成したいオブジェクト

設定値を保持するデータオブジェクトとしての役割を持つ。

コンストラクタがprivateで定義されることで、クライアントに対して直接インスタンス化する方法を提供しない。

ただし以下の説明ではprivateでないところから説明を始め、段階的にprivate化を行う。

基本的にTargetオブジェクトの生成はBuilderを介して行う。

Builder

オブジェクトの生成を担当する。

メソッドチェーンによって簡潔な記述方法を提供する。

Director

オブジェクトの作成過程を決定する。

Builderパターンにとって必須の要素ではないが、Directorの存在によって複雑な初期化ロジックがある場合にクライアントコードから複雑さを切り離す事ができる。

オプショナルな引数が多くある場合に、クライアント側にそれら引数の組み合わせを、より簡潔に選択する方法を提供する。

内部でBuilderを保持している

全体図

builder.png

パターン 〜その1〜

コンストラクタのオプショナルな引数に対する処理がBuilderクラスの提供するメソッドチェーンによって簡潔な記述方法になる。

またDirectorがオブジェクトの生成方法を提供することで、クライアントがTargetに依存しなくなる。

ただし、以下のコードにはクライアントが直接Targetクラスを生成できるという課題がある。これについてはパターン〜その2〜で対応することとしたい。

Target.java
// 生成対象のオブジェクト
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;
    }
}
Builder.java
// オブジェクトを生成する
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);
    }
}
Director.java
// オブジェクトの生成手順を管理する
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();
    }
}
Main.java
// クライアント
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が必要。)

Target.java
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);
        }
    }
}
Main.java
// クライアント
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();

特にセッター部分の定義が不要になるため、フィールドが増える度にセッターを追加する必要がない。

Target.java
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);
        }
    }
}
Main.java
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();
    }
}

参考

徹底攻略Java SE 11 Gold問題集[1Z0-816]対応

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?