カナデKanade

11. テンプレートメソッドパターン (Template Method)

仕事の順番(骨組み)は親が決め、違う段階だけを子が埋めるテンプレートメソッドパターンについて学びましょう。

ひとことで言うと
いくつものクラスが同じ順番で仕事をするのに、いくつかの段階だけが違う時、順番は親が1回だけ決め、違う段階だけを子が埋めるようにするパターンです。

問題: 順番が同じなのに、別々に書かれたコード

カフェで飲み物を作るプログラムを作るとしましょう。コーヒーと緑茶は、作る順番がほとんど同じです。

順番 コーヒー 緑茶
1 お湯を沸かす お湯を沸かす
2 豆にお湯を注いで落とす 茶葉を蒸らす
3 カップに注ぐ カップに注ぐ
4 砂糖を入れる レモンを入れる

まず思いつくのは、飲み物ごとにクラスを作り、順番をそれぞれ書く方法です。

【コーヒー】を設計しよう:
    〈作る〉を作ろう():
        「お湯を沸かします。」を出力しよう
        「豆にお湯を注いで落とします。」を出力しよう
        「カップに注ぎます。」を出力しよう
        「砂糖を入れます。」を出力しよう

(参考:順番はコーヒーと同じで、2つの段階だけが違います)
【緑茶】を設計しよう:
    〈作る〉を作ろう():
        「お湯を沸かします。」を出力しよう
        「茶葉を蒸らします。」を出力しよう
        「カップに注ぎます。」を出力しよう
        「レモンを入れます。」を出力しよう

「(コーヒー)」を出力しよう
新しい【コーヒー】()の〈作る〉()を実行しよう
「(緑茶)」を出力しよう
新しい【緑茶】()の〈作る〉()を実行しよう

実行結果
(コーヒー)
お湯を沸かします。
豆にお湯を注いで落とします。
カップに注ぎます。
砂糖を入れます。
(緑茶)
お湯を沸かします。
茶葉を蒸らします。
カップに注ぎます。
レモンを入れます。

ちゃんと動きます。でも、飲み物が増えると痛くなります。

  • 同じ順番がコピーされます。「お湯を沸かす → 抽出する → 注ぐ → 添える」という順番が、【コーヒー】と【緑茶】に別々に書かれています。ココアやゆず茶ができたら、またコピーします。
  • 順番を変えると、すべての飲み物を直さなければなりません。「注ぐ前にカップを温めてください」というルールができたら、飲み物のクラスを1つずつ開いて同じ行を入れる必要があります。
  • 同じ段階もコピーされます。「お湯を沸かします」と「カップに注ぎます」はどの飲み物でも同じなのに、飲み物ごとに書かれています。お湯の沸かし方を変えるには、全部の場所を直さなければなりません。
  • 新しい飲み物を作る時に、段階を忘れやすいです。 順番を一から書き直していると、カップに注ぐのを忘れても、だれも教えてくれません。

解決策: 順番は親が、違う段階は子が

テンプレートメソッドパターンは、順番(骨組み)を親クラス1か所に上げます。

  1. 親を下設計しよう(抽象クラス)で作り、仕事の順番を書いたメソッド〈作る〉を置きます。このメソッドこそがテンプレートメソッドです。
  2. すべての飲み物で同じ段階(〈お湯を沸かす〉、〈注ぐ〉)は、親が自分で作ります。
  3. 飲み物ごとに違う段階(〈抽出する〉、〈添える〉)は、なければならないで空けておいて子に任せます。
  4. 必ず変える必要はないけれど、変えたい子だけが変える段階も置けます。このような段階をフック(hook)と呼びます。

たとえ: 料理本の基本レシピ

料理本に「基本のスープの作り方」というレシピがあります。「① だしをとる → ② 具を入れる → ③ 味を調える → ④ 薬味をのせる」。わかめスープでも、みそ汁でもこの順番は同じで、②と④に何を入れるかだけが違います。スープごとにレシピ全体を書き直さず、「② わかめ、④ なし」のように空欄だけを埋めればいいんです。この基本のレシピが「テンプレートメソッド」です。

だれが何をするの?

