java-recipes

ホーム デザインパターン › DP-02

DP-02: Builder パターン

コンストラクタの引数が増えすぎる「テレスコーピング問題」を解決するパターンです。java.net.http.HttpRequest.Builder など、 標準ライブラリでも広く使われています。

Builder パターンとは

オブジェクトのフィールドが多くなると、コンストラクタの引数も増えていきます。 引数が5つ・6つと増えると「何番目の引数が何か」がわかりにくくなり、 引数の順番を間違えてもコンパイルエラーにならないというバグが生まれます。 これを「テレスコーピングコンストラクタ問題」と呼びます。

Builder パターンの特徴

  • 名前付き設定: .method("POST") のように何を設定しているか一目でわかる
  • 必須フィールドの強制: Builder のコンストラクタで必須フィールドを受け取り、忘れをコンパイル時に防ぐ
  • デフォルト値の管理: オプションフィールドには Builder 側でデフォルト値を持てる
  • イミュータブルオブジェクト: build() 後のオブジェクトは変更不可(final フィールド)にできる

標準ライブラリでは java.net.http.HttpRequest.Builder(Java 11+)、StringBuilderProcessBuilder などが Builder パターンを採用しています。

サンプルコード

BuilderPatternSample.java
public class BuilderPatternSample {

    // ❌ アンチパターン: 引数が多いコンストラクタ(テレスコーピングコンストラクタ)
    static class BadHttpRequest {
        public BadHttpRequest(String url, String method, String body,
                              int timeout, boolean followRedirect, String userAgent) {
            // 何番目の引数が何かわかりにくい
        }
    }

    // ✅ Builder パターン: 名前付き・段階的に構築
    static class HttpRequest {
        private final String url;
        private final String method;
        private final String body;
        private final int timeoutMs;
        private final boolean followRedirect;
        private final String userAgent;

        private HttpRequest(Builder builder) {
            this.url = builder.url;
            this.method = builder.method;
            this.body = builder.body;
            this.timeoutMs = builder.timeoutMs;
            this.followRedirect = builder.followRedirect;
            this.userAgent = builder.userAgent;
        }

        @Override
        public String toString() {
            return "HttpRequest{url=" + url + ", method=" + method
                + ", timeout=" + timeoutMs + "ms, followRedirect=" + followRedirect + "}";
        }

        // Builder クラス
        static class Builder {
            private final String url;    // 必須フィールド
            private String method = "GET";
            private String body = "";
            private int timeoutMs = 30000;
            private boolean followRedirect = true;
            private String userAgent = "java-recipes/1.0";

            public Builder(String url) {  // 必須フィールドはコンストラクタで強制
                if (url == null || url.isEmpty()) {
                    throw new IllegalArgumentException("URL は必須です");
                }
                this.url = url;
            }

            public Builder method(String method) {
                this.method = method;
                return this; // メソッドチェーン
            }

            public Builder body(String body) {
                this.body = body;
                return this;
            }

            public Builder timeout(int ms) {
                if (ms <= 0) throw new IllegalArgumentException("タイムアウトは正の値を指定してください");
                this.timeoutMs = ms;
                return this;
            }

            public Builder followRedirect(boolean follow) {
                this.followRedirect = follow;
                return this;
            }

            public Builder userAgent(String ua) {
                this.userAgent = ua;
                return this;
            }

            public HttpRequest build() {
                return new HttpRequest(this);
            }
        }
    }

    // メールメッセージの Builder 例
    static class EmailMessage {
        private final String to;
        private final String subject;
        private final String body;
        private final String cc;
        private final boolean htmlEnabled;

        private EmailMessage(Builder b) {
            this.to = b.to;
            this.subject = b.subject;
            this.body = b.body;
            this.cc = b.cc;
            this.htmlEnabled = b.htmlEnabled;
        }

        @Override
        public String toString() {
            return "Email{to=" + to + ", subject=" + subject + ", cc=" + cc + "}";
        }

        static class Builder {
            private final String to;
            private final String subject;
            private String body = "";
            private String cc = "";
            private boolean htmlEnabled = false;

            public Builder(String to, String subject) {
                this.to = to;
                this.subject = subject;
            }

            public Builder body(String body) { this.body = body; return this; }
            public Builder cc(String cc) { this.cc = cc; return this; }
            public Builder html(boolean enabled) { this.htmlEnabled = enabled; return this; }

            public EmailMessage build() { return new EmailMessage(this); }
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Builder パターン: HttpRequest ===");
        HttpRequest req1 = new HttpRequest.Builder("https://api.example.com/users")
            .method("POST")
            .body("{\"name\":\"田中\"}")
            .timeout(5000)
            .followRedirect(false)
            .build();
        System.out.println(req1);

        // 最小構成(必須フィールドのみ)
        HttpRequest req2 = new HttpRequest.Builder("https://api.example.com/users")
            .build();
        System.out.println(req2);

        System.out.println("\n=== Builder パターン: EmailMessage ===");
        EmailMessage email = new EmailMessage.Builder("taro@example.com", "ご連絡")
            .body("本文です")
            .cc("hanako@example.com")
            .html(true)
            .build();
        System.out.println(email);

        System.out.println("\n=== 標準ライブラリの Builder 例 ===");
        StringBuilder sb = new StringBuilder()
            .append("Hello, ")
            .append("World")
            .append("!");
        System.out.println(sb.toString());
    }
}

Java 8 では Builder クラスを内部静的クラスとして実装します。必須フィールドはコンストラクタで受け取り、オプションフィールドはメソッドチェーンで設定します。

よくあるミス・注意点

⚠️ build() を呼び忘れる

メソッドチェーンを書いても最後に .build() を呼ばないと Builder オブジェクトが返り、目的のオブジェクトは生成されません。 IDE の型チェックでは気づきにくい場合もあるため注意しましょう。

⚠️ 必須フィールドをオプションにしてしまう

URL なしで HTTP リクエストを作るなど、意味をなさないオブジェクトが生成されてしまいます。 必須フィールドは Builder のコンストラクタで受け取るよう設計し、 null チェックも忘れずに入れましょう。

⚠️ Builder を再利用して複数オブジェクトを作るとフィールドが混ざる

同じ Builder インスタンスで .build() を複数回呼ぶと、 前の設定が残ったまま別のオブジェクトが作られることがあります。 原則として Builder は使い捨てにしましょう。

テストする観点

  • 必須フィールド(URL)が null のとき IllegalArgumentException がスローされること
  • 必須フィールドが空文字のとき IllegalArgumentException がスローされること(境界値)
  • オプションフィールドを指定しない場合にデフォルト値が設定されること(例: method="GET", timeout=30000)
  • タイムアウトに 0 や負の値を渡すと IllegalArgumentException がスローされること(境界値)
  • 同じ Builder を 2 回 build() しても同じ値を持つ独立したオブジェクトが返ること
  • メソッドチェーンの順序を変えても同じ結果になること

GitHub でソースコードを見る →