きり丸の日記

通信事業会社のエンジニアです。Java、vueを使うエンジニアです。

【Java】lombokのデフォルトのBuilderに不満があったので、解消方法を調べた(デフォルト値/Validation/再びBuilder)

前回の記事にて、まるで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する方法があるなんて…。

速度や効率に関しては特に比較してないので、どっちがいいかは比較したことないです。

nainaistar.hatenablog.com

GitHubソース


単純なBuilder
github.com

自作Builder
github.com

テストコード
github.com

類似記事(lombok


nainaistar.hatenablog.com

終わりに


これを知ったことで、私はかなり便利になりました。

少なくとも、インスタンスからBuilderに戻す方法は簡単に実装できる割には、非常にわかりやすいです。

lombokjavaエンジニアであれば数年後も使っている気がするので、ぜひ覚えて使ってみてください。


もしこの記事が役に立ったのであれば、はてぶ、Twitterでの記事の拡散、twitterのフォローもよろしくお願いします。

私の励みになります。