Javaのビルダーパターン実践 — コンストラクタ引数が増えた時の解決策
はじめに
こんなコードを書いたことはありませんか?
User user = new User("田中太郎", 25, "tokyo@example.com", "Tokyo", true, "admin");
引数が6つあって、どれが何かまったくわかりません。しかも引数の型が同じだと渡し間違えてもコンパイルエラーにならないという怖さがあります。
この問題を解決するのが「ビルダーパターン」です。
問題:コンストラクタ引数が増えた時の苦しみ
// 悪い例:引数が多すぎて意味不明
public class User {
private final String name;
private final int age;
private final String email;
private final String address;
private final boolean active;
private final String role;
public User(String name, int age, String email, String address, boolean active, String role) {
this.name = name;
this.age = age;
this.email = email;
this.address = address;
this.active = active;
this.role = role;
}
}
// 呼び出し側:何が何かわからない
User user = new User("田中太郎", 25, "tokyo@example.com", "Tokyo", true, "admin");
// 名前? 年齢? メール? 住所? 有効? ロール?
問題点:
- 引数の意味が見た目でわからない
-
String型が複数あると渡す順番を間違えやすい - コンパイルエラーにならないので実行時まで気づかない
ビルダーパターンとは
オブジェクトの生成を「段階的に組み立てる」パターンです。
// ビルダーパターンを使った呼び出し
User user = User.builder()
.name("田中太郎")
.age(25)
.email("tokyo@example.com")
.address("Tokyo")
.active(true)
.role("admin")
.build();
何を設定しているかが一目瞭然になります。
手動でビルダーを実装する
public class User {
private final String name;
private final int age;
private final String email;
private final String address;
private final boolean active;
private final String role;
// コンストラクタはprivate(Builderからのみ生成できる)
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
this.address = builder.address;
this.active = builder.active;
this.role = builder.role;
}
// ゲッター
public String name() { return name; }
public int age() { return age; }
public String email() { return email; }
public String address() { return address; }
public boolean active() { return active; }
public String role() { return role; }
// ビルダーのエントリーポイント
public static Builder builder() {
return new Builder();
}
// 静的内部クラスとしてBuilderを定義
public static class Builder {
private String name;
private int age;
private String email;
private String address;
private boolean active;
private String role;
public Builder name(String name) {
this.name = name;
return this; // メソッドチェーンのためにthisを返す
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder active(boolean active) {
this.active = active;
return this;
}
public Builder role(String role) {
this.role = role;
return this;
}
public User build() {
// バリデーションをここに集約できる
if (name == null || name.isBlank()) {
throw new IllegalStateException("nameは必須です");
}
if (email == null || email.isBlank()) {
throw new IllegalStateException("emailは必須です");
}
return new User(this);
}
}
}
Lombokの@Builderで楽をする
実務ではLombokを使うとボイラープレートコードを省略できます。
<!-- pom.xml -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class User {
private final String name;
private final int age;
private final String email;
private final String address;
private final boolean active;
private final String role;
}
これだけで手動実装と同等のビルダーが自動生成されます。
// 呼び出し方は手動実装と同じ
User user = User.builder()
.name("田中太郎")
.age(25)
.email("tokyo@example.com")
.address("Tokyo")
.active(true)
.role("admin")
.build();
デフォルト値を設定したい場合は @Builder.Default を使います:
@Getter
@Builder
public class User {
private final String name;
private final int age;
private final String email;
private final String address;
@Builder.Default
private final boolean active = true; // デフォルトtrue
@Builder.Default
private final String role = "user"; // デフォルト"user"
}
いつビルダーパターンを使うか
| 状況 | 判断 |
|---|---|
| コンストラクタ引数が4つ以上 | ビルダーを検討する |
| 同じ型の引数が複数ある | ビルダーを強く推奨 |
| 省略可能なフィールドがある | ビルダーが適している |
| 引数が2〜3つで全て必須 | コンストラクタでOK |
まとめ
ビルダーパターンを使うメリット:
- 何を設定しているか名前で明示される
- 引数の順番を気にしなくてよい
- 省略可能なフィールドを柔軟に扱える
-
build()にバリデーションを集約できる
手動実装はコード量が増えますが、Lombokの @Builder を使えば1アノテーションで済みます。コンストラクタ引数が増えてきたら、ビルダーパターンへの切り替えを検討してみてください。