カナデKanade

18. ビジターパターン (Visitor)

品物のクラスはそのままにして、品物たちにする新しい仕事を訪問者クラス1つで足すビジターパターンについて学びましょう。

ひとことで言うと
いろいろな種類の品物にする仕事を、品物クラスの外の訪問者に取り出すパターンです。新しい仕事ができたら、品物はそのままで訪問者を1つ足すだけで済みます。

問題: 仕事が増えるたびに、すべての品物クラスを直すコード

ネットショップの買い物かごを作るとしましょう。かごには種類の違う品物がまざっています。

  • 本には題名と値段があります。
  • 果物には重さと1kgあたりの値段があって、重さをかけないと値段が出ません。
  • おもちゃには値段と、割れやすいかどうかが書いてあります。

注文が入ったら、かごで2つの仕事をします。レシートを出して、品物ごとに合ったやり方で包みます。まず思いつくのは、品物クラスごとに〈値段をつける〉と〈包む〉を入れる方法です。

【本】を設計しよう:
    『題名』を「」にしよう
    『値段』を0にしよう

    最初に作られる時(【文字列】の『題名』, 【数字】の『値段』)次のようにしよう:
        『私』の『題名』を『題名』にしよう
        『私』の『値段』を『値段』にしよう

    (参考:レシートの仕事)
    〈値段をつける〉を作ろう():
        枠「· 本《{『私』の『題名』}》: {『私』の『値段』}円」を出力しよう
        『私』の『値段』を返そう

    (参考:包装の仕事)
    〈包む〉を作ろう():
        枠「· 《{『私』の『題名』}》: 紙袋に入れます」を出力しよう

【果物】を設計しよう:
    『名前』を「」にしよう
    『重さ』を0にしよう
    『キロ単価』を0にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『重さ』, 【数字】の『キロ単価』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『重さ』を『重さ』にしよう
        『私』の『キロ単価』を『キロ単価』にしよう

    〈値段をつける〉を作ろう():
        『値』を(『私』の『重さ』 * 『私』の『キロ単価』)にしよう
        枠「· {『私』の『名前』} {『私』の『重さ』}kg: {『値』}円」を出力しよう
        『値』を返そう

    〈包む〉を作ろう():
        枠「· {『私』の『名前』}: 保冷剤と一緒に箱に入れます」を出力しよう

【おもちゃ】を設計しよう:
    『名前』を「」にしよう
    『値段』を0にしよう
    『割れやすい』を偽にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『値段』, 『割れやすい』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『値段』を『値段』にしよう
        『私』の『割れやすい』を『割れやすい』にしよう

    〈値段をつける〉を作ろう():
        枠「· {『私』の『名前』}: {『私』の『値段』}円」を出力しよう
        『私』の『値段』を返そう

    〈包む〉を作ろう():
        もし(『私』の『割れやすい』)なら:
            枠「· {『私』の『名前』}: プチプチで包んで入れます」を出力しよう
        それ以外なら:
            枠「· {『私』の『名前』}: そのまま箱に入れます」を出力しよう

『かご』を【新しい【本】(「星の王子さま」, 9000), 新しい【果物】(「りんご」, 2, 4000), 新しい【おもちゃ】(「ガラスのビー玉」, 3000, 真)】にしよう

「[レシート]」を出力しよう
『合計』を0にしよう
『かご』の『品物』ごとに繰り返そう:
    『合計』を(『合計』 + 『品物』の〈値段をつける〉())にしよう
枠「合計: {『合計』}円」を出力しよう

「[包装]」を出力しよう
『かご』の『品物』ごとに繰り返そう:
    『品物』の〈包む〉()を実行しよう

実行結果
[レシート]
· 本《星の王子さま》: 9000円
· りんご 2kg: 8000円
· ガラスのビー玉: 3000円
合計: 20000円
[包装]
· 《星の王子さま》: 紙袋に入れます
· りんご: 保冷剤と一緒に箱に入れます
· ガラスのビー玉: プチプチで包んで入れます

