はじめに
本記事は 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が自動生成されます。
外部からの変更を防ぐため、各プロパティに private
や final
を付けています。
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クラス
- 全プロパティに値を代入するコンストラクタ
クラス図では表現していない - Builderを返す
builder()
メソッド
- 全プロパティに値を代入するコンストラクタ
-
FooFooFooDtoBuilderクラス
- コンストラクタ
クラス図では表現していない - 全プロパティ
-
全プロパティのセッター
メソッド名はプロパティ名と同等
戻り値にthis
を返す - 生成したいインスタンスを生成して返す
build()
メソッド -
toString()
メソッド
- コンストラクタ
Builderを用いたインスタンスの生成方法
以下の手順でインスタンスを生成します。
-
builder()
メソッドでBuilderを生成する - メソッドチェーンで生成処理(=プロパティのセット)を積み上げる
-
build()
メソッドを実行してインスタンスを生成する
final var fooFooFooDto = FooFooFooDto.builder() // 1. Builderを生成する
.title("テスト") // 2. 生成処理を積み上げる
.number(1)
.isFoo(true)
.build(); // 3. ビルドしてインスタンスを生成する
プロパティのセッターで this
(ここではFooFooFooDtoBuilder)を返すのがポイントで、これによりメソッドチェーンでインスタンスを生成できます。
メソッドとプロパティが同名なのもわかりやすいです。
メリット
インスタンスの生成処理がスッキリする
メソッドチェーンでプロパティを初期化できるため、変数名を毎回書かずにスッキリします。
Setterでセットすると毎回変数名を書く必要があります。
final var fooFooFooDto = new FooFooFooDto();
fooFooFooDto.setTitle("テスト");
fooFooFooDto.setNumber(1);
fooFooFooDto.isFoo(true);
final var fooFooFooDto = FooFooFooDto.builder()
.title("テスト")
.number(1)
.isFoo(true)
.build();
プロパティが他で変更されないことを確約できる
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パターンを適用すると、どのプロパティを初期化しているかわかりやすく、初期化する順番も自由に決められます。
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
にすれば解決できますが、何かそうしない理由があるのかもしれません。
final var fooFooFooDto = new FooFooFooDto("テスト", 1, true);
コメント を頂いて知ったのですが、 @Builder
は内部で @AllArgsConstructor
と同等のコンストラクタを生成しています。
アクセスレベルを private
にした AllArgsConstructor
を生成すれば、コンストラクタ経由でインスタンスが生成されることを防げます。
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で自動生成されたコード
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を追加する
プロパティには privte
と final
を付けているため、このままだとプロパティの値を取得できません。
クラスに @Getter
を付けると全プロパティにGetterが追加されるのでオススメです。
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です。
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
を付けていても使えるので便利です。
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 さんの記事です。
参考資料
- @Builder
- Builderパターン - デザインパターン入門 - IT専科
- 増補改訂版Java言語で学ぶデザインパターン入門
p.89