5. ビルダーパターン (Builder)
メソッドチェーンを活用して複雑なオブジェクトを段階的に組み立てるビルダーパターンについて学びましょう。
ひとことで言うと
選ぶものが多いオブジェクトを一度に作らず、名前の付いた段階を1つずつ踏んで組み立てるようにするパターンです。組み立て終わった時に、まとめて確かめてから渡します。
問題: 引数がずらりと並んだコンストラクタ
ハンバーガー屋さんの注文プログラムを作るとしましょう。お客さんが選ぶものがたくさんあります。
- パンの種類、パティの枚数
- チーズを入れる、レタス・トマトを抜く
- ソースの味
まず思いつくのは、コンストラクタがこれを全部引数で受け取る方法です。毎回全部書かなくて済むように、既定値も付けておきましょう。
【ハンバーガー】を設計しよう:
『パン』を【文字列】の「」にしよう
『パティ』を【数字】の0にしよう
『チーズ』を【論理】の偽にしよう
『レタス』を【論理】の偽にしよう
『トマト』を【論理】の偽にしよう
『ソース』を【文字列】の「」にしよう
(参考:選ぶものが多いので、コンストラクタの引数も多くなります)
最初に作られる時(【文字列】の『パン』 = 「ごまパン」, 【数字】の『パティ』 = 1, 【論理】の『チーズ』 = 偽, 【論理】の『レタス』 = 真, 【論理】の『トマト』 = 真, 【文字列】の『ソース』 = 「普通」)次のようにしよう:
『私』の『パン』を『パン』にしよう
『私』の『パティ』を『パティ』にしよう
『私』の『チーズ』を『チーズ』にしよう
『私』の『レタス』を『レタス』にしよう
『私』の『トマト』を『トマト』にしよう
『私』の『ソース』を『ソース』にしよう
〈見せる〉を作ろう():
枠「パン={『私』の『パン』}、パティ={『私』の『パティ』}枚、チーズ={『私』の『チーズ』}、レタス={『私』の『レタス』}、トマト={『私』の『トマト』}、ソース={『私』の『ソース』}」を出力しよう
(参考:ソースだけ変えたくても、前の引数を全部書かなければなりません)
『辛いバーガー』を新しい【ハンバーガー】(「ごまパン」, 1, 偽, 真, 真, 「辛口」)にしよう
『辛いバーガー』の〈見せる〉()を実行しよう
(参考:チーズを入れてトマトを抜くつもりが、順番を取り違えました)
『チーズバーガー』を新しい【ハンバーガー】(「ライ麦パン」, 2, 真, 偽, 真)にしよう
『チーズバーガー』の〈見せる〉()を実行しよう
(参考:パティ0枚のバーガーも、何も言われずに作られます)
『空っぽバーガー』を新しい【ハンバーガー】(「ごまパン」, 0)にしよう
『空っぽバーガー』の〈見せる〉()を実行しよう
実行結果
パン=ごまパン、パティ=1枚、チーズ=偽、レタス=真、トマト=真、ソース=辛口
パン=ライ麦パン、パティ=2枚、チーズ=真、レタス=偽、トマト=真、ソース=普通
パン=ごまパン、パティ=0枚、チーズ=偽、レタス=真、トマト=真、ソース=普通
2行目を見てください。トマトを抜くつもりだったのに、レタスが抜けています。 エラーはありませんでした。
- どれが何なのか読めません。
(「ライ麦パン」, 2, 真, 偽, 真)を見ただけでは、真がチーズなのかトマトなのか分かりません。コンストラクタを開き直して順番を数える必要があります。 - 順番を取り違えると黙って間違えます。 同じ
【論理】の引数が3つ並んでいるので、1つずれて書いても型が合ってしまい、カナデも気づけません。 - 最後の1つだけ変えるのにも、前を全部書く必要があります。 既定値があっても、ソースだけ変えるにはその前の5つを全部埋めなければなりません。
- ありえない組み合わせも作れてしまいます。 パティ0枚のバーガーのような変な注文を止める場所がありません。コンストラクタの中で確かめればいいのですが、選ぶものが増えるほどコンストラクタがどんどん大きくなります。
解決策: 名前の付いた段階で1つずつ組み立てる
ビルダーパターンは、「一度に全部渡す」を「段階ごとに1つずつ決める」に変えます。
- 作るオブジェクト(ハンバーガー)は、既定値が入った状態で始まります。
- ビルダーが作りかけのハンバーガーを持ち、選ぶものごとに名前の付いたメソッドを1つずつ持ちます。
〈チーズを入れる〉、〈トマトを抜く〉のようにです。 - それぞれの段階は、最後にビルダー自身(
『私』)を返します。だからの〈次の段階〉()でずらりと続けて呼べます。 これをメソッドチェーン(Method Chaining)と呼びます。 - 全部決めたら
〈完成〉を呼びます。ビルダーはこの時にまとめて確かめて、ハンバーガーを渡します。
たとえ: サンドイッチ屋さんの注文カウンター
サンドイッチ屋さんでは、店員さんが「パンはどうしますか?」「チーズは?」「抜きたい野菜はありますか?」「ソースは?」と1つずつ聞いてくれます。お客さんは欲しいものだけ答えればよく、言わなかったものは普通のまま入ります。最後に店員さんが「ご注文を確認しますね」とまとめて確かめてから作ってくれます。この店員さんが「ビルダー」です。材料6つを順番どおり一気に言わなければならないお店より、ずっと迷いにくいですよね。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| 作られるオブジェクト | 【ハンバーガー】 |
既定値を持った状態で始まります。コンストラクタは引数を受け取りません |
| ビルダー | 【ハンバーガービルダー】 |
作りかけのハンバーガーを持ち、段階ごとに1つずつ決めます |
| 段階 | 〈パン〉、〈パティ〉、〈チーズを入れる〉、〈トマトを抜く〉、〈ソース〉 |
1つ決めて、ビルダー自身を返します |
| 仕上げ | 〈完成〉 |
まとめて確かめて、できあがったハンバーガーを渡します |
カナデで作る
同じ注文をビルダーパターンで作り直してみましょう。
【ハンバーガー】を設計しよう:
『パン』を【文字列】の「ごまパン」にしよう
『パティ』を【数字】の1にしよう
『チーズ』を【論理】の偽にしよう
『レタス』を【論理】の真にしよう
『トマト』を【論理】の真にしよう
『ソース』を【文字列】の「普通」にしよう
〈見せる〉を作ろう():
枠「パン={『私』の『パン』}、パティ={『私』の『パティ』}枚、チーズ={『私』の『チーズ』}、レタス={『私』の『レタス』}、トマト={『私』の『トマト』}、ソース={『私』の『ソース』}」を出力しよう
(参考:ステップ1 — ビルダーが作りかけのハンバーガーを持っています)
【ハンバーガービルダー】を設計しよう:
『バーガー』を【ハンバーガー】の新しい【ハンバーガー】()に隠そう
(参考:ステップ2 — 段階ごとに名前の付いたメソッド。最後に『私』を返すので続けて呼べます)
【ハンバーガービルダー】を返す〈パン〉を作ろう(【文字列】の『種類』):
『私』の『バーガー』の『パン』を『種類』にしよう
『私』を返そう
【ハンバーガービルダー】を返す〈パティ〉を作ろう(【数字】の『枚数』):
『私』の『バーガー』の『パティ』を『枚数』にしよう
『私』を返そう
【ハンバーガービルダー】を返す〈チーズを入れる〉を作ろう():
『私』の『バーガー』の『チーズ』を真にしよう
『私』を返そう
【ハンバーガービルダー】を返す〈トマトを抜く〉を作ろう():
『私』の『バーガー』の『トマト』を偽にしよう
『私』を返そう
【ハンバーガービルダー】を返す〈ソース〉を作ろう(【文字列】の『味』):
『私』の『バーガー』の『ソース』を『味』にしよう
『私』を返そう
(参考:ステップ3 — できあがった時に、まとめて確かめてから渡します)
【ハンバーガー】を返す〈完成〉を作ろう():
もし(『私』の『バーガー』の『パティ』が1より小さい)なら:
新しい【エラー】(「パティは1枚以上でなければなりません。」)を発生させよう
『私』の『バーガー』を返そう
『辛いバーガー』を新しい【ハンバーガービルダー】()の〈ソース〉(「辛口」)の〈完成〉()にしよう
『辛いバーガー』の〈見せる〉()を実行しよう
『チーズバーガー』を新しい【ハンバーガービルダー】()の〈パン〉(「ライ麦パン」)の〈パティ〉(2)の〈チーズを入れる〉()の〈トマトを抜く〉()の〈完成〉()にしよう
『チーズバーガー』の〈見せる〉()を実行しよう
とりあえずやってみよう:
『空っぽバーガー』を新しい【ハンバーガービルダー】()の〈パティ〉(0)の〈完成〉()にしよう
発生したら(『エラー』):
(『エラー』の『メッセージ』)を出力しよう
実行結果
パン=ごまパン、パティ=1枚、チーズ=偽、レタス=真、トマト=真、ソース=辛口
パン=ライ麦パン、パティ=2枚、チーズ=真、レタス=真、トマト=偽、ソース=普通
パティは1枚以上でなければなりません。
1行目はリファクタリング前と同じです。2行目では今度こそトマトが抜けました。 そしてパティ0枚の注文は〈完成〉で止められました。
ステップごとにもう一度
- ステップ1 — ビルダーが持っておく。
【ハンバーガー】は、もうコンストラクタで引数を受け取りません。既定値で始まり、【ハンバーガービルダー】がそのハンバーガーを『バーガー』に持っています。 - ステップ2 — 名前がそのまま説明。
〈チーズを入れる〉()と〈トマトを抜く〉()は、読むだけで何をするのか分かります。順番を数える必要はなく、欲しい段階だけを好きな順に呼べばいいんです。呼ばなかったものは既定値のままです。 『私』を返そうが続けて呼べる秘密。 それぞれの段階がビルダー自身を返すので、新しい【ハンバーガービルダー】()の〈パン〉(...)の結果もビルダーです。だから、すぐ後ろにの〈パティ〉(2)をまた付けられます。- ステップ3 — 最後にまとめて確かめる。 段階ごとには確かめず、
〈完成〉で全部選んだ後にまとめて見ます。「チーズを入れるにはパティが必要」のように、いくつもの段階を一緒に見なければならないルールも、ここに置けばいいんです。
1行が長すぎる時は?
続けて呼ぶのは、短い時に見やすいやり方です。段階が多いなら、ビルダーを名札に入れておいて、1行に1段階ずつ呼んでもかまいません。それぞれの段階がビルダーのハンバーガーを直接変えるので、結果は同じです。
『ビルダー』を新しい【ハンバーガービルダー】()にしよう
『ビルダー』の〈パン〉(「ライ麦パン」)を実行しよう
『ビルダー』の〈チーズを入れる〉()を実行しよう
『チーズバーガー』を『ビルダー』の〈完成〉()にしよう
新しい選択肢を足してみると
「ベーコン追加」ができたとしましょう。
- 前のやり方: コンストラクタに引数をもう1つ付けなければなりません。最後に付ければ、ベーコンだけ入れたくても前の6つを全部書く必要があり、途中に挟めば、すでに使っているすべての
新しい【ハンバーガー】(...)の順番がずれます。 - ビルダーパターン:
【ハンバーガー】に『ベーコン』の値を1つ、【ハンバーガービルダー】に〈ベーコンを入れる〉の段階を1つ足せば終わりです。すでに使っている注文のコードは1文字も変わりません。
やってみよう
上のコードにベーコンを足してみてください。【ハンバーガー】に『ベーコン』(既定は偽)を、【ハンバーガービルダー】に〈ベーコンを入れる〉を作り、〈見せる〉にもベーコンを表示します。それから〈完成〉に「ベーコンはパティが2枚以上の時だけ入れられます」という確認を足してみましょう。
どんな時に使うといいの?
- コンストラクタの引数が多く、その多くが選べるものである時
- 同じ型の引数が並んでいて、順番を取り違えやすい時
- いくつもの選択肢を一緒に見なければならないルールがあり、全部選んだ後にまとめて確かめたい時
- オブジェクトをいくつもの段階に分けて少しずつ作らなければならない時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| 段階ごとに名前があって読みやすいです | ビルダーのクラスが1つ増え、選択肢ごとにメソッドができます |
| 欲しいものだけを、好きな順番で決められます | 作るオブジェクトとビルダーの2か所に選択肢が分かれています |
| 新しい選択肢を足しても、すでに使っているコードは変わりません | この例のビルダーはハンバーガーを1つしか持っていないので、同じビルダーで〈完成〉を2回呼ぶと同じハンバーガーが出てきます |
〈完成〉でまとめて確かめられます |
早すぎるパターンは毒です。
選ぶものが2〜3個だけなら、コンストラクタの引数で十分です。引数の順番を取り違え始めたり、同じ型の引数がずらりと並び始めたりしてからビルダーを作っても、遅くはありません。
ほかのパターンとの関係
- ファクトリーパターンと比べてみましょう。 ファクトリーはどの種類を作るか選ぶことに集中し、ビルダーは1つのものをいくつもの段階で組み立てることに集中します。「てりやきバーガーください」のようによく使う組み合わせは、工場のメソッドがビルダーを代わりに呼ぶようにもできます。
- ファサードパターンと一緒に使えます。 いくつものオブジェクトを組み立てなければならない複雑な準備をファサードの裏に隠し、ファサードの中でビルダーを使って組み立てれば、使う側は1行で済みます。
- デコレーターパターンと混同しないようにしましょう。 どちらも「1つずつ足す」ように見えますが、ビルダーは作る途中で値を決め、終われば完成したオブジェクトが1つ残ります。デコレーターは作った後で殻を重ねて機能を足します。