ビジターと二重ディスパッチ
以下の幾何学形状のクラス階層を眺めてみてください (注: 疑似コードです):
interface Graphic is
method draw()
class Shape implements Graphic is
field id
method draw()
// ……
class Dot extends Shape is
field x, y
method draw()
// ……
class Circle extends Dot is
field radius
method draw()
// ……
class Rectangle extends Shape is
field width, height
method draw()
// ……
class CompoundGraphic implements Graphic is
field children: array of Graphic
method draw()
// ……
コードは正常に動作し、 アプリは本番稼働しています。 しかし、 ある日、 エクスポート機能を追加することにしました。 エクスポートのコードをこれらのクラス中に置くのは、 ちょっと変です。 エクスポート機能をこの階層のクラスに追加する代わりに、 このクラス階層からは独立した新規クラスを一つ作り、 そこにエクスポートに関する全てのロジックを入れることにしました。 そのクラスには、 各オブジェクトの公開状態を XML の文字列としてエクスポートするための複数のメソッドが入ります。
class Exporter is
method export(s: Shape) is
print("Exporting shape")
method export(d: Dot)
print("Exporting dot")
method export(c: Circle)
print("Exporting circle")
method export(r: Rectangle)
print("Exporting rectangle")
method export(cs: CompoundGraphic)
print("Exporting compound")
コードは良さそうに見えます。 試してみましょう。
class App() is
method export(shape: Shape) is
Exporter exporter = new Exporter()
exporter.export(shape);
app.export(new Circle());
// 残念ながら、"Exporting shape" が出力されます。
ちょと待って! どうして?
コンパイラーの立場で考える
注: 以下の情報は、 近代的なオブジェクト指向プログラミング言語のほとんど (Java、 C#、 PHP など) に当てはまります。
遅延・動的バインディング
自分がコンパイラーになったと思ってください。 以下のコードをどうコンパイルすればいいか決めるところです:
method drawShape(shape: Shape) is
shape.draw();
えっと、 draw
メソッドは、 Shape
クラスで定義されています。 あ、 ちょっと待って、 四つのサブクラスでこのメソッドを上書きしています。 どの実装がここで呼ばれるのか、 安全に決定できるかな? そうは見えない。 確実な唯一の方法は、 プログラムを走らせて、 メソッドに渡されたオブジェクトのクラスを調べること。 確実にわかっていることは、 オブジェクトが draw
メソッドの実装を行う予定ということだけ。
なので、 結果の機械語は、 shape
パラメーターに渡されたオブジェクトのクラスを調べて、 適切なクラスの draw
メソッドの実装を選ぶことをします。
このような動的な型のチェックは、 遅延 (または動的) バインディングと呼ばれます。
- 遅延は、 コンパイル後、 実行時にオブジェクトと実装をリンクするため。
- 動的は、 どの新規オブジェクトも、 異なる実装にリンクする必要があるかもしれないから。
早期・静的バインディング
では、 以下のコードを 「コンパイル」 してみましょう:
method exportShape(shape: Shape) is
Exporter exporter = new Exporter()
exporter.export(shape);
すべてのことは 2 行目で明らかです。 Exporter
クラスは、 独自定義のコンストラクターがないので、 オブジェクトのインスタンスを一つ作るだけです。 export
の呼び出しはどうでしょうか? Exporter
には、 名前が同じでパラメーター型が異なる五つのメソッドがあります。 どれを呼べばいいでしょうか? どうやら、 ここでも動的バインディングが必要そうです。
もう一つ問題点があります。 適切な export
メソッドが Exporter
クラス中にない新たな形状クラスにはどう対応しますか? たとえば、 Ellipse
(楕円) オブジェクトとかです。 コンパイラーは、 適切な多重定義 (= overload。 上書き= overwrite とは異なる) されたメソッドが存在することは保証できません。 このようなあいまいな状況は、 コンパイラーの許容範囲外です。
そのため、 コンパイラー開発者は安全性を優先し、 多重定義されたメソッドには早期 (または静的) バインディングを使用します:
- 早期は、 プログラム開始時より前のコンパイル時に行われるから。
- 静的は、 実行時の変更ができないから。
最初の例に戻りましょう。 渡される引数が、 Shape
階層のものであることはわかっています。 Shape
クラスか、 そのサブクラスのうちの一つです。 それと、 Exporter
クラスには、 Shape
クラスのエクスポートをサポートする基本的な実装、 export(s: Shape)
があることがわかっています。
あいまいな状況になることなく与えられたコードに安全にリンクできる唯一の実装は、 そうなります。 Rectangle
オブジェクトを exportShape
に渡しても Exporter
が export(s: Shape)
メソッドを呼び続けるのは、 そういう理由によります。
二重ディスパッチ
二重ディスパッチは、 多重定義メソッドとともに動的バインディングを使用することを可能にする技法です。 このように行われます:
class Visitor is
method visit(s: Shape) is
print("Visited shape")
method visit(d: Dot)
print("Visited dot")
interface Graphic is
method accept(v: Visitor)
class Shape implements Graphic is
method accept(v: Visitor)
// コンパイラーは、`this` が確実に `Shape` であることを知っている。
// そのため、`visit(s: Shape)` を安全に呼び出し可能。
v.visit(this)
class Dot extends Shape is
method accept(v: Visitor)
// コンパイラーは、`this` が `Dot` であることを知っている。
// そのため、`visit(s: Dot)` を安全に呼び出し可能。
v.visit(this)
Visitor v = new Visitor();
Graphic g = new Dot();
// `accept` は、(多重定義ではなく)上書きされている。コンパイラー
// は、それを動的にバインディング。従って、`accept` は、メソッド
// を呼び出すオブジェクトに対応したクラス(この場合 `Dot`)上で実行
// される。
g.accept(v);
// 「Visited dot」を表示。
後書き
Visitor パターンは二重ディスパッチの原則に基づいていますが、 それが主目的ではありません。 Visitor を使うと、 クラス階層中のクラスの既存コードを変更せずに、 「外的」 操作をクラス階層全体に追加できます。