71
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaAdvent Calendar 2019

Day 3

LombokのBuilderパターン解説

Last updated at Posted at 2019-12-02

はじめに

本記事は Java Advent Calendar 2019 の3日目の記事です。
LombokのBuilderパターンについて解説します。

「Builderパターン」とは?

デザインパターンの一種で、生成処理を積み上げてインスタンスを生成するパターンです。
処理を積み上げて生成していく様子から「Builder(建築者)パターン」と呼ばれています。

GoFのデザインパターンにもBuilderパターンがありますが、「Builder」が抽象クラスで「ConcreteBuilder(具体的建築者)」や「Director(監督者)」が存在します。
それに対し、LombokのBuilderパターンは具体的な「Builder」のみ存在します。
このように簡潔なBuilderパターンは、「Effective Java」という本で紹介されているようです。

環境

  • Java:10.0.2
  • IDE:IntelliJ IDEA 2019.2.3 (Community Edition)
  • Lombok plugin:0.27-2019.02

Builderパターンの適用方法

Builderパターンを適用するクラスを定義します。
Lombokでは、クラスに @Builder を付けることでBuilderが自動生成されます。

外部からの変更を防ぐため、各プロパティに privatefinal を付けています。

FooFooFooDto.java
import lombok.Builder;

@Builder
final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;
}

この実装により、以下のコードが自動生成されます。

