LoginSignup
0
0

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

Last updated at Posted at 2024-06-12

Builder パターン

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

オブジェクトの生成(インスタンス化)にはコンストラクタが必要になるが、クライアントコードが直接コンスタントラクタを使用する場合、もしコンストラクタにオプショナルな引数(与えなくても良い引数)があると、コンストラクタ内の処理が複雑化してしまう。

このような場合にBuilderパターンを利用することで、コンストラクタ内の複雑な初期化処理を簡潔にすることができ、またクライアントコードなにとってもオブジェクトの初期化が書きやすくなる。

Builderパターンではクライアントか直接コンストラクタを利用しない為、オブジェクト作成は隠蔽される。

その点ではFactoryパターンと似ているが、Builderパターンはオブジェクトに対する知識量が比較的多く必要な点が違いと言える。

Factoryパターンについてはこちらにまとめています。

構成要素

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 自体に属する。

staticインナークラスについてはこちらにまとめています。

これにより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();

        // ラムダ式によって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();
    }
}

参考

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

0
0
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
0