#23 / 23
B振る舞いDIFF
ビジター
Visitor
健康診断の検査項目を増やすイメージ。患者(オブジェクト)には手を加えず、検査員(操作)だけを増やしていく。
Intent · 目的
オブジェクト構造の各要素クラスはそのままに、新しい操作(処理)を別クラス(Visitor)として外に切り出す。要素を訪問する Visitor を切り替えれば、構造側を一切いじらず振る舞いを増やせる。
Motivation · 動機
AST(抽象構文木)に対し『出力する』『型推論する』『最適化する』『コード生成する』── 操作はどんどん増えていく。これを毎回各ノードクラス(Add, Mul, If, While...)にメソッド追加で対応すると、ノードクラスは肥大化し、変更のたびに全ノードを触ることになる。Visitor なら『型推論ビジター』『出力ビジター』『最適化ビジター』とそれぞれ独立したクラスに分け、訪問先のノードに `accept(visitor)` させて二段ディスパッチで型ごとに振る舞いを書き分ける。健康診断で『身長係』『体重係』『血圧係』が順番に来て検査するのと同じ。
適用場面
- 1要素クラスの種類はほぼ固定だが、操作はこれからどんどん増える(コンパイラのAST、構造データ)。
- 2AST やドキュメントツリーなど、構造が安定している木構造。
- 3外部システム向けに別表現へ変換したい場合(出力、解析、検証、変換)。
概念図
サンプルコード
interface Visitor<R> {
visitNum(n: Num): R;
visitAdd(a: Add): R;
}
interface Expr { accept<R>(v: Visitor<R>): R; }
class Num implements Expr {
constructor(public v: number) {}
accept<R>(v: Visitor<R>){ return v.visitNum(this); }
}
class Add implements Expr {
constructor(public l: Expr, public r: Expr) {}
accept<R>(v: Visitor<R>){ return v.visitAdd(this); }
}
const evalV: Visitor<number> = {
visitNum: n => n.v,
visitAdd: a => a.l.accept(evalV) + a.r.accept(evalV),
};
const printV: Visitor<string> = {
visitNum: n => String(n.v),
visitAdd: a => `(${a.l.accept(printV)}+${a.r.accept(printV)})`,
};
const e: Expr = new Add(new Num(1), new Add(new Num(2), new Num(3)));
console.log(e.accept(evalV)); // 6
console.log(e.accept(printV)); // (1+(2+3))Pros · メリット
- +要素クラスは無傷のまま、操作をどんどん追加できる。コンパイラ系で輝く。
- +操作ごとにロジックが集約されるので、機能単位で読み・書き・テストしやすい。
- +ダブルディスパッチで、型ごとの分岐が綺麗に分かれる。
Cons · デメリット
- −新しい『要素クラス』を1個足すと、すべての Visitor に対応メソッドを追加する必要がある。要素軸の拡張に弱い。
- −I/F が太りやすい(要素の型が増えるほど、Visitor の中身も膨らむ)。
- −プライベートな状態にビジターから触りたいと、要素側のカプセル化を緩める誘惑が出る。
関連パターン
⚔ ready to test
理解度を確かめる時間だ
3 問のクイズで、 このパターンを身体に染み込ませよう。