GoFデザインパターン23個を完全に理解するブログ #23 Visitor(ビジター)

2025/11/18

プログラミング

t f B! P L
eyecatch 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は自分自身が行うようになります。 適正に使い分けることで、設計が安定するでしょう。

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