目次
そもそも何が問題なのか
例えば、あなたがスーツを注文するとします。サイズ、色、ボタンの種類、裏地、ポケットの形、ネーム刺繍など、決めることがたくさんあります。
もし店員が「全部一度に言ってください」と言ってきたらどうでしょうか。順番も自由、抜けも許されない、途中で変更も難しい。かなり混乱します。
これは、コンストラクタに大量の引数を渡してオブジェクトを作る状況と似ています。どの値がどの意味なのか分かりにくく、順番を間違えるとバグになります。
引数が多いオブジェクトの問題
例えば「ユーザー登録情報」を考えてみましょう。名前、メールアドレス、電話番号、住所、生年月日、ロール、通知設定など、多くの情報があります。
すべてを一度に渡してオブジェクトを作る設計では、コードの可読性が下がり、必須項目と任意項目の区別も分かりづらくなります。また、将来項目が増えたときに既存のコードにも影響が出やすくなります。
Builderという考え方
ここで、スーツの例に戻ります。通常のオーダースーツでは、次のように進みます。
まずサイズを決める。次に色を選ぶ。そのあとボタンや裏地を選ぶ。最後に全体を確認して注文を確定する。
一つずつ段階的に決めていき、最後に完成させる。この流れこそが Builder パターンの考え方です。
Builderは「オブジェクトを直接作らない」代わりに、「組み立てる人(Builder)」を用意します。そして必要な情報を順番に設定し、最後に完成させます。
実際のコード例
では、実務ではどう書くのでしょうか。
TypeScriptでユーザーオブジェクトを作るケースを考えてみます。必須なのは id と email。name や age は任意。isAdmin や isActive にはデフォルト値を持たせたいとします。
まずは通常の型定義です。
export type User = {
id: string
email: string
name?: string
age?: number
isAdmin: boolean
isActive: boolean
}
このままオブジェクトを作ると、すべてを一度に指定することになります。
const user: User = {
id: "u1",
email: "test@example.com",
name: "Taro",
age: 30,
isAdmin: true,
isActive: true,
}
フィールドが増えれば増えるほど、可読性は下がっていきます。
そこでBuilderを用意します。
export class UserBuilder {
private id: string
private email: string
private name?: string
private age?: number
private isAdmin: boolean = false
private isActive: boolean = true
constructor(id: string, email: string) {
this.id = id
this.email = email
}
setName(name: string) {
this.name = name
return this
}
setAge(age: number) {
this.age = age
return this
}
setAdmin(isAdmin: boolean) {
this.isAdmin = isAdmin
return this
}
deactivate() {
this.isActive = false
return this
}
build(): User {
return {
id: this.id,
email: this.email,
name: this.name,
age: this.age,
isAdmin: this.isAdmin,
isActive: this.isActive,
}
}
}
使い方はこうなります。
const user = new UserBuilder( "u1", "test@example.com" )
.setName("Taro")
.setAge(30)
.setAdmin(true)
.build()
何を設定しているのかが、自然に読める形になります。
Kotlinでも同様です。data class だけでも十分に書けますが、生成ロジックを隠したい場合はBuilderを使います。
class User private constructor(
val id: String,
val email: String,
val name: String?,
val age: Int?,
val isAdmin: Boolean,
val isActive: Boolean
) {
class Builder(
private val id: String,
private val email: String
) {
private var name: String? = null
private var age: Int? = null
private var isAdmin: Boolean = false
private var isActive: Boolean = true
fun name(name: String) = apply {
this.name = name
}
fun age(age: Int) = apply {
this.age = age
}
fun admin(isAdmin: Boolean) = apply {
this.isAdmin = isAdmin
}
fun deactivate() = apply {
this.isActive = false
}
fun build(): User {
return User( id, email, name, age, isAdmin, isActive )
}
}
}
呼び出し側は次のようになります。
val user = User.Builder( "u1", "test@example.com")
.name("Taro")
.age(30)
.admin(true)
.build()
必須項目は最初に強制され、任意項目は段階的に設定でき、最後に完成する。まさにスーツを仕立てる流れと同じです。
Builderを使うメリット
まず、可読性が高まります。何を設定しているのかが明確になります。
次に、任意項目の扱いが自然になります。設定しなければデフォルト値を使う、という設計が可能です。
さらに、オブジェクトを不変にしやすくなります。一度完成したオブジェクトは変更できないようにすることで、安全性も高まります。
どんなときに使うべきか
以下のような場合に Builder パターンは効果的です。
・引数が多いオブジェクトを生成する場合
・必須項目と任意項目が混在している場合
・同じオブジェクトでも生成パターンが複数ある場合
・不変オブジェクトを作りたい場合
逆に向いていないケース
単純なデータ構造や、フィールドが2〜3個しかない場合には、Builderは過剰設計になることもあります。
パターンは万能ではありません。「複雑さを下げるため」に使うのであって、「パターンを使うこと」が目的になってはいけません。
まとめ
・Builderパターンは、複雑なオブジェクトを段階的に安全に組み立てるための設計手法です。
・大量の引数による可読性低下やバグのリスクを減らせます。
・必須項目と任意項目を整理し、不変オブジェクトとも相性が良いのが特徴です。
・ただし、シンプルなケースでは無理に使う必要はありません。
設計に迷ったときは、「このオブジェクトは一度に全部決めるべきか、それとも段階的に組み立てるべきか」と考えてみると、Builderを使うべきかどうかが見えてきます。