役割 この例では やること
親(骨組み) 【飲み物】(下設計) 仕事の順番を決め、同じ段階は自分でします
テンプレートメソッド 【飲み物】の〈作る〉 段階を決まった順番で呼びます
空けておいた段階 〈抽出する〉、〈添える〉 子が必ず埋めなければなりません
フック 〈添えるか〉 既定は真で、変えたい子だけが変えます
子 【コーヒー】、【緑茶】、【アメリカーノ】 違う段階だけを埋めます

カナデで作る

同じカフェをテンプレートメソッドパターンで作り直してみましょう。何も添えないアメリカーノも1つ足しました。

(参考:ステップ1 — 順番(骨組み)は親が1回だけ決めます)
【飲み物】を下設計しよう:
    〈作る〉を作ろう():
        『私』の〈お湯を沸かす〉()を実行しよう
        『私』の〈抽出する〉()を実行しよう
        『私』の〈注ぐ〉()を実行しよう
        もし(『私』の〈添えるか〉()が真と同じだ)なら:
            『私』の〈添える〉()を実行しよう

    (参考:ステップ2 — みんな同じ段階は、親が自分でします)
    〈お湯を沸かす〉を作ろう():
        「お湯を沸かします。」を出力しよう

    〈注ぐ〉を作ろう():
        「カップに注ぎます。」を出力しよう

    (参考:ステップ3 — 飲み物ごとに違う段階は、子に任せます)
    〈抽出する〉がなければならない()
    〈添える〉がなければならない()

    (参考:変えたい子だけが変える「フック」の段階です。既定は真)
    【論理】を返す〈添えるか〉を作ろう():
        真を返そう

【飲み物】をもとにして【コーヒー】を設計しよう:
    〈抽出する〉を作ろう():
        「豆にお湯を注いで落とします。」を出力しよう

    〈添える〉を作ろう():
        「砂糖を入れます。」を出力しよう

【飲み物】をもとにして【緑茶】を設計しよう:
    〈抽出する〉を作ろう():
        「茶葉を蒸らします。」を出力しよう

    〈添える〉を作ろう():
        「レモンを入れます。」を出力しよう

(参考:何も添えない飲み物は、フックだけを変えます)
【飲み物】をもとにして【アメリカーノ】を設計しよう:
    〈抽出する〉を作ろう():
        「エスプレッソをお湯で割ります。」を出力しよう

    〈添える〉を作ろう():
        「(添えるものなし)」を出力しよう

    【論理】を返す〈添えるか〉を作ろう():
        偽を返そう

『飲み物たち』を【新しい【コーヒー】(), 新しい【緑茶】(), 新しい【アメリカーノ】()】にしよう
『飲み物たち』の『飲み物』ごとに繰り返そう:
    「(1杯)」を出力しよう
    『飲み物』の〈作る〉()を実行しよう

実行結果
(1杯)
お湯を沸かします。
豆にお湯を注いで落とします。
カップに注ぎます。
砂糖を入れます。
(1杯)
お湯を沸かします。
茶葉を蒸らします。
カップに注ぎます。
レモンを入れます。
(1杯)
お湯を沸かします。
エスプレッソをお湯で割ります。
カップに注ぎます。

コーヒーと緑茶は、リファクタリング前と同じ順番で作られます。アメリカーノは、フック〈添えるか〉が偽を返すので、添える段階を飛ばしました。

ステップごとにもう一度

  • ステップ1 — 順番は1か所に。「お湯を沸かす → 抽出する → 注ぐ → 添える」の順番は、【飲み物】の〈作る〉の中にだけあります。子たちは〈作る〉を作らず、親のものをそのまま使います。
  • ステップ2 — 同じ段階は親が。〈お湯を沸かす〉と〈注ぐ〉は、親に1回だけあります。お湯の沸かし方を変えるなら、ここだけ直せば済みます。
  • ステップ3 — 違う段階は子が。〈抽出する〉と〈添える〉は、なければならないで空けておきました。【コーヒー】で『私』の〈抽出する〉()が呼ばれると、親の〈作る〉の中から呼んでも子の〈抽出する〉が実行されます。親は順番だけを知っていて、何を抽出するかは知りません。
  • フック — 変えたい時だけ。〈添えるか〉は、親が真を返すように作っておきました。コーヒーと緑茶は触らず、アメリカーノだけが偽を返すように上書きしました。フックのおかげで、順番を変えなくても段階を飛ばせます。

