コーディング · デザインパターン
Builder パターンを例でわかりやすく解説
結論
Builder パターンは、複雑なオブジェクトの構築とその最終的な表現を分離し、同じ段階的な手順から異なる結果を作れるようにする生成パターンです。現代のコードでは、多くの場合、流れるようなAPI(fluent API)として現れます。ビルダーに対して .url()、.header()、.timeout() などのメソッドを呼び、最後に .build() が検証済みの完成したオブジェクトを返します。多数のオプション引数を持つ長いコンストラクタの代わりになります。
目的
複雑なオブジェクトの構築をその表現から分離し、同じ構築プロセスから異なる表現を作れるようにします。Builder パターンは、Gang of Four(GoF)の書籍にある5つの生成パターンの1つです。
解決する問題
URL、メソッド、任意の数のヘッダー、省略可能なボディ、タイムアウト、リトライ回数を持つHTTPリクエストのオブジェクトを想像してください。これらすべてを受け取るコンストラクタは、正しく呼び出すのが困難です。
class HttpRequest {
constructor(
readonly url: string,
readonly method: string,
readonly headers: Record<string, string>,
readonly body?: string,
readonly timeoutMs?: number,
readonly retries?: number,
) {}
}
// Which argument is which? Easy to swap 3 and 5000 by mistake.
const req = new HttpRequest("https://api.example.com", "POST", {}, undefined, 3, 5000);これはテレスコーピングコンストラクタと呼ばれます。「GETリクエストにボディは付けられない」といったルールを置く場所もなく、送信前に複数の場所でヘッダーを追加していくような、段階的な構築もできません。
構造と登場要素
| 要素 | 役割 | 例では |
|---|---|---|
| Builder | 製品の各部分を作成するステップを宣言します。 | RequestBuilder のメソッド |
| ConcreteBuilder | ステップを実装し、途中の状態を保持し、結果を返します。 | RequestBuilder |
| Director | 省略可能です。ステップを決まった順序で実行して、よく使う構成を生み出します。 | jsonPost() |
| Product | 構築される複雑なオブジェクトです。 | HttpRequest |
TypeScriptでの実装
各ステップは値を保存して this を返すので、呼び出しをチェーンできます。build() は組み合わせを検証し、不変の製品を返します。
interface HttpRequest {
readonly url: string;
readonly method: "GET" | "POST";
readonly headers: Readonly<Record<string, string>>;
readonly body?: string;
readonly timeoutMs: number;
}
class RequestBuilder {
private method: "GET" | "POST" = "GET";
private headers: Record<string, string> = {};
private body?: string;
private timeoutMs = 10_000;
constructor(private readonly url: string) {}
post(body: string): this { this.method = "POST"; this.body = body; return this; }
header(name: string, value: string): this { this.headers[name] = value; return this; }
timeout(ms: number): this { this.timeoutMs = ms; return this; }
build(): HttpRequest {
if (this.timeoutMs <= 0) throw new Error("timeout must be positive");
return Object.freeze({
url: this.url, method: this.method, headers: { ...this.headers },
body: this.body, timeoutMs: this.timeoutMs,
});
}
}
const request = new RequestBuilder("https://api.example.com/orders")
.header("Accept", "application/json")
.timeout(5_000)
.build();
console.log(request.method, request.timeoutMs, request.headers);
// GET 5000 { Accept: 'application/json' }Directorは、よく使うレシピを知っているコードにすぎません。TypeScriptでは、ビルダーを操作する単なる関数であることがよくあります。
interface Steps {
header(name: string, value: string): this;
post(body: string): this;
}
// Director: a reusable recipe that drives any builder with these steps.
function jsonPost<B extends Steps>(builder: B, data: unknown): B {
return builder
.header("Content-Type", "application/json")
.post(JSON.stringify(data));
}
class LogBuilder implements Steps {
log: string[] = [];
header(name: string, value: string): this { this.log.push(`${name}=${value}`); return this; }
post(body: string): this { this.log.push(`POST ${body}`); return this; }
}
console.log(jsonPost(new LogBuilder(), { id: 7 }).log);
// [ 'Content-Type=application/json', 'POST {"id":7}' ]ここにGoFの元の考え方が表れています。同じDirectorが操作する同じ構築ステップで、異なるビルダーに入力し、異なる表現(ここではリクエストの代わりにログ)を生み出せます。
使うべきとき、使わないとき
次の場合はBuilder パターンを使います。
- オブジェクトに多くの省略可能な部分があるか、部分を1つずつ追加する必要がある場合。
- 構築に複数のフィールドにまたがるルールがあり、
build()で1度だけ検査すべき場合。 - 完成したオブジェクトは不変にしつつ、段階的に組み立てたい場合。
- HTML、Markdown、PDFとして書き出す文書のように、同じ構築プロセスから異なる表現を作る必要がある場合。
次の場合は避けます。
- オブジェクトのフィールドが少ない場合。コンストラクタのほうが短く、同じくらい明快です。
- 言語がすでに名前付き引数や省略可能な引数を備えている場合。TypeScriptでは、単一のオプションオブジェクトで、ビルダーが完全に不要になることがよくあります。
interface RequestOptions {
url: string;
method?: "GET" | "POST";
headers?: Record<string, string>;
timeoutMs?: number;
}
function createRequest({ url, method = "GET", headers = {}, timeoutMs = 10_000 }: RequestOptions) {
return { url, method, headers, timeoutMs };
}
console.log(createRequest({ url: "/orders", timeoutMs: 5_000 }).method); // GET実際の利用例
- Java:
java.net.http.HttpRequest.newBuilder()はHttpRequest.Builderを返します。uri()、header()、timeout()をチェーンして、build()を呼びます。 - Java:
Locale.Builderは、言語、地域、スクリプトの各部分からロケールを組み立てて検証します。 - JavaとC#:
StringBuilderは、多くの断片を追加して文字列を組み立ててから、最終的な不変の文字列を生成します。 - 多くのSQLクエリビルダーやテストデータビルダーが、同じ流れるようなスタイルを使っています。
関連するパターン
- Abstract Factory パターンは製品のファミリー全体を作り、それぞれをすぐに返します。Builder パターンは1つの複雑な製品を段階的に構築し、最後に返します。
- Composite パターンの木構造は、ビルダーで作られることがよくあります。ノードを1つずつ木に組み上げる作業は、自然な一連のステップだからです。
- fluent interfaceは、
thisを返す呼び出しをつなげるAPIのスタイルです。Builder パターンはそれをよく使いますが、両者は同じものではありません。 - 新しいオブジェクトの大半が、既製のテンプレートのわずかな変形である場合は、代わりにPrototype パターンも選択肢になります。
よくある質問
ビルダーの各メソッドがthisを返すのはなぜですか?
ビルダーを返すことで、.header(...).timeout(...).build() のように、1つの式で呼び出しをチェーンできます。TypeScriptで this を返すと、サブクラスでも正しい型が保たれます。
Directorクラスは必要ですか?
いいえ。GoFの説明でも、Directorは省略可能です。実際には、よく使う一連のステップを適用する関数があれば通常は十分です。
TypeScriptでもBuilder パターンは役に立ちますか?
場合によります。既定値付きのオプションオブジェクトで、単純なケースの大半はカバーできます。オブジェクトを段階的に組み立てる場合、フィールドをまたぐ検証が必要な場合、不変の結果が欲しい場合には、Builder パターンが今でも役立ちます。
Builder パターンとFactory Method パターンの違いは何ですか?
Factory Method パターンは、どのクラスをインスタンス化するかを決め、1回の呼び出しでオブジェクトを返します。Builder パターンは、1つのオブジェクトをどう組み立てるかを、複数回の呼び出しにわたってクライアントが制御できるようにします。