Builderパターン(ビルダーパターン)は、複雑なオブジェクトを一度のコンストラクタ呼び出しで作らず、組み立てる手順と出来上がる表現を分けて段階的に作る設計です。ただ「Builderパターン」という名前は、GoFが1994年に定義したDirectorを伴う型と、Joshua Blochが『Effective Java』で広めた引数の多いコンストラクタを置き換える型の2つを指しており、解説記事ごとに図が違って見えるのはこのためです。この記事では2つの系統を先に区別し、Go言語で実行確認したコード、Go流のFunctional Optionsや設定構造体との使い分け、Builderを使わない方がいい場面まで順に説明します。
まとめ:Builderパターンの要点
- 定義:オブジェクトの構築手順と、出来上がる表現(データ構造や出力形式)を分ける生成パターン。GoF 23パターンの1つ。
- 2つの系統:GoF型はDirectorが同じ手順を呼び、差し替えたBuilderごとに別の表現を作る。Effective Java型は任意項目の多いオブジェクトをメソッドチェーンで組み立て、最後の
build()で完成品を返す。 - Goでの選び方:任意項目の多いコンストラクタにはFunctional Optionsが定番。ゼロ値で困らない設定なら構造体リテラルで足りる。SQLのように順番に積み上げて最後に出力するものだけメソッドチェーンのBuilderにする。
- Factoryとの違い:Factoryは「どのクラスを作るか」を隠し、Builderは「どう組み立てるか」を段階化する。
- 使わない方がいい場面:任意項目が無く引数も少ないオブジェクトや、キーワード引数・名前付き引数・オブジェクトリテラルがある言語(Python・Kotlin・TypeScript)で任意項目を渡したいだけの場合は、Builderクラスを書くと項目の二重管理が増える。
Builderパターンの定義:GoF型とEffective Java型の2系統
GoFのBuilder:構築手順と表現の分離
GoF(Gamma・Helm・Johnson・Vlissidesの4人)の『Design Patterns』(1994年)は、Builderの目的を「複雑なオブジェクトの構築をその表現から分離し、同じ構築過程で異なる表現を作れるようにする」と定めています。原著の題材はRTF文書の変換で、文書を読み進める側(Director)は同じでも、渡す変換器(Builder)を替えるとプレーンテキストにもTeXにも画面表示用の部品にもなる、という構成です。
登場する役割は4つです。
| 役割 | 責任 | 後述のGo例での対応 |
|---|---|---|
| Builder | 部品を作る操作を宣言する | Builder インタフェース |
| ConcreteBuilder | 操作を実装し、完成品を保持する | TextBuilder・HTMLBuilder |
| Director | 操作を呼ぶ順序だけを知る | Construct 関数 |
| Product | 組み上がった完成品 | 文字列(テキスト・HTML) |
要になるのはDirectorが具体的な表現を知らないことです。出力形式が1つ増えても、ConcreteBuilderを1つ足すだけでDirectorは変わりません。
Effective Java型:引数の多いコンストラクタの代替
現在「Builderパターン」で検索して出てくる例の多くは、こちらの系統です。Joshua Blochは『Effective Java』第3版(2018年)の項目2「Consider a builder when faced with many constructor parameters」で、2つの書き方の欠点を挙げています。1つは引数の組み合わせごとにコンストラクタを増やすテレスコーピング・コンストラクタで、引数が増えると同じ型の引数の順番を取り違えてもコンパイラが気づきません。もう1つはsetterで1項目ずつ設定するJavaBeansパターンで、設定の途中でオブジェクトが不完全な状態のまま使われうるうえ、不変(イミュータブル)にできません。
Builderはこの両方を避けます。必須項目はBuilderの生成時に受け取り、任意項目はメソッドチェーンで名前を付けて渡し、build() で検証してから不変の完成品を返します。Directorは登場しません。
Javaではこの型を手で書く代わりに、Lombokの @Builder アノテーションを付けてBuilderクラスをコンパイル時に自動生成させる方法もあります。静的ネストクラスでの書き方、必須項目の強制、recordとの組み合わせなどJava固有の実装はJavaで書くBuilderパターンの基本構造と実装方法で、@Builder の仕組みはLombokのアノテーション解説で扱っています。
GoのGoF型Builder実装:Directorと2つのConcreteBuilder
障害報告を、同じ手順からプレーンテキストとHTMLの2形式で出力する例です。Construct がDirectorで、見出しと項目を呼ぶ順序だけを知っています。HTML側は入力に < や & が含まれても壊れないよう、html.EscapeString を通してから連結しています。
package main
import (
"fmt"
"html"
"strings"
)
// Builder は「報告書」を組み立てる手順を宣言する
type Builder interface {
Title(s string)
Item(s string)
}
// TextBuilder はプレーンテキストを組み立てる
type TextBuilder struct{ b strings.Builder }
func (t *TextBuilder) Title(s string) { fmt.Fprintf(&t.b, "== %s ==\n", s) }
func (t *TextBuilder) Item(s string) { fmt.Fprintf(&t.b, "- %s\n", s) }
func (t *TextBuilder) Result() string { return t.b.String() }
// HTMLBuilder は同じ手順からHTMLを組み立てる
type HTMLBuilder struct {
items []string
title string
}
func (h *HTMLBuilder) Title(s string) { h.title = html.EscapeString(s) }
func (h *HTMLBuilder) Item(s string) { h.items = append(h.items, "<li>"+html.EscapeString(s)+"</li>") }
func (h *HTMLBuilder) Result() string {
return "<h2>" + h.title + "</h2><ul>" + strings.Join(h.items, "") + "</ul>"
}
// Construct は Director の役。手順の順序だけを知っている
func Construct(b Builder) {
b.Title("障害報告")
b.Item("発生 10:02")
b.Item("復旧 10:40")
}
func main() {
t := &TextBuilder{}
Construct(t)
fmt.Print(t.Result())
h := &HTMLBuilder{}
Construct(h)
fmt.Println(h.Result())
}
実行結果です(Go 1.26.5 で go run)。
== 障害報告 ==
- 発生 10:02
- 復旧 10:40
<h2>障害報告</h2><ul><li>発生 10:02</li><li>復旧 10:40</li></ul>
Markdown出力を足すなら MarkdownBuilder を1つ書くだけで、Construct には手を入れません。逆に、出力形式が1つしかなく今後も増えないなら、Builderインタフェースを挟む理由はありません。GoF型が効くのは「手順は固定、表現が複数」という条件がそろったときです。
なお、Go標準ライブラリの strings.Builder は名前こそBuilderですが、公式ドキュメントの説明は「Writeメソッドで文字列を効率よく組み立てるためのもの」で、メモリのコピーを減らすバッファです。GoFのBuilderパターンの実装ではありません。上の例では TextBuilder の内部で部品として使っています。
Goのメソッドチェーン型Builder:最初のエラーをBuild()で返す設計
Effective Java型をGoで書くと、例外が無いぶん「途中のメソッドで不正な値が来たらどうするか」を決める必要があります。各メソッドで error を返すとチェーンが切れるため、最初のエラーをBuilderに記録しておき、Build() で返すのが定石です。なお、Goには不変の構造体が無いため、この例は不変性の再現ではなくエラーの扱いに絞っています。Build() が Request を値で返すので、受け取った後にBuilderを操作しても完成品は変わりません。
package main
import (
"errors"
"fmt"
"time"
)
type Request struct {
URL string
Method string
Timeout time.Duration
Retries int
}
type RequestBuilder struct {
req Request
err error
}
func NewRequest(url string) *RequestBuilder {
return &RequestBuilder{req: Request{URL: url, Method: "GET", Timeout: 10 * time.Second}}
}
func (b *RequestBuilder) Method(m string) *RequestBuilder {
b.req.Method = m
return b
}
func (b *RequestBuilder) Timeout(d time.Duration) *RequestBuilder {
if d <= 0 && b.err == nil {
b.err = fmt.Errorf("timeout must be positive: %v", d)
}
b.req.Timeout = d
return b
}
func (b *RequestBuilder) Retries(n int) *RequestBuilder {
if n < 0 && b.err == nil {
b.err = errors.New("retries must be >= 0")
}
b.req.Retries = n
return b
}
// Build で最初のエラーをまとめて返す
func (b *RequestBuilder) Build() (Request, error) {
if b.err != nil {
return Request{}, b.err
}
return b.req, nil
}
func main() {
r, err := NewRequest("https://api.example.com").Method("POST").Retries(3).Build()
fmt.Printf("%+v %v\n", r, err)
_, err = NewRequest("https://api.example.com").Timeout(0).Retries(-1).Build()
fmt.Println(err)
}
{URL:https://api.example.com Method:POST Timeout:10s Retries:3} <nil>
timeout must be positive: 0s
2つ目の呼び出しでは Timeout(0) と Retries(-1) の両方が不正ですが、返るのは最初のエラーだけです。全部の誤りを一度に知らせたいなら、err をスライスにして errors.Join で束ねます。
Goでこの形が実際に使われているのは、SQLのように部品を順に積み上げて最後に文字列を出す場面です。たとえばSQLビルダーのsquirrelは sq.Select("*").From("users") のように句をつなぎ、最後の ToSql() でSQL文・引数・エラーを返します。一方、HTTPクライアントやサーバーの設定のような「任意項目付きのコンストラクタ」には、次のFunctional Optionsの方がGoでは一般的です。
Functional Optionsとは:Goで定番の任意項目の渡し方
Functional Optionsは、任意項目を「設定を書き換える関数」として可変長引数で受け取る書き方です。Rob Pikeが2014年1月のブログ記事「Self-referential functions and the design of options」で考え方を示し、Dave Cheneyが同年10月の「Functional options for friendly APIs」で広めました。
package main
import (
"fmt"
"time"
)
type Server struct {
addr string
timeout time.Duration
maxConn int
}
type Option func(*Server) error
func WithTimeout(d time.Duration) Option {
return func(s *Server) error {
if d <= 0 {
return fmt.Errorf("timeout must be positive: %v", d)
}
s.timeout = d
return nil
}
}
func WithMaxConn(n int) Option {
return func(s *Server) error {
s.maxConn = n
return nil
}
}
// NewServer は必須の addr を引数で受け、残りはオプションで上書きする
func NewServer(addr string, opts ...Option) (*Server, error) {
s := &Server{addr: addr, timeout: 30 * time.Second, maxConn: 100}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
func main() {
s, err := NewServer(":8080")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(s.addr, s.timeout, s.maxConn)
s, err = NewServer(":8080", WithTimeout(5*time.Second), WithMaxConn(1000))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(s.addr, s.timeout, s.maxConn)
_, err = NewServer(":8080", WithTimeout(-1))
fmt.Println(err)
}
:8080 30s 100
:8080 5s 1000
timeout must be positive: -1ns
必須の addr は普通の引数で受け、任意項目は渡したものだけがデフォルト値を上書きします。新しいオプションを足しても NewServer のシグネチャは変わらないので、ライブラリの利用者のコードを壊しません。gRPCのGo実装の grpc.NewClient(target string, opts ...DialOption) も同じ形です。
Uber社のGoスタイルガイドは、コンストラクタや公開APIで「将来拡張する見込みがあり、特に引数がすでに3つ以上ある」場合にこのパターンを使うよう勧めています。実装は上の例のような関数型ではなく、非公開メソッド apply を持つ Option インタフェースを推奨しています。関数の値は nil 以外と比較できませんが、bool や比較可能な構造体でOptionを実装すれば、テストやモックで期待するオプションと比較できます。fmt.Stringer を実装して中身を表示させることもできる、というのが理由です。
Goでの使い分け:Builder・Functional Options・設定構造体
Goで任意項目の多いオブジェクトを作る方法は、実際には4つあります。表で並べてから判断の基準を示します。
| 方式 | 書き方の例 | 向く場面 | 弱点 |
|---|---|---|---|
| 構造体リテラル | &http.Server{Addr: ":8080"} |
ゼロ値が妥当な既定値 | 値型の項目は未設定と0が同じ |
| 設定構造体+New関数 | New(Config{...}) |
設定をファイルから読む | 既定値の補完を自前で書く |
| Functional Options | New(addr, WithTimeout(5*time.Second)) |
公開ライブラリ | オプション関数の数だけ記述が増える |
| メソッドチェーンBuilder | New().A().B().Build() |
順に積み上げて出力 | エラーの扱いが不自然 |
判断は次の順で行います。
- ゼロ値がそのまま既定値として通用するか。標準ライブラリの
http.Serverは、ReadTimeoutとWriteTimeoutのゼロ値を「タイムアウトなし」と解釈するため、構造体リテラルで済ませています(ReadHeaderTimeoutとIdleTimeoutはゼロのときReadTimeoutの値を引き継ぎます)。これで足りるならBuilderもオプションも要りません。 - 「0を明示した」と「未設定」を区別する必要があるか。リトライ回数0回とデフォルト3回を分けたい場合、
intの項目のままでは区別できません。ポインタ型の項目や設定済みフラグを足す方法もありますが、利用者にnilの意味を覚えさせることになるため、ここでFunctional Optionsを選びます。 - 利用者が社外にいるか。公開ライブラリなら、互換性を保ったまま項目を足せるFunctional Optionsが第一候補です。社内の1パッケージ内で完結するなら、設定構造体の方が読みやすく、YAMLやJSONからそのまま読み込めます。
- 組み立ての途中経過そのものに意味があるか。SQLの句やHTTPリクエストのヘッダーのように、部品を順に積み上げて最後に1つの出力にまとめるものだけ、メソッドチェーンのBuilderにします。
JavaのBuilderをそのままGoへ移植して、全部の構造体にBuilderを付けるのは避けるべきです。項目ごとにsetter風メソッドが1つ増え、エラーの記録と Build() での検証も構造体ごとに書くことになり、項目を足すたびに構造体とBuilderの2か所を直す必要が出ます。
Builderパターンを使わない方がいい場面
キーワード引数や名前付き引数がある言語
Effective Java型のBuilderは、Javaに名前付き引数とデフォルト引数が無いことを補う仕組みです。Pythonのキーワード引数、Kotlinの名前付き引数とデフォルト値、TypeScriptのオブジェクトリテラル引数があれば、テレスコーピング・コンストラクタは最初から起きません。Pythonなら dataclass で不変のオブジェクトと検証まで書けます。
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Request:
url: str
method: str = "GET"
timeout: float = 10.0
retries: int = 0
def __post_init__(self):
if self.timeout <= 0:
raise ValueError("timeout must be positive")
r = Request("https://api.example.com", method="POST", retries=3)
print(r)
print(replace(r, timeout=5.0))
try:
Request("https://api.example.com", timeout=0)
except ValueError as e:
print(e)
Request(url='https://api.example.com', method='POST', timeout=10.0, retries=3)
Request(url='https://api.example.com', method='POST', timeout=5.0, retries=3)
timeout must be positive
Builderの build() で行う検証は __post_init__ に、既存オブジェクトから一部だけ変えたコピーを作る処理は dataclasses.replace に置き換わります。これらの言語でBuilderを書く意味が残るのは、GoF型のように「同じ手順から別の表現を作る」場合や、KotlinのType-safe buildersのようにHTMLや設定の階層構造をDSLとして組み立てる場合です。任意項目を名前付きで渡したいだけなら、言語機能で足ります。
項目が少ない、または全部必須のオブジェクト
項目が2つしかないオブジェクトや、全部の項目が必須で省略できないオブジェクトにBuilderを付けると、完成品のクラスとBuilderクラスで同じ項目を二重に管理することになります。項目を1つ足すたびに2か所を直す必要があり、片方の直し漏れはコンパイルが通ったまま残ります。任意項目が無く、引数の取り違えも型で防げる程度の数なら、普通のコンストラクタで十分です。
Builderパターンと他の生成パターンの違い
GoFの生成パターン5つのうち、Builderと混同されやすいものを並べます。23パターン全体の位置付けはGoFのデザインパターン23種類の一覧にまとめています。
| パターン | 隠すもの | 完成品の返し方 |
|---|---|---|
| Builder | 組み立ての手順と表現 | 手順を重ねた後に返す |
| Factory Method | どのクラスを作るか | 1回の呼び出しで返す |
| Abstract Factory | 関連する部品一式の種類 | 部品ごとに即座に返す |
| Prototype | 生成方法(既存の複製) | 複製して返す |
FactoryとBuilderの分かれ目は「選ぶ」か「組み立てる」かです。Factoryは条件に応じてどの実装クラスを作るかを決め、多くは1回の呼び出しで完成品を返します。Builderは1つの製品を段階的に組み立てる手順に主眼があり、GoF型では渡すBuilderによって出来上がる表現が変わります。GoFの書籍も、似た役割のAbstract Factoryとの違いを「Builderは段階的に組み立てて最後の手順で完成品を返し、Abstract Factoryは製品をすぐに返す」点に置いています。ファクトリ側の詳細はファクトリパターンの3種類の違いと実装例を参照してください。
Builderパターンに関するよくある質問
Builderパターンとは何ですか?
複雑なオブジェクトの組み立て手順と、出来上がる表現を分けて段階的に作る生成パターンです。GoFの23パターンの1つで、Directorが同じ手順を呼び、差し替えたBuilderごとに別の表現を作るのが原型です。現在はJavaなどで、任意項目の多いオブジェクトをメソッドチェーンで作る書き方を指すことも多くなっています。
BuilderパターンとFactoryパターンの違いは?
Factoryは「どのクラスのオブジェクトを作るか」を呼び出し側から隠し、1回の呼び出しで完成品を返します。Builderは「どう組み立てるか」を複数の手順に分け、最後に完成品を返します。実装クラスの作り分けならFactory、段階的な組み立てや項目の多いオブジェクトの生成ならBuilderです。
Directorクラスは必ず必要ですか?
必要ありません。Directorは同じ組み立て手順を何度も再利用するときに、その順序を1か所にまとめる役です。Effective Java型のように呼び出し側がメソッドチェーンで直接組み立てる場合は、Directorを置きません。
Lombokの@Builderとは何ですか?
Javaのクラスやコンストラクタに付けると、Effective Java型のBuilderクラスと builder() メソッドをコンパイル時に自動生成するLombokのアノテーションです。手書きで二重管理になる項目定義を1か所にできます。使い方はLombokのアノテーション解説を参照してください。
Go言語ではBuilderとFunctional Optionsのどちらを使うべきですか?
任意項目付きのコンストラクタならFunctional Optionsが第一候補です。Uberのスタイルガイドも、拡張が見込まれ、特に引数がすでに3つ以上ある場合に推奨しています。ゼロ値が既定値として通用するなら構造体リテラルで足ります。メソッドチェーンのBuilderは、SQL組み立てのように部品を順に積み上げて最後に出力するものに限ると、Goのコードとしてなじみます。