空けておいた段階を子が忘れるとどうなるでしょう?ココアを作る時に〈添える〉をうっかり忘れてみましょう。なければならないで空けておいた段階なので、カナデがエラーで教えてくれます。

【飲み物】を下設計しよう:
    〈作る〉を作ろう():
        『私』の〈抽出する〉()を実行しよう
        『私』の〈添える〉()を実行しよう

    〈抽出する〉がなければならない()
    〈添える〉がなければならない()

(参考:ココアを作る時に〈添える〉をうっかり忘れました)
【飲み物】をもとにして【ココア】を設計しよう:
    〈抽出する〉を作ろう():
        「ココアの粉を溶かします。」を出力しよう

新しい【ココア】()の〈作る〉()を実行しよう

骨組みを子が変えてしまったら?
カナデでは、子が〈作る〉を上書きすることも止めません。そうすると順番を1か所にまとめた意味がなくなるので、テンプレートメソッドは子で上書きしないということを、チームの約束として守ってください。名前に「順番」が分かるように付け、上に(参考:子で上書きしないでください)と書いておくのもいい方法です。

新しい飲み物を足してみると

「ゆず茶」ができたとしましょう。

  • 前のやり方:【ゆず茶】に4つの段階の順番を一から全部書かなければなりません。お湯を沸かすことも注ぐことも、また書きます。
  • テンプレートメソッドパターン:【飲み物】をもとにして【ゆず茶】を作り、〈抽出する〉と〈添える〉の2つだけを埋めれば済みます。順番も同じ段階も、親がやってくれます。

やってみよう

  1. 上のコードに【ゆず茶】を足してみてください。抽出するは「ゆず茶のもとを溶かします。」、添えるは「はちみつを入れます。」です。
  2. 今度は順番を変えてみましょう。すべての飲み物が注ぐ前に「カップを温めます。」をするようにするには、どこを直せばいいでしょう?【飲み物】の〈作る〉に1行、そして〈カップを温める〉を親に作れば終わりです。子のクラスは1つも直しません。

どんな時に使うといいの?

  • いくつものクラスが同じ順番で仕事をし、いくつかの段階だけが違う時
  • 順番を1か所で守らなければならない時(忘れてはいけない段階がある時)
  • 同じ段階がいくつものクラスにコピーされ始めた時
  • 子が変えていい場所といけない場所をはっきり分けたい時

いいところと気になるところ

いいところ 気になるところ
順番が1か所にしかないので、変えやすいです 継承を使うので、子は親を1つしか選べません
同じ段階をコピーしません 順番そのものが飲み物ごとに大きく違うなら、合いません
新しい子は違う段階だけを埋めれば済みます 親を直すと、すべての子に影響します
フックで段階を選んで変えられます 段階が細かく分かれると、どこで何をしているのか追いかけにくくなります

早すぎるパターンは毒です。
飲み物が2つだけで増える予定もないなら、2つのクラスがそれぞれ順番を書いてもかまいません。3つ目の飲み物を作りながら、同じ順番をまたコピーしようとした瞬間が、親に上げる時です。

ほかのパターンとの関係

  • ファクトリーパターンの「ファクトリーメソッド」が、まさにこのパターンの一種です。 順番は親が決め、そのうち「何を作るか」の1段階だけを子が埋めるものです。
  • ストラテジーパターンと比べてみましょう。 テンプレートメソッドは継承で一部の段階を変え、ストラテジーは組み合わせ(オブジェクトを差し込むこと)で方法全体を変えます。実行中に差し替えたいならストラテジーが、順番をしっかり守りたいならテンプレートメソッドが向いています。
  • コマンドパターンと一緒に使えます。 いくつものコマンドが「記録を残す → 実行する → 結果を知らせる」のように同じ順番を守らなければならないなら、コマンドの親をテンプレートメソッドにできます。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。