11. テンプレートメソッドパターン (Template Method)
仕事の順番(骨組み)は親が決め、違う段階だけを子が埋めるテンプレートメソッドパターンについて学びましょう。
ひとことで言うと
いくつものクラスが同じ順番で仕事をするのに、いくつかの段階だけが違う時、順番は親が1回だけ決め、違う段階だけを子が埋めるようにするパターンです。
問題: 順番が同じなのに、別々に書かれたコード
カフェで飲み物を作るプログラムを作るとしましょう。コーヒーと緑茶は、作る順番がほとんど同じです。
| 順番 | コーヒー | 緑茶 |
|---|---|---|
| 1 | お湯を沸かす | お湯を沸かす |
| 2 | 豆にお湯を注いで落とす | 茶葉を蒸らす |
| 3 | カップに注ぐ | カップに注ぐ |
| 4 | 砂糖を入れる | レモンを入れる |
まず思いつくのは、飲み物ごとにクラスを作り、順番をそれぞれ書く方法です。
【コーヒー】を設計しよう:
〈作る〉を作ろう():
「お湯を沸かします。」を出力しよう
「豆にお湯を注いで落とします。」を出力しよう
「カップに注ぎます。」を出力しよう
「砂糖を入れます。」を出力しよう
(参考:順番はコーヒーと同じで、2つの段階だけが違います)
【緑茶】を設計しよう:
〈作る〉を作ろう():
「お湯を沸かします。」を出力しよう
「茶葉を蒸らします。」を出力しよう
「カップに注ぎます。」を出力しよう
「レモンを入れます。」を出力しよう
「(コーヒー)」を出力しよう
新しい【コーヒー】()の〈作る〉()を実行しよう
「(緑茶)」を出力しよう
新しい【緑茶】()の〈作る〉()を実行しよう
実行結果
(コーヒー)
お湯を沸かします。
豆にお湯を注いで落とします。
カップに注ぎます。
砂糖を入れます。
(緑茶)
お湯を沸かします。
茶葉を蒸らします。
カップに注ぎます。
レモンを入れます。
ちゃんと動きます。でも、飲み物が増えると痛くなります。
- 同じ順番がコピーされます。「お湯を沸かす → 抽出する → 注ぐ → 添える」という順番が、
【コーヒー】と【緑茶】に別々に書かれています。ココアやゆず茶ができたら、またコピーします。 - 順番を変えると、すべての飲み物を直さなければなりません。「注ぐ前にカップを温めてください」というルールができたら、飲み物のクラスを1つずつ開いて同じ行を入れる必要があります。
- 同じ段階もコピーされます。「お湯を沸かします」と「カップに注ぎます」はどの飲み物でも同じなのに、飲み物ごとに書かれています。お湯の沸かし方を変えるには、全部の場所を直さなければなりません。
- 新しい飲み物を作る時に、段階を忘れやすいです。 順番を一から書き直していると、カップに注ぐのを忘れても、だれも教えてくれません。
解決策: 順番は親が、違う段階は子が
テンプレートメソッドパターンは、順番(骨組み)を親クラス1か所に上げます。
- 親を
下設計しよう(抽象クラス)で作り、仕事の順番を書いたメソッド〈作る〉を置きます。このメソッドこそがテンプレートメソッドです。 - すべての飲み物で同じ段階(
〈お湯を沸かす〉、〈注ぐ〉)は、親が自分で作ります。 - 飲み物ごとに違う段階(
〈抽出する〉、〈添える〉)は、なければならないで空けておいて子に任せます。 - 必ず変える必要はないけれど、変えたい子だけが変える段階も置けます。このような段階をフック(hook)と呼びます。
たとえ: 料理本の基本レシピ
料理本に「基本のスープの作り方」というレシピがあります。「① だしをとる → ② 具を入れる → ③ 味を調える → ④ 薬味をのせる」。わかめスープでも、みそ汁でもこの順番は同じで、②と④に何を入れるかだけが違います。スープごとにレシピ全体を書き直さず、「② わかめ、④ なし」のように空欄だけを埋めればいいんです。この基本のレシピが「テンプレートメソッド」です。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| 親(骨組み) | 【飲み物】(下設計) |
仕事の順番を決め、同じ段階は自分でします |
| テンプレートメソッド | 【飲み物】の〈作る〉 |
段階を決まった順番で呼びます |
| 空けておいた段階 | 〈抽出する〉、〈添える〉 |
子が必ず埋めなければなりません |
| フック | 〈添えるか〉 |
既定は真で、変えたい子だけが変えます |
| 子 | 【コーヒー】、【緑茶】、【アメリカーノ】 |
違う段階だけを埋めます |
カナデで作る
同じカフェをテンプレートメソッドパターンで作り直してみましょう。何も添えないアメリカーノも1つ足しました。
(参考:ステップ1 — 順番(骨組み)は親が1回だけ決めます)
【飲み物】を下設計しよう:
〈作る〉を作ろう():
『私』の〈お湯を沸かす〉()を実行しよう
『私』の〈抽出する〉()を実行しよう
『私』の〈注ぐ〉()を実行しよう
もし(『私』の〈添えるか〉()が真と同じだ)なら:
『私』の〈添える〉()を実行しよう
(参考:ステップ2 — みんな同じ段階は、親が自分でします)
〈お湯を沸かす〉を作ろう():
「お湯を沸かします。」を出力しよう
〈注ぐ〉を作ろう():
「カップに注ぎます。」を出力しよう
(参考:ステップ3 — 飲み物ごとに違う段階は、子に任せます)
〈抽出する〉がなければならない()
〈添える〉がなければならない()
(参考:変えたい子だけが変える「フック」の段階です。既定は真)
【論理】を返す〈添えるか〉を作ろう():
真を返そう
【飲み物】をもとにして【コーヒー】を設計しよう:
〈抽出する〉を作ろう():
「豆にお湯を注いで落とします。」を出力しよう
〈添える〉を作ろう():
「砂糖を入れます。」を出力しよう
【飲み物】をもとにして【緑茶】を設計しよう:
〈抽出する〉を作ろう():
「茶葉を蒸らします。」を出力しよう
〈添える〉を作ろう():
「レモンを入れます。」を出力しよう
(参考:何も添えない飲み物は、フックだけを変えます)
【飲み物】をもとにして【アメリカーノ】を設計しよう:
〈抽出する〉を作ろう():
「エスプレッソをお湯で割ります。」を出力しよう
〈添える〉を作ろう():
「(添えるものなし)」を出力しよう
【論理】を返す〈添えるか〉を作ろう():
偽を返そう
『飲み物たち』を【新しい【コーヒー】(), 新しい【緑茶】(), 新しい【アメリカーノ】()】にしよう
『飲み物たち』の『飲み物』ごとに繰り返そう:
「(1杯)」を出力しよう
『飲み物』の〈作る〉()を実行しよう
実行結果
(1杯)
お湯を沸かします。
豆にお湯を注いで落とします。
カップに注ぎます。
砂糖を入れます。
(1杯)
お湯を沸かします。
茶葉を蒸らします。
カップに注ぎます。
レモンを入れます。
(1杯)
お湯を沸かします。
エスプレッソをお湯で割ります。
カップに注ぎます。
コーヒーと緑茶は、リファクタリング前と同じ順番で作られます。アメリカーノは、フック〈添えるか〉が偽を返すので、添える段階を飛ばしました。
ステップごとにもう一度
- ステップ1 — 順番は1か所に。「お湯を沸かす → 抽出する → 注ぐ → 添える」の順番は、
【飲み物】の〈作る〉の中にだけあります。子たちは〈作る〉を作らず、親のものをそのまま使います。 - ステップ2 — 同じ段階は親が。
〈お湯を沸かす〉と〈注ぐ〉は、親に1回だけあります。お湯の沸かし方を変えるなら、ここだけ直せば済みます。 - ステップ3 — 違う段階は子が。
〈抽出する〉と〈添える〉は、なければならないで空けておきました。【コーヒー】で『私』の〈抽出する〉()が呼ばれると、親の〈作る〉の中から呼んでも子の〈抽出する〉が実行されます。親は順番だけを知っていて、何を抽出するかは知りません。 - フック — 変えたい時だけ。
〈添えるか〉は、親が真を返すように作っておきました。コーヒーと緑茶は触らず、アメリカーノだけが偽を返すように上書きしました。フックのおかげで、順番を変えなくても段階を飛ばせます。
空けておいた段階を子が忘れるとどうなるでしょう?ココアを作る時に〈添える〉をうっかり忘れてみましょう。なければならないで空けておいた段階なので、カナデがエラーで教えてくれます。
【飲み物】を下設計しよう:
〈作る〉を作ろう():
『私』の〈抽出する〉()を実行しよう
『私』の〈添える〉()を実行しよう
〈抽出する〉がなければならない()
〈添える〉がなければならない()
(参考:ココアを作る時に〈添える〉をうっかり忘れました)
【飲み物】をもとにして【ココア】を設計しよう:
〈抽出する〉を作ろう():
「ココアの粉を溶かします。」を出力しよう
新しい【ココア】()の〈作る〉()を実行しよう
骨組みを子が変えてしまったら?
カナデでは、子が〈作る〉を上書きすることも止めません。そうすると順番を1か所にまとめた意味がなくなるので、テンプレートメソッドは子で上書きしないということを、チームの約束として守ってください。名前に「順番」が分かるように付け、上に(参考:子で上書きしないでください)と書いておくのもいい方法です。
新しい飲み物を足してみると
「ゆず茶」ができたとしましょう。
- 前のやり方:
【ゆず茶】に4つの段階の順番を一から全部書かなければなりません。お湯を沸かすことも注ぐことも、また書きます。 - テンプレートメソッドパターン:
【飲み物】をもとにして【ゆず茶】を作り、〈抽出する〉と〈添える〉の2つだけを埋めれば済みます。順番も同じ段階も、親がやってくれます。
やってみよう
- 上のコードに
【ゆず茶】を足してみてください。抽出するは「ゆず茶のもとを溶かします。」、添えるは「はちみつを入れます。」です。- 今度は順番を変えてみましょう。すべての飲み物が注ぐ前に「カップを温めます。」をするようにするには、どこを直せばいいでしょう?
【飲み物】の〈作る〉に1行、そして〈カップを温める〉を親に作れば終わりです。子のクラスは1つも直しません。
どんな時に使うといいの?
- いくつものクラスが同じ順番で仕事をし、いくつかの段階だけが違う時
- 順番を1か所で守らなければならない時(忘れてはいけない段階がある時)
- 同じ段階がいくつものクラスにコピーされ始めた時
- 子が変えていい場所といけない場所をはっきり分けたい時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| 順番が1か所にしかないので、変えやすいです | 継承を使うので、子は親を1つしか選べません |
| 同じ段階をコピーしません | 順番そのものが飲み物ごとに大きく違うなら、合いません |
| 新しい子は違う段階だけを埋めれば済みます | 親を直すと、すべての子に影響します |
| フックで段階を選んで変えられます | 段階が細かく分かれると、どこで何をしているのか追いかけにくくなります |
早すぎるパターンは毒です。
飲み物が2つだけで増える予定もないなら、2つのクラスがそれぞれ順番を書いてもかまいません。3つ目の飲み物を作りながら、同じ順番をまたコピーしようとした瞬間が、親に上げる時です。
ほかのパターンとの関係
- ファクトリーパターンの「ファクトリーメソッド」が、まさにこのパターンの一種です。 順番は親が決め、そのうち「何を作るか」の1段階だけを子が埋めるものです。
- ストラテジーパターンと比べてみましょう。 テンプレートメソッドは継承で一部の段階を変え、ストラテジーは組み合わせ(オブジェクトを差し込むこと)で方法全体を変えます。実行中に差し替えたいならストラテジーが、順番をしっかり守りたいならテンプレートメソッドが向いています。
- コマンドパターンと一緒に使えます。 いくつものコマンドが「記録を残す → 実行する → 結果を知らせる」のように同じ順番を守らなければならないなら、コマンドの親をテンプレートメソッドにできます。