Lombokで自動生成されたコード
final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;

    FooFooFooDto(String title, int number, boolean isFoo) {
        this.title = title;
        this.number = number;
        this.isFoo = isFoo;
    }

    public static FooFooFooDtoBuilder builder() {
        return new FooFooFooDtoBuilder();
    }

    public static class FooFooFooDtoBuilder {
        private String title;
        private int number;
        private boolean isFoo;

        FooFooFooDtoBuilder() {
        }

        public FooFooFooDto.FooFooFooDtoBuilder title(String title) {
            this.title = title;
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder number(int number) {
            this.number = number;
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder isFoo(boolean isFoo) {
            this.isFoo = isFoo;
            return this;
        }

        public FooFooFooDto build() {
            return new FooFooFooDto(title, number, isFoo);
        }

        public String toString() {
            return "FooFooFooDto.FooFooFooDtoBuilder(title=" + this.title + ", number=" + this.number + ", isFoo=" + this.isFoo + ")";
        }
    }
}

自動生成されたコードのクラス図

コードのみを読んでも頭に入りにくいため、クラス図を作成しました。

PlantUMLで表現する方法がわからなかったのですが、FooFooFooDtoBuilderクラスはstaticです。
関連の +-- は入れ子になっているクラスを表しています。今回だとFooFooFooDtoBuilderクラスはFooFooFooDtoクラスの内部で定義されています。

FooFooFooDto.png

追加された全処理を紹介します。
特に見てほしい処理は 太字 で表しています。

  • FooFooFooDtoクラス
    • 全プロパティに値を代入するコンストラクタ
      クラス図では表現していない
    • Builderを返す builder() メソッド
  • FooFooFooDtoBuilderクラス
    • コンストラクタ
      クラス図では表現していない
    • 全プロパティ
    • 全プロパティのセッター
      メソッド名はプロパティ名と同等
      戻り値に this を返す
    • 生成したいインスタンスを生成して返す build() メソッド
    • toString() メソッド

Builderを用いたインスタンスの生成方法

以下の手順でインスタンスを生成します。

  1. builder() メソッドでBuilderを生成する
  2. メソッドチェーンで生成処理(=プロパティのセット)を積み上げる
  3. build() メソッドを実行してインスタンスを生成する
Builderを用いたインスタンスの生成
final var fooFooFooDto = FooFooFooDto.builder() // 1. Builderを生成する
    .title("テスト") // 2. 生成処理を積み上げる
    .number(1)
    .isFoo(true)
    .build(); // 3. ビルドしてインスタンスを生成する

プロパティのセッターで this (ここではFooFooFooDtoBuilder)を返すのがポイントで、これによりメソッドチェーンでインスタンスを生成できます。
メソッドとプロパティが同名なのもわかりやすいです。

イメージはこんな感じです。
FooFooFooDtoImage.png

メリット

インスタンスの生成処理がスッキリする

メソッドチェーンでプロパティを初期化できるため、変数名を毎回書かずにスッキリします。
Setterでセットすると毎回変数名を書く必要があります。

Setterを使用
final var fooFooFooDto = new FooFooFooDto();
fooFooFooDto.setTitle("テスト");
fooFooFooDto.setNumber(1);
fooFooFooDto.isFoo(true);
Builderパターンを適用
final var fooFooFooDto = FooFooFooDto.builder()
    .title("テスト")
    .number(1)
    .isFoo(true)
    .build();

プロパティが他で変更されないことを確約できる

Setterでセットするとバラバラに書けてしまいます。

Setterだとプロパティをいつでも変更できる
final var fooFooFooDto = new FooFooFooDto();
// …
// いろいろな処理
// …
fooFooFooDto.setTitle("テスト");
// …
// いろいろな処理
// …
fooFooFooDto.setNumber(1);
// …
// いろいろな処理
// …
fooFooFooDto.isFoo(true);

バラバラに書けると、追うのに時間がかかって可読性が下がります。
もしまとまって書かれていても、他で変更されているか確認する手間がかかります。

どのプロパティを初期化しているかわかりやすい

お気づきの方もいると思いますが、上記の2つはコンストラクタで解決できます。

コンストラクタを使用
final var fooFooFooDto = new FooFooFooDto("テスト", 1, true);

しかし、コンストラクタは呼び出し時に引数名を書けないので、どのプロパティを初期化しているかわかりづらいです。
引数を書く順番も決まっていて、好きな順番で初期化できません。

コンストラクタでは引数の順番が決まっている
final var fooFooFooDto = new FooFooFooDto("テスト", 1, true); // `(1, "テスト", true)` とは書けない

Builderパターンを適用すると、どのプロパティを初期化しているかわかりやすく、初期化する順番も自由に決められます。

Builderパターンでは初期化する順番が自由
final var fooFooFooDto = FooFooFooDto.builder()
    .title("テスト")
    .number(1)
    .isFoo(true)
    .build();

final var fooFooFooDto = FooFooFooDto.builder()
    .number(1) // 初期化する順番を変えられる
    .title("テスト")
    .isFoo(true)
    .build();

引数が多いと渡す順番がわからなくなりますが、Builderパターンだとそれがありません。

引数が多くても実装しやすい
// コンストラクタを使用
// メソッドの定義を行き来しないと実装しづらい
final var manyFooDto = new ManyFooDto("テスト", 1, true, 2, 3, 4, false, true, false);

// Builderパターンを適用
final var manyFooDto = ManyFooDto.builder()
    .title("テスト")
    .number(1)
    .isFoo(true)
    .number2(2)
    .number3(3)
    .number4(4)
    .isBar(false)
    .isHoge(true)
    .isFuga(false)
    .build();

IDEによっては引数名が表示され、Builderパターンを適用しなくても実装しやすいことがあります。
それでも、コードレビュー時などIDEを使わないときは可読性が低いままなので、Builderパターンは効果的です。

メソッドチェーンがカッコいい

これが一番のメリットです 。(私調べ)
やはりメソッドチェーンで書けるとカッコいいです。
Stream APIと組み合わせるとカッコよさが増します。

適当なカッコいい処理
final var fooFooFooDtoList = barDtoList
    .stream()
    .filter(Objects::nonNull)
    .map(it -> {
        return FooFooFooDto.builder()
            .title(it.title)
            .number(it.number)
            .isFoo(true)
            .build();
    })
    .collect(Collectors.toList());

デメリット

### 同一パッケージ内だとインスタンスをnewできる

自動生成されるコンストラクタにはアクセス修飾子が付かないため、同一パッケージ内だとコンストラクタ経由でインスタンスを生成できてしまいます。
コンストラクタを private にすれば解決できますが、何かそうしない理由があるのかもしれません。

同一パッケージだとnewできる
final var fooFooFooDto = new FooFooFooDto("テスト", 1, true);

コメント を頂いて知ったのですが、 @Builder は内部で @AllArgsConstructor と同等のコンストラクタを生成しています。
アクセスレベルを private にした AllArgsConstructor を生成すれば、コンストラクタ経由でインスタンスが生成されることを防げます。

コンストラクタを明示的にprivateにする
import lombok.AccessLevel; // 追加
import lombok.AllArgsConstructor; // 追加
import lombok.Builder;

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 追加
public final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;
}
Lombokで自動生成されたコード
FooFooFooDto.java
public final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;

    // コンストラクタが `private` になる
    private FooFooFooDto(String title, int number, boolean isFoo) {
        this.title = title;
        this.number = number;
        this.isFoo = isFoo;
    }

    public static FooFooFooDtoBuilder builder() {
        return new FooFooFooDtoBuilder();
    }

    public static class FooFooFooDtoBuilder {
        private String title;
        private int number;
        private boolean isFoo;

        FooFooFooDtoBuilder() {
        }

        public FooFooFooDto.FooFooFooDtoBuilder title(String title) {
            this.title = title;
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder number(int number) {
            this.number = number;
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder isFoo(boolean isFoo) {
            this.isFoo = isFoo;
            return this;
        }

        public FooFooFooDto build() {
            return new FooFooFooDto(title, number, isFoo);
        }

        public String toString() {
            return "FooFooFooDto.FooFooFooDtoBuilder(title=" + this.title + ", number=" + this.number + ", isFoo=" + this.isFoo + ")";
        }
    }
}

インスタンスの生成方法が複数あると混乱するので、こちらの実装をデフォルトにしてほしいです…。

コメント を頂いて知ったのですが、BuilderのJavadocには「デフォルトが private 」と書かれていますw
https://github.com/rzwitserloot/lombok/blob/master/src/core/lombok/Builder.java#L36

Issue が上がっていますが、全部読んでもデフォルトが package private の理由はわかりませんでした。
「他のパッケージで継承してBuilderパターンを適用したいため」とコメントがありましたが、その場合は public にしないといけないので…。

プロパティの初期化を強制できない

コンストラクタではプロパティの初期化を強制できますが、Builderパターンでは強制できません。

プロパティの初期化を強制できない
final var fooFooFooDto = FooFooFooDto.builder()
    .title("テスト")
    .number(1)
    // `isFoo` が初期化されてなくてもビルドできる
    .build();

あとから追加したプロパティの初期化を忘れやすいため、頻繁にプロパティを追加するクラスには適用しない方がよさそうです。

Builderパターンの用途

データを受け渡すためだけに使うDTOに適用すると効果的です。

応用:Getterを追加する

プロパティには privtefinal を付けているため、このままだとプロパティの値を取得できません。
クラスに @Getter を付けると全プロパティにGetterが追加されるのでオススメです。

FooFooFooDto.java
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter // 追加
final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;
}

