Java Builderパターン

Java Builderパターン

카테고리
Java
태그
Java
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() ); */
上記のコードはupdatedByupdatedAtの引数がないコンストラクタです。
でもcreatedBycreatedAtの引数がないコンストラクタも作成する場合は?
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() ); */
上記のコードはエラーになります。
updatedByupdatedAtの引数がないコンストラクタと上記のコンストラクタは引数の数とタイプが同様でオーパーロード不可能です。フィールドがずっと増えていったらかなり複雑な問題になりそうですね。
このようなフィールドの多いインスタンスを生成する時に発生する問題を解決するため、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(); */
  1. builderメソッドを呼び出してCustomBoardBuilderクラスを生成
  1. CustomBoardBuilderクラス内のフィールドをセットするメソッドを呼び出して値をセットし、this(ここではCustomBoardBuilderクラス)を返却
  1. buildメソッドでCustomBoardクラスのコンストラクタを呼び出し
  1. 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(); */
コードが難しくなりましたが、少しずつ読んでみましょう。
例のコードでジェネリックTUpdatableBoardBuilderです。
  1. <T extends BaseBoardBuilder<T>>ジェネリックTBaseBoardBuilderを継承したクラス
  1. selfメソッドはTを返却
    1. コメント2-1ではthis(ここではUpdatableBoardBuilderクラス)を返却
  1. スーパークラスのフィールドに値をセットした後、BaseBoardBuilderのサブクラスを返却するselfメソッドを呼び出し
  1. buildメソッドはBaseBoardを返却
    1. 抽象メソッドなので戻り値のBaseBoardをサブクラスではメソッドをオーバーライドしてサブクラスを返却(コメント4-1)
  1. BaseBoardのコンストラクタにBaseBoardBuilderのサブクラスUpdatableBoardBuilderを引数として渡してインスタンスを生成
 
上記のスーパークラスはサブクラス(ジェネリックT)によって返却タイプが変わります。 このようにスーパークラスのメソッドがサブクラスのタイプを返却することを共変戻り値型(convariant return type)と言います。
 

まとめ

Lombokのアノテーションを使ってコードを作成することもいいですが、自分で機能を作ることができたらもっと適切なソリューションができると思います。
ご質問についてはなんでもいいので下記のメールでお問い合わせください。