DPDesign Pattern Quest
#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 問のクイズで、 このパターンを身体に染み込ませよう。

挑戦する →