【プログラミングしてみよう59】Javaのメソッドチェーンとビルダーパターンを深める

Java

前回はFunctional Interfaceを自作して関数型プログラミングの応用を学びました。

今回は「メソッドチェーン」と「ビルダーパターン」を深掘りします。 以前デザインパターンの記事でビルダーパターンを紹介しましたが、今回はバリデーション・必須チェック・ネストしたビルダーなど、より実践的な実装を解説します。

メソッドチェーンとビルダーパターンの復習

「メソッドチェーン」とはメソッドの戻り値を使って続けてメソッドを呼び出す書き方です。

// メソッドチェーンを使わない場合
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");

// メソッドチェーンを使う場合
String result = new StringBuilder()
        .append("Hello")
        .append(", ")
        .append("World")
        .toString();

「ビルダーパターン」はメソッドチェーンを活用して、フィールドが多いクラスのインスタンスを読みやすく生成するデザインパターンです。以前の記事では staticネストクラスとして簡単に紹介しましたが、今回はバリデーション・必須チェックも含めた実践的な実装を学びます。

サンプルコード(実践的なビルダーパターン)

ファイル作成

「Java47」フォルダに HttpRequest.java、BuilderTest.java という名前でファイルを作成します

コード入力

HttpRequest.java に以下のコードを入力します。

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class HttpRequest {
    // すべてのフィールドはfinal(イミュータブル)
    private final String method;
    private final String url;
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutSeconds;

    // コンストラクタはprivate(Builderからのみ生成可能)
    private HttpRequest(Builder builder) {
        this.method         = builder.method;
        this.url            = builder.url;
        this.headers        = Collections.unmodifiableMap(new HashMap<>(builder.headers));
        this.body           = builder.body;
        this.timeoutSeconds = builder.timeoutSeconds;
    }

    public String method()         { return method; }
    public String url()            { return url; }
    public Map<String, String> headers() { return headers; }
    public String body()           { return body; }
    public int timeoutSeconds()    { return timeoutSeconds; }

    @Override
    public String toString() {
        return String.format("HttpRequest{method='%s', url='%s', headers=%s, body='%s', timeout=%ds}",
                method, url, headers, body != null ? body : "(なし)", timeoutSeconds);
    }

    // Builder クラス
    public static class Builder {
        // 必須フィールド
        private final String method;
        private final String url;
        // 任意フィールド(デフォルト値あり)
        private Map<String, String> headers = new HashMap<>();
        private String body = null;
        private int timeoutSeconds = 30;

        // 必須フィールドはコンストラクタで受け取る
        public Builder(String method, String url) {
            Objects.requireNonNull(method, "methodは必須です");
            Objects.requireNonNull(url, "urlは必須です");
            if (!url.startsWith("http://") && !url.startsWith("https://")) {
                throw new IllegalArgumentException("URLはhttp://またはhttps://で始まる必要があります");
            }
            this.method = method.toUpperCase();
            this.url    = url;
        }

        public Builder header(String key, String value) {
            Objects.requireNonNull(key, "ヘッダーのキーは必須です");
            Objects.requireNonNull(value, "ヘッダーの値は必須です");
            this.headers.put(key, value);
            return this; // thisを返すことでメソッドチェーンが可能
        }

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

        public Builder timeout(int seconds) {
            if (seconds <= 0) {
                throw new IllegalArgumentException("タイムアウトは正の値でなければなりません");
            }
            this.timeoutSeconds = seconds;
            return this;
        }

        public HttpRequest build() {
            // POSTやPUTにはbodyが必要
            if (("POST".equals(method) || "PUT".equals(method)) && body == null) {
                throw new IllegalStateException(method + "リクエストにはbodyが必要です");
            }
            return new HttpRequest(this);
        }
    }
}

BuilderTest.java に以下のコードを入力します。

