前回の記事にて、まるでlombokに何の不満もないような書き方しました。
ただ、実際にデフォルトで使っているといくつか不満点がでました(タイトルにある通りざっくり3つ)。
なので、lombokの不満点を解消すべく、調べたのがこの記事です。
不満点一覧
- どんな変なデータでも生成できてしまうからNG
- クラス内部で別クラスを持つ場合にも、Stringとか基本型で渡したい
- デフォルト値があると嬉しい
- 生成したドメインを再度Builderの形に戻したい
不満点詳細
どんな変なデータでも生成できてしまうからNG
不変条件を満たさないまま生成できちゃうので、ありえないデータを生成しても気づけない。
不変条件を満たせるようにコンストラクタ・staticファクトリメソッドでオブジェクトを生成する前にガードするのが一般的だと考えられます。
しかし、lombokが作成するBuilderクラスだと、項目が全部Nullのデータというのも作れてしまいます。
データを作成した後に不変条件を満たせているか、Validationをかけることもできます。
しかし、Validationを掛け忘れることもあるので、100%の安全は担保できません。
なので、掛け忘れる自分のためにも、オブジェクトの生成前にガードしたいです。
クラス内部で別クラスを持つ場合にも、Stringとか基本型で渡したい
下記のオブジェクトを例にします。
Bookオブジェクトはその名の通り、本をモデリングしたものです。
Isbnとは、本に対する一意なIDです。
ISBNには、10桁or 13桁のデータしかありえない、というルールがあります。
class Book{ private final Isbn isbn; // ISBN } class Isbn{ private final String isbn; Isbn(String isbn){ if (isbn.length == 10 || isbn.length == 13){ return new Isbn(isbn); } throw new RuntimeException("ISBNではない!"); } }
これをデフォルトのBuilderで生成すると、こういうコードになります。
Book.builder() .isbn(new Isbn("978XXXXXXXXXX")) .build()
ISBNは大事な要素ではありますが、本を利用する際にISBNを意識したくありません。
上記のままでは、本を生成する前にISBNを生成する必要があります。
なので、理想としてはこういう形になってくれると嬉しいです。
Book.builder() .isbn("978XXXXXXXXXX") // String! .builder();
デフォルト値があると嬉しい
特別な深い意味はないです。
オブジェクトを新規生成する時に、あるべき初期状態が設定されていると非常に楽、程度です。
生成したドメインを再度Builderの形に戻したい
一度ドメインを生成した後は、Builderの形への戻し方が分からなかったです。
ぶっちゃけ、setterでも同じことはできます。
生成時はBuilder使っているのに、更新時はsetterを使う。
これが、自分の中で違和感があって、何とかしたいなって思ってました。
不満点の解消方法
どんな変なデータでも生成できてしまうからNG
lombokが自動生成するクラス名やメソッド名を記載しておくと、class生成時に上書きされません。
なので、自前でvalidateメソッドを用意しておいて、buildで生成する前にvalidateメソッドを呼べばいいです。
リフレクションで無理やりvalidateを呼ぶ方法じゃなくて助かりました。
デフォルトのBuilderクラス名は分かりやすいですが長いので、こちらで指定したほうが良いと思います。
デフォルトBuilder名:(TypeName)Builder 下記クラスだとCustomBuilderBookBuilderになります。 Builderクラスをそのまま使うことはありませんが、利用時に下記はちょっと長すぎる…。 CustomBuilderBook.CustomBuilderBookBuilder
@Value @Builder(builderClassName = "Builder") // Builderクラス名指定 public class CustomBuilderBook { private final int money; // staticである必要があります public static class Builder { // buildがデフォルト名。変更も一応可能。 public CustomBuilderBook build() { validate(); // Builderクラスがあると、 // 内部的にAllArgsConstructorが作られるようです return new CustomBuilderBook(money); } // 本当はBuilderクラスの内部メソッドではなく、 // CustomBuilderBookのメソッドにしたい… public void validate() { if (money == 0) { throw new RuntimeException("エラー!"); } } } }
クラス内部で別クラスを持つ場合にも、Stringとか基本型で渡したい
基本的には同じ要領です。
Builderクラス名を指定し、基本型で定義したい項目のメソッドを作成します。
下記のようにStringの基本型とISBNの自作型のどちらも作成しておくと、Builderでの生成時にどちらでも受け取れるようになります。
Stringの基本型だけあれば、元々生成していたlombokでISBNの自作型も自動生成してくれるかと思いましたが、そこまではやってくれませんでした。
@Value @Builder(builderClassName = "Builder") public class CustomBuilderBook { private final Isbn id; public static class Builder { public CustomBuilderBook.Builder id(final Isbn id) { this.id = id; return this; } public CustomBuilderBook.Builder id(final String id) { this.id = new Isbn(id); return this; } } }
なお、ISBNの自作型も定義しているのは、好みです。
DBから取得した場合は、ISBN型は既に作られていると思いますし…。
デフォルト値があると嬉しい
「@Builder.Default」を設定して、値を設定しているとBuilderでの生成時に値を設定できます。
@Value @Builder(toBuilder = true) public class Book { @Builder.Default private final String author = "kirimaru"; // デフォルト値 }
でも、自作Builderを作っている時には効きません。
自作するしかないですが、大変でもないので頑張って作りましょう。
クラスを逆コンパイルして確認してみましたが、難しいことはしてませんし。
@Generated public Book build() { String author = this.author; if (!this.author$set) { author = Book.$default$author(); } return new Book(author); }
生成したドメインを再度Builderの形に戻したい
アノテーションの属性、toBuilderにtrueを設定するだけでオッケーでした。
@Value @Builder(toBuilder = true) public class Book { private final int money; }
これで、以下のように使えます。
Book oldBook = new Book(100); Book newBook = oldBook.toBuilder() // toBuilder .money(500) .build(); // 以下と同じ。 Book oldBook = new Book(100); oldBook.setMoney(500);
インスタンスが異なるので、データをコピーしたいだけでもこの方法使えますね。
前に調べたこの方法じゃなくてもcloneする方法があるなんて…。
速度や効率に関しては特に比較してないので、どっちがいいかは比較したことないです。
GitHubソース
単純なBuilder
github.com
自作Builder
github.com
テストコード
github.com
類似記事(lombok)
終わりに
これを知ったことで、私はかなり便利になりました。
少なくとも、インスタンスからBuilderに戻す方法は簡単に実装できる割には、非常にわかりやすいです。
lombokはjavaエンジニアであれば数年後も使っている気がするので、ぜひ覚えて使ってみてください。
もしこの記事が役に立ったのであれば、はてぶ、Twitterでの記事の拡散、twitterのフォローもよろしくお願いします。
私の励みになります。