もし外部に公開したくないプロパティが存在する場合、プロパティごとにGetterを付ければOKです。

FooFooFooDto.java
import lombok.Builder;
import lombok.Getter;

@Builder
final class FooFooFooDto {
    @Getter //追加
    private final String title;

    private final int number; // Getterを付けなければ隠蔽される

    @Getter // 追加
    private final boolean isFoo;
}

応用:デフォルト値を設定する

プロパティに @Builder.Default を付けることで、デフォルト値を設定できます。
final を付けていても使えるので便利です。

FooFooFooDto.java
import lombok.Builder;
import lombok.Getter;

@Builder
final class FooFooFooDto {
    private final String title;

    @Builder.Default // 追加
    private final int number = 1;

    private final boolean isFoo;
}

なぜ final を付けていても使えるのかは、自動生成されるコードを見るとわかります。

Lombokで自動生成されたコード
final class FooFooFooDto {
    private final String title;
    private final int number;
    private final boolean isFoo;

    FooFooFooDto(String title, int number, boolean isFoo) {
        this.title = title;
        this.number = number;
        this.isFoo = isFoo;
    }

    // デフォルト値を返すメソッド
    private static int $default$number() {
        return 1;
    }

    public static FooFooFooDtoBuilder builder() {
        return new FooFooFooDtoBuilder();
    }

    public static class FooFooFooDtoBuilder {
        private String title;
        private int number;
        private boolean number$set; // セットしたかどうかをboolean型で持つ
        private boolean isFoo;

        FooFooFooDtoBuilder() {
        }

        public FooFooFooDto.FooFooFooDtoBuilder title(String title) {
            this.title = title;
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder number(int number) {
            this.number = number;
            this.number$set = true; // セットしたら `true` にする
            return this;
        }

        public FooFooFooDto.FooFooFooDtoBuilder isFoo(boolean isFoo) {
            this.isFoo = isFoo;
            return this;
        }

        public FooFooFooDto build() {
            // セットしていない場合にデフォルト値で初期化する
            return new FooFooFooDto(title, number$set ? number : FooFooFooDto.$default$number(), isFoo);
        }

        public String toString() {
            return "FooFooFooDto.FooFooFooDtoBuilder(title=" + this.title + ", number=" + this.number + ", isFoo=" + this.isFoo + ")";
        }
    }
}

応用:継承しているクラスにBuilderパターンを適用する

継承しているクラスにBuilderパターンを適用しようとすると、親クラスのプロパティを考慮しないためビルドエラーになります。

実装を工夫する必要があり、以下の記事が参考になります。
https://www.baeldung.com/lombok-builder-inheritance

おわりに

Builderパターンのよさがわかり、実際に取り入れていただけると嬉しいです😊

以上、 Java Advent Calendar 2019 の3日目の記事でした。
明日は @n_slender さんの記事です。

参考資料

71
54
8

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
71
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?