ちゃんと動きます。でも、お店が大きくなると痛くなります。

  • 新しい仕事ができるたびに、すべての品物クラスを直さなければなりません。 「配送の重さを量る」ができたら、【本】、【果物】、【おもちゃ】を全部開いて〈重さを量る〉を1つずつ入れることになります。品物の種類が10個なら、10か所を直すことになります。
  • 品物クラスに、よその仕事がたまっていきます。 本は本なだけなのに、レジの仕事(値段をつける)も倉庫の仕事(包む)も全部本の中に入っています。仕事が増えるほど、品物クラスがどんどん太っていきます。
  • 1つの仕事がいくつものクラスに散らばっています。 包み方のルールを知るには、品物クラスを1つずつ開いてみなければなりません。包装の担当者が直す場所もあちこちにあります。

解決策: 仕事を訪問者にまかせる

ビジターパターンは、品物にする仕事を品物の外に取り出して、仕事ごとに訪問者クラス1つに集めます。

  1. 訪問者の約束を決めます。品物の種類ごとに「この品物に会ったらすること」を1つずつ約束します。
  2. 品物は自分の情報だけを持っています。訪問者が来たら、〈迎える〉1つで「私は本です」と言って、訪問者の合ったメソッドを呼んであげます。
  3. 仕事ごとに訪問者1つを作ります。レシートの仕事は【レジ係】に、包装の仕事は【包装係】に集めます。
  4. 使う側は、訪問者を連れてかごを1周します。

たとえ: 家々を回る検針員

マンションには、家ごとに形の違うメーターがあります。電気の検針員、ガスの検針員、水道の検針員が順番に来ますが、家はメーターの読み方を知らなくてもかまいません。ドアを開けて「うちはこんな形です」と迎えるだけで、検針員が自分のやり方で読んでいきます。新しく「ネット点検員」ができても、家を直す必要はありません。点検員が1人、新しく回るだけです。検針員こそが訪問者です。

だれが何をするの?

役割 この例では やること
訪問者の約束 【訪問者】(規定) 品物の種類ごとに〈本に会う〉、〈果物に会う〉、〈おもちゃに会う〉を約束します
具体的な訪問者 【レジ係】、【包装係】 1つの仕事を、品物の種類ごとにどうするかまとめておきます
品物たち 【本】、【果物】、【おもちゃ】 自分の情報を持ち、〈迎える〉で訪問者に自分の種類を教えます
使う側 最後の部分 訪問者を作って、品物に1つずつ迎えさせます

カナデで作る

同じ買い物かごを、ビジターパターンで作り直してみましょう。

(参考:ステップ1 — 訪問者の約束。品物の種類ごとに「この品物に会ったらすること」が1つずつあります)
【訪問者】を規定しよう:
    〈本に会う〉がなければならない(【本】の『本』)
    〈果物に会う〉がなければならない(【果物】の『果物』)
    〈おもちゃに会う〉がなければならない(【おもちゃ】の『おもちゃ』)

(参考:ステップ2 — 品物は自分の情報だけを持ち、来た訪問者に「私は本です」と教えます)
【本】を設計しよう:
    『題名』を「」にしよう
    『値段』を0にしよう

    最初に作られる時(【文字列】の『題名』, 【数字】の『値段』)次のようにしよう:
        『私』の『題名』を『題名』にしよう
        『私』の『値段』を『値段』にしよう

    〈迎える〉を作ろう(【訪問者】の『訪問者』):
        『訪問者』の〈本に会う〉(『私』)を実行しよう

【果物】を設計しよう:
    『名前』を「」にしよう
    『重さ』を0にしよう
    『キロ単価』を0にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『重さ』, 【数字】の『キロ単価』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『重さ』を『重さ』にしよう
        『私』の『キロ単価』を『キロ単価』にしよう

    〈迎える〉を作ろう(【訪問者】の『訪問者』):
        『訪問者』の〈果物に会う〉(『私』)を実行しよう

