最近、レベルアップのためにEffective Javaの第3版を読むようになりました。
そのなかで、「第2章 項目2 多くのコンストラクタパラメータに直面した時にはビルダーを検討する」を読んだ時にこう思いました。
「意外とBuilderって知られてない?」
なので、lombokで簡単に付与できるBuilderを紹介する記事を書きました。
なお、タイトルに〇〇編と書いてますが、Builder編以外は需要がなさそうなので書かないと思います。
lombokとは
getterやsetter等を自動生成してくれるライブラリ。かなり便利。
VSCodeやIntellij Ideaはプラグインを入れるだけで導入できる。
Eclipseはちょっと手間がかかった気がします(今はどうだろう…)
私はlombokで特に以下のアノテーションを使ってます。
詳しいことは調べてください。
他のアノテーションもいいよ!
っていうのがあれば教えてくれると嬉しいです。
- Data
- Value
- Builder
- NoArgsConstructor
- AllArgsConstructor
- RequeiredArgsConstructor
使い方
クラスにつけるだけで使えるようになります。
使い方も簡単です。
// 使えるようにする方法 @Builder // クラスにつけるだけ! class Book{ private final String isbn; // ISBN private final String author; // 筆者 private final int money; // 価格 }
// 使い方 Book.builder() .isbn("XXXXX") .author("きり丸") .money(500) .build();
下記と一緒のものが生成されます。
// コンストラクタでの生成するパターン new Book("XXXXX", "きり丸", 500); // or // setterで後で設定するパターン Book book = new Book(); book.setIsbn("XXXXX"); book.author("きり丸"); book.money(500);
ビルダーの強みとは
知らない人は他の言語でも使えるから知ってほしい。
コンストラクタでは
コンストラクタの場合、パラメータを設定する順番を意識する必要があります。
しかも、パラメータの順番を変えたい、増やしたいという場合にも弱いです。
開発者が一人ならまだしも、チーム開発で急にパラメータの順番を変更されたときは修正が大変です。
全項目がStringになっている場合は、最後まで気づけないことも…。
// 変更前 class Book{ private final String isbn; // ISBN private final String author; // 筆者 private final LocalDateTime end; // 絶版した時期 private final LocalDateTime start; // 販売開始時期 private final int money; // 値段 }
// 変更後(start/endの順番変更と編集者追加) class Book{ private final String isbn; // ISBN private final String author; // 筆者 private final String editor; // 編集者 private final LocalDateTime start; // 販売開始時期 private final LocalDateTime end; // 絶版した時期 private final int money; // 値段 }
LocalDateTime now = LocalDateTime.of(2020, 1, 2, 3, 4, 5); // 修正前のコンストラクタの場合 new Book("978XXXXXXXXXX", "きり丸", null, now, 500); // 修正後のコンストラクタの場合(順番の変更も意識しないといけない) new Book("978XXXXXXXXXX", "きり丸", "乱太郎", now, null, 500);
ビルダーでは
ビルダーの場合は、項目を選んで値を設定します。
なので、パラメータの順番が変わったり、増えた場合でも目的通りの値を設定できます。
変数が2,3個しかないときはコンストラクタでもいいですが、変数が増えたときのことを考えて最初からBuilderで作っておくと楽です。
// 修正前のBuilderでの生成 Book.builder() .isbn("978XXXXXXXXXX") .author("きり丸") .end(null) .start(now) .money(500) .build();
// 修正後のBuilderでの生成 Book.builder() .isbn("978XXXXXXXXXX") .author("きり丸") .editor("乱太郎") // この行追加だけ! .end(null) .start(now) .money(500) .build();
Builderの有効的な使い方
私は異常系テストを行うときもBuilderパターンで作ることが多いです。
正常系との相違点が非常にわかりやすいので、何が違うのかが分かりやすくなります。
他にも、この生成オブジェクトを元にINSERTする等のロジックを組むと、SQLのWHERE文の差分を簡単に確認できるようになります。
setterだと戻り値がvoid型になり、ワンライナーで書けないので意識が散りがちです。
// Builder型で正常系のデータを返却する public Book.Builder createSuccess(){ return Book.builder() .isbn("978XXXXXXXXXX") .author("きり丸") .editor("乱太郎") .start(now) .end(null) .money(500); } // 使用する直前で、buildして異常系のデータを生成する。 Book success = createSuccess().build(); Book notIsbn = createSuccess().isbn("あいうえお").build(); Book notAuthor = createSuccess().author(null).build(); ...
コンストラクタで生成した場合、差分が一目ではわかりません。
notAuthorの桁数が落ちているのにも気づけない。
テストのコツは、できるだけ意識するポイントを減らすことだと考えているので、パラメータの数だけ意識が散るのはNGです。
Book success = new Book("978XXXXXXXXXX", "きり丸", "乱太郎", now, null, 500); Book notIsbn = new Book("あいうえお", "きり丸", "乱太郎", now, null, 500); Book notAuthor = new Book("978XXXXXXXXX", null, "乱太郎", now, null, 500); ...
setterは悪くないけど、オブジェクト生成と違う行でセットするので、他にも値を変更している可能性を拭えない。
// setterの場合 Book notStart = createSuccess().build(); notStart.setStart(null); // 以降で setAuthor(null)とかするのでは…?と意識が散る。
Builderのデメリット
生成するときの行数が増えます。
あと、明らかに1つの属性しか持たないのであれば、コンストラクタの方が見やすいです。
Book book = new Book();
Book book = Book.builder().build();
終わりに
lombokで簡単に使える割には、他の言語でも有効なパターンなのでぜひ覚えて欲しいです。
テスト時にBuilder型で値をセットアップするのを思いついたときは、自分事ながら感動しました。
ぜひ、使ってみてください。
もしこの記事が役に立ったのであれば、はてぶ、Twitterでの記事の拡散、twitterのフォローもよろしくお願いします。
私の励みになります。