Javaでコードを作成する際、インスタンスの生成制御の時にLombokの
@Builder
をよく使います。Builderを使ってない時の問題とBuilderを使う時の長所、Builderパターンの作成方をご紹介します。
JavaBeans
JavaBeansBoard
クラスpublic class JavaBeansBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; public void setId(Integer id) { this.id = id; } public void setTitle(String title) { this.title = title;} public void setContent(String content) { this.content = content; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } } /* JavaBeansBoardクラスの使い JavaBeansBoard board = new JavaBeansBoard(); -- インスタンスの生成 board.setId(1); board.setTitle("some title"); board.setContent("some content"); board.setCreatedAt(LocalDateTime.now()); -- 完全なインスタンス */
JavaBeansパターンで作成したコードはインスタンスが生成されるタイミングと完全なインスタンスになるタイミングが異なります。
なら、タイミングを合わせるためにコンストラクタでインスタンスを生成してみまよう。
ConstructorBoard
クラスpublic class ConstructorBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; public ConstructorBoard( Integer id, String title, String content, LocalDateTime createdAt) { this.id = id; this.title = title; this.content = content; this.createdAt = createdAt; } } /* ConstructorBoardの使い ConstructorBoard board = new ConstructorBoard( 1, "some title", "some content", LocalDateTime.now() ); */
コンストラクタを利用してインスタンスの生成と同時にデータのセットするようになりました。でも上記のコードはフィールドが追加された場合、短所が明確に現れます。
ManyFieldsBoard
クラスpublic class ManyFieldsBoard { private Integer id; private String title; private String content; private List<String> tags; private Integer createdBy; private LocalDateTime createdAt; private Integer updatedBy; private LocalDateTime updatedAt; public ManyFieldsBoard( Integer id, String title, String content, List<String> tags, Integer createdBy, LocalDateTime createdAt, Integer updatedBy, LocalDateTime updatedAt) { // .... } } /* ManyFieldsBoardの使い ManyFieldsBoard board = new ManyFieldsBoard( 1, "some title", "some content", List.of("Java Builder"), 1, LocalDateTime.now(), 1, LocalDateTIme.now() ); */
フィールドが増えた場合、コンストラクタの引数も一緒に増えることでコードがかなり読みにくくなりました。
コンストラクタの引数を減らすために他のコンストラクタを作成してみます。
public ManyFieldsBoard( Integer id, String title, String content, List<String> tags, Integer createdBy, LocalDateTime createdAt) { // .... } /* ManyFieldsBoardの使い ManyFieldsBoard board = new ManyFieldsBoard( 1, "some title", "some content", List.of("Java Builder"), 1, LocalDateTime.now() ); */
上記のコードは
updatedBy
とupdatedAt
の引数がないコンストラクタです。でも
createdBy
とcreatedAt
の引数がないコンストラクタも作成する場合は?public ManyFieldsBoard( Integer id, String title, String content, List<String> tags, Integer updatedBy, LocalDateTime updatedAt) { // .... } /* ManyFieldsBoardの使い ManyFieldsBoard board = new ManyFieldsBoard( 1, "some title", "some content", List.of("Java Builder"), 1, LocalDateTime.now() ); */
上記のコードはエラーになります。
updatedBy
とupdatedAt
の引数がないコンストラクタと上記のコンストラクタは引数の数とタイプが同様でオーパーロード不可能です。フィールドがずっと増えていったらかなり複雑な問題になりそうですね。このようなフィールドの多いインスタンスを生成する時に発生する問題を解決するため、Builderパターンをよく使います。
LombokのBuilder
LombokBoard
クラス@Builder public class LombokBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; } /* LombokBoardクラスの使い LombokBoard borad = LombokBoard.builder() .id(1) .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .build(); -- インスタンスの生成の同時に完全なインスタンス */
Builderパターンで作成したコードは
.build()
メソッドを呼び出すタイミングでインスタンスの生成の同時に値のセットも行いますのでもっと安全なコードになることに加えてクラスにフィールドを追加してもどんなフィールドにどんな値がセットされるか分かりやすくなりました。Builderパターンを作成
CustomBoard
クラスpublic class CustomBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; public static class CustomBoardBuilder { private Integer id; private String title; private String content; private LocalDateTime createdAt; public CustomBoardBuilder id(Integer id) { // 2 this.id = id; return this; } public CustomBoardBuilder title(String title) { // 2 this.title = title; return this; } public CustomBoardBuilder content(String content) { // 2 this.content = content; return this; } public CustomBoardBuilder createdAt(LocalDateTime createdAt) { // 2 this.createdAt = createdAt; return this; } public CustomBoard build() { // 3 return new CustomBoard(this); } } public static CustomBoardBuilder builder() { // 1 return new CustomBoardBuilder(); } private CustomBoard(CustomBoardBuilder builder) { // 4 this.id = builder.id; this.title = builder.title; this.content = builder.content; this.createdAt = builder.createdAt; } } /* CustomBoardクラスの使い CustomBoard borad = CustomBoard.builder() .id(1) .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .build(); */
builder
メソッドを呼び出してCustomBoardBuilder
クラスを生成
CustomBoardBuilder
クラス内のフィールドをセットするメソッドを呼び出して値をセットし、this
(ここではCustomBoardBuilder
クラス)を返却
build
メソッドでCustomBoard
クラスのコンストラクタを呼び出し
CustomBoard
クラスのコンストラクタでCustomBoardBuilder
クラス内のフィールドに値をCustomBoard
クラスのフィールドにセット
Lombokの
@Builder
を使った時と使い方は同じです。活用1:必須フィールドのBuilder
インスタンスの生成する時エラーをチェックする方法
RequiredArgsBoard
クラスpublic class RequiredArgsBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; public static class RequiredArgsBoardBuilder { private Integer id; private String title; private String content; private LocalDateTime createdAt; public RequiredArgsBoardBuilder id(Integer id) { this.id = id; return this; } public RequiredArgsBoardBuilder title(String title) { this.title = title; return this; } public RequiredArgsBoardBuilder content(String content) { this.content = content; return this; } public RequiredArgsBoardBuilder createdAt(LocalDateTime createdAt) { this.createdAt = createdAt; return this; } public RequiredArgsBoard build() { if (this.id == null || this.title == null) { throw new NullPointerException(); } return new RequiredArgsBoard(this); } } public static RequiredArgsBoardBuilder builder() { return new RequiredArgsBoardBuilder(); } private RequiredArgsBoard(RequiredArgsBoardBuilder builder) { this.id = builder.id; this.title = builder.title; this.content = builder.content; this.createdAt = builder.createdAt; } } /* RequiredArgsBoardクラスの使い CustomBoard borad = CustomBoard.builder() .id(1) .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .build(); CustomBoard borad = CustomBoard.builder() .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .build(); NullPointerException */
使い方は同じですが、必須フォールドの
id
またはtitle
の値をセットしない時はエラーになります。builder
メソッドを呼び出す時、引数をセットする方法
RequiredArgsBoard
クラスpublic class RequiredArgsBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; public static class RequiredArgsBoardBuilder { // 必須のフィールド private final Integer id; private final String title; private String content; private LocalDateTime createdAt; public RequiredArgsBoardBuilder(Integer id, String title) { this.id = id; this.title = title; } public RequiredArgsBoardBuilder content(String content) { this.content = content; return this; } public RequiredArgsBoardBuilder createdAt(LocalDateTime createdAt) { this.createdAt = createdAt; return this; } public RequiredArgsBoard build() { return new RequiredArgsBoard(this); } } public static RequiredArgsBoardBuilder builder(Integer id, String title) { if (this.id == null || this.title == null) { throw new NullPointerException(); } return new RequiredArgsBoardBuilder(id, title); } private RequiredArgsBoard(RequiredArgsBoardBuilder builder) { this.id = builder.id; this.title = builder.title; this.content = builder.content; this.createdAt = builder.createdAt; } } /* RequiredArgsBoardクラスの使い RequiredArgsBoard board = RequiredArgsBoard.builder(1, "some title") .content("some content") .createdAt(LocalDateTime.now()) .build(); */
builder
メソッドを呼び出す時に、引数を渡すようになりました。活用2:継承時のBuilder
Lombokの
@Builder
アノテーションではスーパークラスのフィールドをセットすることが不可能です。スーパークラスのフィールドにも値をセットしたい場合は
@SuperBuilder
を使います。Lombokで作成
スーバークラスの
BaseBoard
クラス@SuperBuilder public abstract class BaseBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; }
サブクラスの
UpdatableBoard
クラス@SuperBuilder public class UpdatableBoard extends BaseBoard { private LocalDateTime updatedAt; } /* UpdatableBoardクラスの使い UpdatableBoard board = UpdatableBoard.builder() .id(1) .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .updatedAt(LocalDateTime.now()) .build(); */
スーパークラスとサブクラスに
@SuperBuilder
をセットすることでスーバークラスのフィールドも値をセットすることができます。継承時のBuilderパターンを作成
スーバークラスの
BaseBoard
クラスpublic abstract class BaseBoard { private Integer id; private String title; private String content; private LocalDateTime createdAt; abstract static class BaseBoardBuilder<T extends BaseBoardBuilder<T>> { // 1 private Integer id; private String title; private String content; private LocalDateTime createdAt; public T id(Integer id) { // 3 this.id = id; return self(); } public T title(String title) { // 3 this.title = title; return self(); } public T content(String content) { // 3 this.content = content; return self(); } public T createdAt(LocalDateTime createdAt) { // 3 this.createdAt = createdAt; return self(); } // 4 abstract BaseBoard build(); // 2 protected abstract T self(); } protected BaseBoard(BaseBoardBuilder<?> builder) { this.id = builder.id; this.title = builder.title; this.content = builder.content; this.createdAt = builder.createdAt; } }
サブクラスの
UpdatableBoard
クラスpublic class UpdatableBoard extends BaseBoard { private LocalDateTime updatedAt; public UpdatableBoard(UpdatableBoardBuilder builder) { // 5 super(builder); this.updatedAt = builder.updatedAt; } public static class UpdatableBoardBuilder extends BaseBoardBuilder<UpdatableBoardBuilder> { private LocalDateTime updatedAt; public UpdatableBoardBuilder updatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; return this; } @Override public UpdatableBoard build() { // 4-1 return new UpdatableBoard(this); } @Override protected UpdatableBoardBuilder self() { // 2-1 return this; } } public static UpdatableBoardBuilder builder() { return new UpdatableBoardBuilder(); } } /* UpdatableBoardクラスの使い UpdatableBoard board = UpdatableBoard.builder() .id(1) .title("some title") .content("some content") .createdAt(LocalDateTime.now()) .updatedAt(LocalDateTime.now()) .build(); */
コードが難しくなりましたが、少しずつ読んでみましょう。
例のコードでジェネリック
T
はUpdatableBoardBuilder
です。<T extends BaseBoardBuilder<T>>
ジェネリックT
はBaseBoardBuilder
を継承したクラス
self
メソッドはT
を返却- コメント2-1では
this
(ここではUpdatableBoardBuilder
クラス)を返却
- スーパークラスのフィールドに値をセットした後、
BaseBoardBuilder
のサブクラスを返却するself
メソッドを呼び出し
build
メソッドはBaseBoard
を返却- 抽象メソッドなので戻り値の
BaseBoard
をサブクラスではメソッドをオーバーライドしてサブクラスを返却(コメント4-1)
BaseBoard
のコンストラクタにBaseBoardBuilder
のサブクラスUpdatableBoardBuilder
を引数として渡してインスタンスを生成
上記のスーパークラスはサブクラス(ジェネリックT
)によって返却タイプが変わります。 このようにスーパークラスのメソッドがサブクラスのタイプを返却することを共変戻り値型(convariant return type)と言います。
まとめ
Lombokのアノテーションを使ってコードを作成することもいいですが、自分で機能を作ることができたらもっと適切なソリューションができると思います。
ご質問についてはなんでもいいので下記のメールでお問い合わせください。