【おもちゃ】を設計しよう:
    『名前』を「」にしよう
    『値段』を0にしよう
    『割れやすい』を偽にしよう

    最初に作られる時(【文字列】の『名前』, 【数字】の『値段』, 『割れやすい』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『値段』を『値段』にしよう
        『私』の『割れやすい』を『割れやすい』にしよう

    〈迎える〉を作ろう(【訪問者】の『訪問者』):
        『訪問者』の〈おもちゃに会う〉(『私』)を実行しよう

(参考:ステップ3 — 仕事ごとに訪問者1つ。レシートの仕事はすべてレジ係に集まっています)
【訪問者】に従う【レジ係】を設計しよう:
    『合計』を0にしよう

    〈本に会う〉を作ろう(【本】の『本』):
        枠「· 本《{『本』の『題名』}》: {『本』の『値段』}円」を出力しよう
        『私』の『合計』を(『私』の『合計』 + 『本』の『値段』)にしよう

    〈果物に会う〉を作ろう(【果物】の『果物』):
        『値』を(『果物』の『重さ』 * 『果物』の『キロ単価』)にしよう
        枠「· {『果物』の『名前』} {『果物』の『重さ』}kg: {『値』}円」を出力しよう
        『私』の『合計』を(『私』の『合計』 + 『値』)にしよう

    〈おもちゃに会う〉を作ろう(【おもちゃ】の『おもちゃ』):
        枠「· {『おもちゃ』の『名前』}: {『おもちゃ』の『値段』}円」を出力しよう
        『私』の『合計』を(『私』の『合計』 + 『おもちゃ』の『値段』)にしよう

(参考:包装の仕事はすべて包装係に集まっています)
【訪問者】に従う【包装係】を設計しよう:
    〈本に会う〉を作ろう(【本】の『本』):
        枠「· 《{『本』の『題名』}》: 紙袋に入れます」を出力しよう

    〈果物に会う〉を作ろう(【果物】の『果物』):
        枠「· {『果物』の『名前』}: 保冷剤と一緒に箱に入れます」を出力しよう

    〈おもちゃに会う〉を作ろう(【おもちゃ】の『おもちゃ』):
        もし(『おもちゃ』の『割れやすい』)なら:
            枠「· {『おもちゃ』の『名前』}: プチプチで包んで入れます」を出力しよう
        それ以外なら:
            枠「· {『おもちゃ』の『名前』}: そのまま箱に入れます」を出力しよう

(参考:ステップ4 — 訪問者を連れて、かごを1周します)
『かご』を【新しい【本】(「星の王子さま」, 9000), 新しい【果物】(「りんご」, 2, 4000), 新しい【おもちゃ】(「ガラスのビー玉」, 3000, 真)】にしよう

「[レシート]」を出力しよう
『レジ係』を新しい【レジ係】()にしよう
『かご』の『品物』ごとに繰り返そう:
    『品物』の〈迎える〉(『レジ係』)を実行しよう
枠「合計: {『レジ係』の『合計』}円」を出力しよう

「[包装]」を出力しよう
『包装係』を新しい【包装係】()にしよう
『かご』の『品物』ごとに繰り返そう:
    『品物』の〈迎える〉(『包装係』)を実行しよう

実行結果
[レシート]
· 本《星の王子さま》: 9000円
· りんご 2kg: 8000円
· ガラスのビー玉: 3000円
合計: 20000円
[包装]
· 《星の王子さま》: 紙袋に入れます
· りんご: 保冷剤と一緒に箱に入れます
· ガラスのビー玉: プチプチで包んで入れます

結果はリファクタリング前と1行も変わりません。でも今は、品物クラスには自分の情報と〈迎える〉だけが残り、レシートの仕事は【レジ係】に、包装の仕事は【包装係】に1つにまとまっています。

ステップごとにもう一度

  • ステップ1 — 訪問者の約束。【訪問者】は、品物の種類ごとにメソッドを1つずつ約束します。だから訪問者を作る時、どの品物も忘れることができません。
  • ステップ2 — 品物は迎えるだけ。【本】の〈迎える〉は、『訪問者』の〈本に会う〉(『私』)を呼ぶだけです。来た訪問者がレジ係なのか包装係なのか知らなくてもかまいません。そのかわり「自分は本だ」ということは本がいちばんよく知っているので、合ったメソッドは本が選んであげます。
  • ステップ3 — 仕事ごとに訪問者1つ。【レジ係】を開けば、レシートのルールが品物の種類ごとにひと目で見えます。合計もレジ係が『合計』にためていきます。包み方のルールは【包装係】1か所にしかありません。
  • ステップ4 — 1周する。 使う側は、品物が何の種類かを調べません。すべての品物に同じように〈迎える〉を呼べば、品物が自分で訪問者の合ったメソッドにつないでくれます。

どうして2回通るの?
使う側がいきなり『レジ係』の〈本に会う〉(『品物』)を呼ぶには、その品物が本かどうかを先に知らなければなりません。すると、また「本なら…果物なら…」と調べることになります。品物が〈迎える〉で自分に合ったメソッドを選んでくれれば、使う側は種類を知らなくて済みます。品物が1回、訪問者が1回選ぶことで、「どの品物 × どの仕事」がぴったり決まります。

訪問者は約束にある品物をすべて扱わなければならないので、1つでも忘れるとその場で止められます。

【訪問者】を規定しよう:
    〈本に会う〉がなければならない(『本』)
    〈果物に会う〉がなければならない(『果物』)
    〈おもちゃに会う〉がなければならない(『おもちゃ』)

(参考:重さを量る訪問者を新しく作っていて、おもちゃを忘れました)
【訪問者】に従う【重さ係】を設計しよう:
    〈本に会う〉を作ろう(『本』):
        「· 本: 0.3kg」を出力しよう

    〈果物に会う〉を作ろう(『果物』):
        枠「· 果物: {『果物』の『重さ』}kg」を出力しよう

『重さ係』を新しい【重さ係】()にしよう

新しい仕事を足してみると

「配送の重さを量る」ができたとしましょう。

  • 前のやり方:【本】、【果物】、【おもちゃ】を全部開いて、〈重さを量る〉を1つずつ入れなければなりません。
  • ビジターパターン:【訪問者】に従う【重さ係】クラスを1つ新しく作るだけで済みます。品物クラスは1行も直しません。

やってみよう

  1. 本は0.3kg、果物は自分の重さ、おもちゃは0.5kgで量る【重さ係】を作って、かご全体の配送の重さを出力してみてください。
  2. 今度は新しい品物【植木鉢】を足してみてください。そのためには【訪問者】に〈植木鉢に会う〉を足して、【レジ係】と【包装係】にも全部入れなければなりません。新しい仕事を足すのはかんたんでも、新しい品物を足すのは大変だということを感じてみてください。

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

  • 品物の種類はあまり変わらないのに、その品物たちにする仕事がどんどん増える時
  • 1つの仕事のルールを1か所に集めて見たい時(計算のルール、包装のルール、出力のルール…)
  • 品物クラスに、品物と関係ない仕事がたまってきている時
  • 文書の段落・表・図、プログラムの文・式のように、種類が決まった構造全体を1周して仕事をする時

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

いいところ 気になるところ
新しい仕事を足しても、品物クラスはそのままです 新しい品物の種類を足すと、すべての訪問者を直さなければなりません
1つの仕事のルールが訪問者1か所に集まります 訪問者が品物の情報を読む必要があるので、品物の中身を隠しにくくなります
訪問者が回りながら、合計のような値をためておけます 〈迎える〉を通って2回呼ぶ流れが、最初はなじみにくいです

早すぎるパターンは毒です。
仕事が1つか2つだけで、これ以上増えそうにないなら、品物クラスにメソッドを置くほうが単純です。品物の種類がよく変わるなら、訪問者はかえって荷物になります。仕事が増え続けて、直すたびに品物クラスをいくつも開くようになった時に、ビジターを取り出しても遅くありません。

ほかのパターンとの関係

  • コンポジットパターンとよく合います。 箱の中に箱が入った木の形を訪問者が1周して、箱と品物にそれぞれ合った仕事をすることができます。
  • イテレーターパターンと一緒に使えます。 イテレーターは「次の品物を1つずつ取り出す方法」を、ビジターは「取り出した品物にすること」を受け持ちます。
  • ストラテジーパターンと似て見えます。 どちらも仕事をほかのオブジェクトにまかせます。ストラテジーは1つの仕事のやり方を取りかえ、ビジターは品物の種類ごとに違う仕事を1つのオブジェクトに集めます。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。