Builder パターン — 引数 4 つを超えたら迷わず導入する
コンストラクタ引数が多いクラスの可読性を救う Builder。手書き・Lombok・record の併用パターンと、Fluent Builder でよくあるハマり所。

どういう問題を解くパターンか
コンストラクタ引数が多すぎて、呼び出し側で順番を間違える 問題を解きます。
// どの引数が何か分からない
new User("太郎", "山田", 32, "tokyo", "090-1234-5678", true, true, false);
これを Builder で書き直すと:
User user = User.builder()
.firstName("太郎")
.lastName("山田")
.age(32)
.city("tokyo")
.phone("090-1234-5678")
.notificationEnabled(true)
.marketingOptIn(true)
.suspended(false)
.build();
引数の意味がコードに現れる のが Builder の本質的な価値です。 派生して、オプショナルな項目を省略できる・不変オブジェクトを安全に作れる、 といった利点も得られます。
いつ使う / 使わない
| 使うべき | 使わないほうがよい |
|---|---|
| コンストラクタ引数が 4 つ以上 | 引数 1〜3 個 (素のコンストラクタで十分) |
| オプショナル項目が多い | 全項目必須で順序も自明 |
| 不変オブジェクトを作りたい | 内部で頻繁に setter 呼ばれる前提 |
入力の バリデーションを build() で一括 したい | 1 つずつ検証したい |
Joshua Bloch の 「Effective Java」 Item 2 の基準としては 「オプショナル引数が 2 つ以上あれば検討する価値あり」。
実装 (手書き)
典型的な Fluent Builder の形。
public final class User {
private final String firstName;
private final String lastName;
private final int age;
private final String city;
private final String phone;
private final boolean notificationEnabled;
private User(Builder b) {
this.firstName = b.firstName;
this.lastName = b.lastName;
this.age = b.age;
this.city = b.city;
this.phone = b.phone;
this.notificationEnabled = b.notificationEnabled;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String firstName;
private String lastName;
private int age;
private String city;
private String phone;
private boolean notificationEnabled = false; // デフォルト値
public Builder firstName(String v) { this.firstName = v; return this; }
public Builder lastName(String v) { this.lastName = v; return this; }
public Builder age(int v) { this.age = v; return this; }
public Builder city(String v) { this.city = v; return this; }
public Builder phone(String v) { this.phone = v; return this; }
public Builder notificationEnabled(boolean v) {
this.notificationEnabled = v; return this;
}
public User build() {
// 必須項目の検証はここで一括
Objects.requireNonNull(firstName, "firstName");
Objects.requireNonNull(lastName, "lastName");
if (age < 0) throw new IllegalArgumentException("age must be >= 0");
return new User(this);
}
}
}
ポイント:
Userのコンストラクタはprivate。生成経路を Builder に一本化- デフォルト値は Builder のフィールドで宣言
- バリデーションは
build()に集約 (1 ヶ所に書く) - 全フィールド
finalで不変化
Director パターン (GoF 本来の形)
GoF 本来の Builder は 「何を作るか」 の順序を Director に外出しします。
API ライブラリより 複数ステップのオブジェクト構築 (DOM/SQL ビルダー等) で見られる形。
public interface SqlBuilder {
void select(String... columns);
void from(String table);
void where(String condition);
String build();
}
public final class SqlDirector {
public static String userByCity(SqlBuilder b, String city) {
b.select("id", "name");
b.from("users");
b.where("city = '" + city + "'");
return b.build();
}
}
ただし業務コードで Director を切り出すシーンは稀。 多くの場合は Fluent Builder のみ で十分です。
Lombok を使う場合
全フィールドが単純な Fluent Builder なら、Lombok の @Builder でボイラープレートがほぼ消えます。
@Value
@Builder
public class User {
String firstName;
String lastName;
int age;
String city;
@Builder.Default boolean notificationEnabled = false;
}
Lombok を避けたほうがいいケース:
build()で複雑なバリデーションや副作用が必要 → 手書き or@Builder(toBuilder = true)+ カスタマイズ- フィールド名を公開 API 名と変えたい → 手書き
- 新人が多いチームでアノテーションマジックを嫌う → 手書き
record と組み合わせる
Java 16+ の record は Builder と排他ではなく、組み合わせる のが実務的です。
public record User(
String firstName,
String lastName,
int age,
String city,
boolean notificationEnabled
) {
public static Builder builder() { return new Builder(); }
public static final class Builder {
private String firstName;
private String lastName;
private int age;
private String city;
private boolean notificationEnabled = false;
public Builder firstName(String v) { this.firstName = v; return this; }
// ... 以下同様
public User build() {
return new User(firstName, lastName, age, city, notificationEnabled);
}
}
}
record が equals/hashCode/toString を自動生成してくれるぶん、
Builder だけ書けばよくなります。
落とし穴
1. ビルダーのフィールドを引き継ぎしたい (toBuilder)
既存オブジェクトから一部だけ変えて新しいオブジェクトを作りたいケース:
User modified = original.toBuilder().city("osaka").build();
Lombok は @Builder(toBuilder = true)、手書きなら Builder(User src) を実装。
不変オブジェクトの更新パターン として非常に便利なので、よく使うなら最初から入れておきましょう。
2. build() を呼び忘れる
Fluent メソッドチェーンの最後に .build() を忘れると、Builder 型のまま使ってしまい
ClassCastException やコンパイルエラー。
Builder クラスを final にしておき、呼び出し側コードレビューで気付けるようにします。
3. Builder を使い回して複数オブジェクトを作る
一つの Builder インスタンスで build() を複数回呼ぶと、状態が共有されて事故ります。
build() 内でフィールドをクリアするか、Builder を 「1 回使い切り」 とドキュメント化を。
4. 可変 Collection をそのまま入れる
public Builder tags(List<String> tags) {
this.tags = tags; // 外部から変更されうる
return this;
}
→ this.tags = List.copyOf(tags); にして防衛。
Builder は 「書くコストが高いが、読むコストが劇的に下がる」 パターン。 公開 API ・ドメインの中心にあるオブジェクト・テストで多用するオブジェクト には積極的に導入する価値があります。
関連記事
- アルゴリズム·
Adapter パターン — 外部ライブラリをドメインに染めない防波堤
既存の API を自分たちのドメインに合わせて包むアダプタ。クラスアダプタ vs オブジェクトアダプタ、SLF4J や Spring の HandlerAdapter などの実例まで。
- アルゴリズム·
Iterator パターン — Java では `Iterable` 実装 + 拡張 for で終わり
GoF の Iterator パターンを Java で書くときの最小実装。拡張 for 文、Spliterator、Stream との関係まで整理する。
- アルゴリズム·
TemplateMethod パターン — Spring JdbcTemplate に見る典型実装
処理の骨格は親、差分は子に書き分ける Template Method。Spring の `*Template` 系の設計思想と、Strategy / Decorator との使い分け。