public class BuilderTest {
    public static void main(String[] args) {
        // GETリクエスト
        HttpRequest getRequest = new HttpRequest.Builder("GET", "https://api.example.com/users")
                .header("Authorization", "Bearer token123")
                .header("Accept", "application/json")
                .timeout(10)
                .build();
        System.out.println("GETリクエスト:");
        System.out.println(getRequest);

        // POSTリクエスト
        HttpRequest postRequest = new HttpRequest.Builder("POST", "https://api.example.com/users")
                .header("Content-Type", "application/json")
                .body("{\"name\": \"ぱんまる\", \"age\": 25}")
                .timeout(30)
                .build();
        System.out.println("\nPOSTリクエスト:");
        System.out.println(postRequest);

        // バリデーションエラーのテスト
        System.out.println("\n--- バリデーションエラー ---");

        try {
            new HttpRequest.Builder("POST", "https://api.example.com/users")
                    .build(); // POSTにbodyなし
        } catch (IllegalStateException e) {
            System.out.println("エラー: " + e.getMessage());
        }

        try {
            new HttpRequest.Builder("GET", "ftp://example.com") // 不正なURL
                    .build();
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }

        try {
            new HttpRequest.Builder("GET", "https://api.example.com")
                    .timeout(-1) // 不正なタイムアウト
                    .build();
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

コンパイル

javac HttpRequest.java BuilderTest.java

実行

java BuilderTest

実行結果は次のようになります。

GETリクエスト:
HttpRequest{method='GET', url='https://api.example.com/users', headers={Authorization=Bearer token123, Accept=application/json}, body='(なし)', timeout=10s}

POSTリクエスト:
HttpRequest{method='POST', url='https://api.example.com/users', headers={Content-Type=application/json}, body='{"name": "ぱんまる", "age": 25}', timeout=30s}

--- バリデーションエラー ---
エラー: POSTリクエストにはbodyが必要です
エラー: URLはhttp://またはhttps://で始まる必要があります
エラー: タイムアウトは正の値でなければなりません

サンプルコード(ジェネリクスを使った汎用ビルダー)

ファイル作成

「Java47」フォルダに GenericBuilderTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class GenericBuilderTest {
    // 汎用ビルダー: Consumer<T>を使ってフィールドを設定する
    static class GenericBuilder<T> {
        private final T instance;

        private GenericBuilder(T instance) {
            this.instance = instance;
        }

        public static <T> GenericBuilder<T> of(java.util.function.Supplier<T> supplier) {
            return new GenericBuilder<>(supplier.get());
        }

        public GenericBuilder<T> with(Consumer<T> setter) {
            setter.accept(instance);
            return this;
        }

        public T build() {
            return instance;
        }
    }

    // 設定可能なフィールドを持つクラス(setterあり)
    static class Config {
        String host = "localhost";
        int port = 8080;
        int maxConnections = 10;
        boolean ssl = false;
        List<String> allowedOrigins = new ArrayList<>();

        @Override
        public String toString() {
            return String.format("Config{host='%s', port=%d, maxConnections=%d, ssl=%b, allowedOrigins=%s}",
                    host, port, maxConnections, ssl, allowedOrigins);
        }
    }

    public static void main(String[] args) {
        // 汎用ビルダーでConfigを組み立てる
        Config config = GenericBuilder.of(Config::new)
                .with(c -> c.host = "api.example.com")
                .with(c -> c.port = 443)
                .with(c -> c.ssl = true)
                .with(c -> c.maxConnections = 100)
                .with(c -> c.allowedOrigins.add("https://example.com"))
                .with(c -> c.allowedOrigins.add("https://app.example.com"))
                .build();

        System.out.println("設定: " + config);

        // デフォルト設定(何も変更しない)
        Config defaultConfig = GenericBuilder.of(Config::new).build();
        System.out.println("デフォルト: " + defaultConfig);
    }
}

コンパイル

javac GenericBuilderTest.java

実行

java GenericBuilderTest

実行結果は次のようになります。

設定: Config{host='api.example.com', port=443, maxConnections=100, ssl=true, allowedOrigins=[https://example.com, https://app.example.com]}
デフォルト: Config{host='localhost', port=8080, maxConnections=10, ssl=false, allowedOrigins=[]}

コードの解説

HttpRequest.Builder の設計ポイントは3つです。まず必須フィールド(method・url)はBuilderのコンストラクタで受け取り、作成時点でバリデーションします。任意フィールドはデフォルト値を設定し、メソッドチェーンで上書きできるようにします。最後に build() でビルド時のバリデーション(POSTにbodyが必要かどうかなど)を行います。

各メソッドが return this を返すことでメソッドチェーンが可能になります。HttpRequest 本体のコンストラクタを private にすることで、Builderを経由しないとインスタンスを作れなくなります。これによりバリデーションをスキップされることを防ぎます。

Objects.requireNonNull(method, "methodは必須です") はnullチェックを簡潔に書けるメソッドです。nullの場合 NullPointerException を投げ、第2引数のメッセージが表示されます。

汎用ビルダーの GenericBuilder<T> は Supplier<T> でインスタンスを生成し、Consumer<T> を受け取る with() メソッドでフィールドを設定します。クラス固有のBuilderを書かなくてもラムダ式でフィールドを設定できるため、小さなクラスや一時的なオブジェクト生成に便利です。

まとめ

ビルダーパターンは必須フィールドをコンストラクタで受け取り、任意フィールドをメソッドチェーンで設定し、build() でバリデーションしてインスタンスを生成するという流れが基本です。return this でメソッドチェーンを実現し、コンストラクタを private にすることでBuilderを経由したインスタンス生成を強制できます。フィールドが多いクラスや、生成時のバリデーションが複雑なクラスに特に向いています。

2026/08/07(金) ぱんまる

コメント

タイトルとURLをコピーしました