JavaScript Builder パターンによる柔軟なオブジェクト生成
ユーザーオブジェクトを作成する関数があるとします。最初は3つのパラメータで始まり、やがて5つ、そして7つに増えていきます。そのうち半分はオプションです。呼び出し側は正確な順序を覚えておく必要があり、1つでも位置を間違えると、エラーも出ずに壊れたオブジェクトが生成されてしまいます。これが JavaScript Builder パターン が解決する問題です。
重要なポイント
- Builder パターンは、エラーが発生しやすい位置引数リストの代わりに、連鎖的なセッターメソッドと最終的な
build()呼び出しを使用して、段階的にオブジェクトを構築します。 - 各セッターが
thisを返す流暢な API により、呼び出し箇所が自己文書化され、順序に依存しなくなります。 - 多数のオプションパラメータ、必須フィールドの強制、またはバリデーションルールを持つオブジェクトに対してこのパターンを使用してください。よりシンプルなケースでは、オブジェクトリテラルまたはファクトリ関数を使用します。
build()メソッドは、必須フィールドを検証し、ビルダー自体とは別のクリーンで凍結されたオブジェクトを返す唯一の場所です。
JavaScript Builder パターンとは?
Builder パターンは、オブジェクトを一度にではなく段階的に構築する生成デザインパターンです。すべての値を単一のコンストラクタ呼び出しに渡すのではなく、セッターメソッドを連鎖させ、検証を行って完成したオブジェクトを返す build() ステップで生成を完了します。
これは万能なソリューションではありません。2つまたは3つの明確に定義されたフィールドを持つシンプルなオブジェクトの場合、プレーンなオブジェクトリテラルまたはファクトリ関数の方がクリーンです。Builder パターンが価値を発揮するのは、オブジェクト生成に以下が含まれる場合です:
- 順序が重要でない 多数のオプションパラメータ
- オブジェクトが使用される前に実行する必要がある バリデーションルール
- 生成時に強制する必要がある 必須フィールド
- 中間状態を公開すべきでない 多段階の構築
問題点: コンストラクタの肥大化
次のような一般的なパターンを考えてみましょう:
// ❌ 読みにくく、引数の順序を間違えやすい
const request = new ApiRequest('GET', '/users', null, true, 5000, 'json')
6つの位置引数。ラベルなし。バリデーションなし。2つの値を入れ替えても、何も警告してくれません。
クリーンな JavaScript Builder パターンの例
以下は、JavaScript の流暢な API を使用したクラスベースの実装です。各セッターが this を返すことで、メソッドチェーンが可能になります:
class ApiRequestBuilder {
constructor() {
this.method = 'GET' // 妥当なデフォルト値
this.url = null
this.body = null
this.timeout = 3000 // デフォルトのタイムアウト
this.responseType = 'json'
}
setMethod(method) {
this.method = method
return this
}
setUrl(url) {
this.url = url
return this
}
setBody(body) {
this.body = body
return this
}
setTimeout(ms) {
this.timeout = ms
return this
}
build() {
if (!this.url) {
throw new Error('URL is required')
}
// ビルダー自体ではなく、プレーンで凍結されたオブジェクトを返す
return Object.freeze({
method: this.method,
url: this.url,
body: this.body,
timeout: this.timeout,
responseType: this.responseType,
})
}
}
// 使用例
const request = new ApiRequestBuilder()
.setUrl('/api/users')
.setMethod('POST')
.setBody({ name: 'Alice' })
.build()
各呼び出しは自己文書化されています。バリデーションはオブジェクトが使用される前に build() で実行されます。デフォルト値は自動的に適用されます。build() がビルダーではなく凍結されたプレーンオブジェクトを返すことに注目してください。これにより結果がクリーンに保たれ、誤った変更を防ぎます。
Discover how at OpenReplay.com.
Builder パターンとよりシンプルな代替手段の比較
| シナリオ | より良いアプローチ |
|---|---|
| 2〜3個の必須フィールド、バリデーションなし | オブジェクトリテラルまたはファクトリ関数 |
| オプションフィールド、ルールなし | createUser({ name, age }) による名前付きパラメータ |
| 必須フィールド + バリデーション + デフォルト値 | Builder パターン |
| 複雑な多段階構築 | Builder パターン |
createRequest({ url, method = 'GET' }) のような名前付きパラメータのファクトリ関数は、多くのケースをクリーンに処理できます。バリデーションロジックやシーケンスによってその関数が理解しにくくなる場合に、ビルダーを使用してください。
TypeScript に関する注意点
TypeScript はビルダーを大幅に安全にすることができます。条件型やステップビルダーインターフェースを使用して、必須のセッターが呼び出された後にのみ build() が呼び出し可能であることを強制できます。プロジェクトで TypeScript を使用している場合は、検討する価値があります。ただし、コアとなる JavaScript パターンは TypeScript なしでも十分に機能します。
まとめ
Builder パターンは、オブジェクト生成に強制すべきルールがある場合に使用し、デフォルトのオブジェクト生成戦略としては使用しないでください。流暢な API により呼び出し箇所が読みやすくなり、build() ステップによりバリデーションが明示的になり、デフォルト値によりノイズが減少します。それよりシンプルなものについては、ファクトリ関数またはプレーンなオブジェクトリテラルが適切なツールです。
よくある質問
オプションオブジェクトは名前付きパラメータをグループ化し、位置引数の問題を解決します。ビルダーは、必須フィールドの検証、制約の強制、使用前の結果の凍結を行うビルドステップを追加します。これらの保証が必要な場合は、ビルダーがより適しています。名前付きキーとデフォルト値だけが必要な場合は、オプションオブジェクトの方がシンプルです。
はい、ただし注意が必要です。build を呼び出した後も、ビルダーは以前の設定からの状態を保持しています。すべてのフィールドをリセットするか、各オブジェクトに対して新しいビルダーインスタンスを作成する必要があります。毎回新しいインスタンスを作成する方が、より安全で予測可能なアプローチです。
その可能性があります。オブジェクトに少数の既知のフィールドしかなく、バリデーションルールがない場合、プレーンなオブジェクトリテラルまたは名前付きパラメータを持つファクトリ関数の方がクリーンです。Builder パターンは、オプションフィールドの数が増える場合、デフォルト値が相互作用する場合、または生成に強制的な制約が必要な場合に効果を発揮します。
Object.freeze は、返されたオブジェクトのトップレベルのプロパティが生成後に変更されることを防ぎます。これにより、構築された結果が予測可能で読み取り専用に保たれ、オブジェクトが複数のコード層を通過する場合に特に有用です。ビルダー内部の設定時と外部での使用時の間に明確な境界を引きます。
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..