
Visitor(ビジター)は、データ構造と、その処理(操作)を分離するデザインパターンです。
各要素(オブジェクト)は「受け入れる(accept)」だけを持ち、実際の処理は「訪問者(Visitor)」が担当する。
分かりやすいたとえ話
「本棚(データ構造)」に本がたくさんあるとする。
本は、自分のデータ(タイトル・価格など)を持っている状態。
でも、「合計金額を計算する処理」や「目次を出す処理」などは本自身がやる必要はない。
・・・そこで、
Visitor(訪問者)が本を1冊ずつ「訪問」して、合計金額を計算したり、タイトル一覧を出したりする、という仕組み。
構成
| 役割 |
内容 |
| Element(要素) |
データ構造側のクラス(例:Book, DVD) |
| Visitor(訪問者) |
要素に対して行う処理をまとめるクラス |
| accept(visitor) |
要素が訪問者を受け入れるメソッド(visitor.visit(this)を呼ぶ) |
サンプルコード1
Visitor(訪問者インターフェース)
class Visitor {
visitBook(book) {}
visitDVD(dvd) {}
}
Element(要素クラス)
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
accept(visitor) { visitor.visitBook(this); }
}
class DVD {
constructor(title, price) {
this.title = title;
this.price = price;
}
accept(visitor) { visitor.visitDVD(this); }
}
ConcreteVisitor1:合計金額計算
class PriceCalculator extends Visitor {
constructor() { super(); this.total = 0; }
visitBook(book) { this.total += book.price; }
visitDVD(dvd) { this.total += dvd.price; }
}
ConcreteVisitor2:タイトル出力
class TitlePrinter extends Visitor {
visitBook(book) { console.log("📘 " + book.title); }
visitDVD(dvd) { console.log("💿 " + dvd.title); }
}
実行
const items = [
new Book("JavaScript入門", 2000),
new DVD("デザインパターン解説", 3500),
new Book("Node.js実践ガイド", 2800)
];
// 訪問者1:タイトル一覧を出す
const printer = new TitlePrinter();
items.forEach(item => item.accept(printer));
// => 📘 JavaScript入門
// => 💿 デザインパターン解説
// => 📘 Node.js実践ガイド
// 訪問者2:合計金額を計算
const calc = new PriceCalculator();
items.forEach(item => item.accept(calc));
console.log("💰 合計金額:", calc.total + "円");
// => 💰 合計金額: 8300円
サンプルコード2
Canvas図形+Visitorで「面積計算・色変更」をする可視化デモ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Visitor Pattern Canvas Demo</title>
<style>
body { font-family: sans-serif; text-align: center; margin-top: 20px; }
canvas { border: 1px solid #999; background: #fafafa; margin-top: 10px; }
button { margin: 5px; padding: 8px 16px; border-radius: 8px; cursor: pointer; }
</style>
</head>
<body>
<h2>🧩 Visitorパターン:Canvas図形の「面積計算」と「色変更」</h2>
<div>
<button id="areaBtn">面積を計算</button>
<button id="colorBtn">色を変更</button>
</div>
<canvas id="canvas" width="400" height="300"></canvas>
<script>
// === Visitorインターフェース ===
class Visitor {
visitCircle(circle) {}
visitSquare(square) {}
}
// === Element(要素)インターフェース ===
class Shape {
accept(visitor) { throw new Error("acceptを実装してください"); }
draw(ctx) { throw new Error("drawを実装してください"); }
}
// === 具象要素:円 ===
class Circle extends Shape {
constructor(x, y, r, color) {
super();
this.x = x; this.y = y; this.r = r; this.color = color;
}
accept(visitor) { visitor.visitCircle(this); }
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.stroke();
}
}
// === 具象要素:四角 ===
class Square extends Shape {
constructor(x, y, size, color) {
super();
this.x = x; this.y = y; this.size = size; this.color = color;
}
accept(visitor) { visitor.visitSquare(this); }
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
ctx.strokeRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
}
}
// === Visitor1: 面積計算 ===
class AreaCalculator extends Visitor {
constructor() {
super();
this.totalArea = 0;
}
visitCircle(circle) {
this.totalArea += Math.PI * circle.r * circle.r;
}
visitSquare(square) {
this.totalArea += square.size * square.size;
}
}
// === Visitor2: 色変更 ===
class ColorChanger extends Visitor {
visitCircle(circle) {
circle.color = this.randomColor();
}
visitSquare(square) {
square.color = this.randomColor();
}
randomColor() {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
return `rgb(${r},${g},${b})`;
}
}
// === Scene(描画コンテキスト) ===
class Scene {
constructor(ctx) {
this.ctx = ctx;
this.shapes = [];
}
add(shape) { this.shapes.push(shape); }
draw() {
this.ctx.clearRect(0, 0, 400, 300);
this.shapes.forEach(shape => shape.draw(this.ctx));
}
accept(visitor) {
this.shapes.forEach(shape => shape.accept(visitor));
}
}
// === 実行 ===
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const scene = new Scene(ctx);
// 図形追加
scene.add(new Circle(100, 150, 40, "#ff6666"));
scene.add(new Square(250, 150, 80, "#66ccff"));
scene.draw();
// 面積計算
document.getElementById("areaBtn").onclick = () => {
const calc = new AreaCalculator();
scene.accept(calc);
alert(`🧮 合計面積: ${Math.round(calc.totalArea)}`);
};
// 色変更
document.getElementById("colorBtn").onclick = () => {
const changer = new ColorChanger();
scene.accept(changer);
scene.draw();
};
</script>
</body>
</html>
ポイント開設
Scene は 要素(Circle, Square) を保持するコンテナです。
各図形は accept(visitor) を通じて訪問を受ける状態。
Visitorを差し替えるだけで、「面積計算」「色変更」、といった処理を後から自由に追加できるようなパターン。
メリット
・データ構造(Book, DVD)に新しい処理を追加する時、
クラスを変更せずに新しいVisitorを追加できる。
・各処理を「Visitor」として独立させられるため、コードの見通しが良くなる。
デメリット
・要素(Element)の種類が増えると、Visitorすべてを修正する必要がある。
→ 逆に「データ構造の拡張」がしにくくなる。
・ダブルディスパッチ(accept → visitXxx())の構造が少し複雑。
よく使われる場面
既存のデータ構造に対して、後からいろんな処理を追加したいとき
・ファイルツリーに対して「サイズ計算」「検索」「バックアップ」などを追加
・AST(構文木)を走査して、構文チェック・最適化・変換を行う(←実際のコンパイラで多用)
あとがき
データ構造と、その処理を分離するデザインパターンですが、実際のコードを見ると、TemplateMethodと酷似しているように思ったので、違いを調べてみると、
どちらも、次の共通点でした。
1. オブジェクトの振る舞い(アルゴリズム)を拡張するデザインパターン。
2. ポリモーフィズムを活かして、処理の変更・追加を柔軟にする。
違いは、次の通りです。
Template Methodは、処理の流れを固定し、一部だけサブクラスで差し替えるパターン。
Visitorは、データ構造を変えずに、外部から新しい処理を追加するパターン。
分かりやすくいうと、制御するのが、Template Methodは、
スーパークラスが行い、Visotorは
自分自身が行うようになります。
適正に使い分けることで、設計が安定するでしょう。
0 件のコメント:
コメントを投稿