← 一覧に戻る
振る舞いに関するパターン

Visitor

構造を渡り歩きながら仕事をする

概要

データ構造と処理を分離

マンガでわかる Visitor

マンガでわかる Visitor #デザインパターン - Qiita

でざぱたんで覚える Visitor

ちびキャラは「ビジターたん」。ファサード商会のトップ訪問販売員で、客は家から動かず、彼女の方が一軒ずつ訪ねて相手ごとに違う業務をこなす。データ構造(客)は据え置き、処理(営業)の方を訪問者として持ち回らせる——同じ巡回で実施内容だけが異なるケースに効く、というVisitorの分離構造を訪問販売で覚える。

出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.135〜)

登場人物

  • Visitor: ConcreteElementごと訪問するvisitメソッドを宣言するインタフェース
  • ConcreteVisitor: Visitorを実装
  • Element:Visitorの訪問先インタフェース
  • Visitorを引数にもち受け入れるacceptメソッドを持つ
  • ConcreteElement:Elementを実装
  • ObjectStructure: Elementの集合

クラス図

VisitorClassDiagram.png (523×393)

このサイトの実装(ファイル/ディレクトリの一覧表示)での対応関係:

classDiagram
  class Element {
    <<interface>>
    +accept(v)
  }
  class Visitor {
    <<abstract>>
    +visit(file File)
    +visit(directory Directory)
  }
  class Entry {
    <<abstract>>
    +getName() String
    +getSize() int
    +add(entry) Entry
    +iterator() Iterator
    +toString() String
  }
  class File {
    -name String
    -size int
    +getName() String
    +getSize() int
    +accept(v)
  }
  class Directory {
    -name String
    -dir ArrayList
    +getName() String
    +getSize() int
    +add(entry) Entry
    +iterator() Iterator
    +accept(v)
  }
  class ListVisitor {
    -currentdir String
    +visit(file File)
    +visit(directory Directory)
  }
  class FileTreatmentException
  Element <|.. Entry
  Entry <|-- File
  Entry <|-- Directory
  Visitor <|-- ListVisitor
  Directory *-- Entry : dir
  Element ..> Visitor : accept(v)
  Entry ..> FileTreatmentException

やり方

メリット(用途)

  • 部品としての独立性が高まる
  • 各ConcreteElementに処理を持たせる必要がない
  • 全要素に対する新しい処理の追加が簡単
  • ConcreteVisitorを追加すればいいだけ
Java
Element.java
public interface Element {
    public abstract void accept(Visitor v);
}
ListVisitor.java
import java.util.Iterator;

public class ListVisitor extends Visitor {
    private String currentdir = "";                         // 現在注目しているディレクトリ名
    public void visit(File file) {                  // ファイルを訪問したときに呼ばれる
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {   // ディレクトリを訪問したときに呼ばれる
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
        currentdir = savedir;
    }
}
Visitor.java
public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}
Directory.java
import java.util.Iterator;
import java.util.ArrayList;

public class Directory extends Entry {
    private String name;                    // ディレクトリの名前
    private ArrayList dir = new ArrayList();      // ディレクトリエントリの集合
    public Directory(String name) {         // コンストラクタ
        this.name = name;
    }
    public String getName() {               // 名前を得る
        return name;
    }
    public int getSize() {                  // サイズを得る
        int size = 0;
        Iterator it = dir.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {         // エントリの追加
        dir.add(entry);
        return this;
    }
    public Iterator iterator() {      // Iteratorの生成
        return dir.iterator();
    }
    public void accept(Visitor v) {         // 訪問者の受け入れ
        v.visit(this);
    }
}
File.java
public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());              

            System.out.println("");
            System.out.println("Making user entries...");
            Directory yuki = new Directory("yuki");
            Directory hanako = new Directory("hanako");
            Directory tomura = new Directory("tomura");
            usrdir.add(yuki);
            usrdir.add(hanako);
            usrdir.add(tomura);
            yuki.add(new File("diary.html", 100));
            yuki.add(new File("Composite.java", 200));
            hanako.add(new File("memo.tex", 300));
            tomura.add(new File("game.doc", 400));
            tomura.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());              
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}
FileTreatmentException.java
public class FileTreatmentException extends RuntimeException {
    public FileTreatmentException() {
    }
    public FileTreatmentException(String msg) {
        super(msg);
    }
}
Entry.java
import java.util.Iterator;

public abstract class Entry implements Element {
    public abstract String getName();                                   // 名前を得る
    public abstract int getSize();                                      // サイズを得る
    public Entry add(Entry entry) throws FileTreatmentException {       // エントリを追加する
        throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {    // Iteratorの生成
        throw new FileTreatmentException();
    }
    public String toString() {                                          // 文字列表現
        return getName() + " (" + getSize() + ")";
    }
}
Go

Composite/go版のEntryをベースに、二重ディスパッチの起点となるAccept(v Visitor)を追加。Goのinterfaceは引数型によるオーバーロードを持てないため、Java版visit(File)/visit(Directory)VisitFile/VisitDirectoryという別名メソッドに分けている(Composite版で共通実装を自由関数へ切り出したのと同じ「言語制約をコメントで埋める」流儀)。一覧表示の責務はEntry自身からListVisitorへ完全に移ったので、Composite/go版にあったprintListは持たない。

実行: go run ./GoF/patterns/Visitor/go

$ go run ./GoF/patterns/Visitor/go
entry.go
package main

import (
	"errors"
	"fmt"
)

// ---- entry層(Java版の Entry.java / Element.java / FileTreatmentException.java 相当)----
//
// Composite/go版のEntryをベースに、Visitorパターンの主役であるAccept(v Visitor)
// (Java版Element.accept(Visitor v)相当)を追加した版。
// Composite/go版にあった非公開メソッド printList(prefix string) は、Visitor版の
// Java Entry.java(Visitor/Sample/Entry.java)には存在しない
// (一覧表示の責務がEntry自身からListVisitorへ完全に移ったため)。ここでも同様に削除している。

// ErrFileTreatment はJava版 FileTreatmentException 相当。
// 子を持てないEntry(File)に対してAdd()しようとしたときに返す。
var ErrFileTreatment = errors.New("this entry cannot contain other entries")

// Entry はファイルシステムのエントリ(File/Directory共通)を表す抽象。
// Java版では Element(accept)と Entry(getName/getSize/add)が別インタフェース/抽象クラスに
// 分かれているが、Goではそれをこの1つのinterfaceにまとめている。
type Entry interface {
	Name() string                   // 名前を得る
	Size() int                      // サイズを得る
	Add(entry Entry) (Entry, error) // エントリを追加する。Fileはデフォルトでエラーを返す
	Accept(v Visitor)               // Java版 Element.accept(Visitor v) 相当。二重ディスパッチの起点
}

// entryString はJava版 Entry.toString() 相当の共通ロジック。
// File/Directoryどちらの String() もこれを呼ぶだけにする。
func entryString(e Entry) string {
	return fmt.Sprintf("%s (%d)", e.Name(), e.Size())
}
visitor.go
package main

// ---- visitor層(Java版 Visitor.java 相当)----
//
// Java版は
//
//	public abstract void visit(File file);
//	public abstract void visit(Directory directory);
//
// という同名メソッドのオーバーロードで二重ディスパッチを表現する。Goのinterfaceは
// 引数型によるオーバーロードを持てないため、VisitFile/VisitDirectoryという
// 別名メソッドに分けている。
//
// それでも二重ディスパッチという骨格自体はJava版と変わらない。
//   - File.Accept(v)/Directory.Accept(v) の呼び出しは、Entryの具象型(File or Directory)
//     によって動的に決まる(=1段目のディスパッチ)。
//   - その中で呼ぶ v.VisitFile(this)/v.VisitDirectory(this) は、Visitorの具象型
//     (ListVisitor等)によって動的に決まる(=2段目のディスパッチ)。
//
// 「どのメソッド名を呼ぶか」をJavaはコンパイル時のオーバーロード解決(静的型)に、
// Goはメソッド名そのものの選び分け(Accept側で書き分け)に委ねている、という違いだけで、
// 「要素の型 × Visitorの型」の組み合わせで処理が決まる点は同じ。
type Visitor interface {
	VisitFile(file *File)
	VisitDirectory(directory *Directory)
}
file.go
package main

// ---- file層(Java版の File.java 相当)----

// File はEntryのConcreteComponent(Leaf = 子を持てない末端)。
type File struct {
	name string
	size int
}

// NewFile はコンストラクタ相当。
func NewFile(name string, size int) *File {
	return &File{name: name, size: size}
}

func (f *File) Name() string { return f.name }
func (f *File) Size() int    { return f.size }

// Add はJava版で「Fileはadd()をオーバーライドしない」=Entryのデフォルト実装
// (常に例外)をそのまま使う、を明示的に書き下したもの。
// Goには継承のデフォルトメソッドが無いので、「Fileにaddしたら常にエラー」を
// ここに書くしかない。
func (f *File) Add(entry Entry) (Entry, error) {
	return nil, ErrFileTreatment
}

// Accept はJava版 File.accept(Visitor v) 相当。
// 「自分はFileである」という具象型の情報をもとに v.VisitFile(this) を呼ぶ
// (=二重ディスパッチの1段目)。
func (f *File) Accept(v Visitor) {
	v.VisitFile(f)
}

// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (f *File) String() string {
	return entryString(f)
}
directory.go
package main

// ---- directory層(Java版の Directory.java 相当)----

// Directory はEntryのConcreteComponent(Composite = 子Entryの集合を持てる)。
type Directory struct {
	name    string
	entries []Entry
}

// NewDirectory はコンストラクタ相当。
func NewDirectory(name string) *Directory {
	return &Directory{name: name}
}

func (d *Directory) Name() string { return d.name }

// Size は自分が抱える全エントリのサイズ合計(再帰的に集計される)。
func (d *Directory) Size() int {
	total := 0
	for _, e := range d.entries {
		total += e.Size()
	}
	return total
}

// Add はエントリの追加。Java版と同様、追加後は自分自身を返す。
func (d *Directory) Add(entry Entry) (Entry, error) {
	d.entries = append(d.entries, entry)
	return d, nil
}

// Entries はJava版 Directory.iterator() 相当。ListVisitorが子エントリを再帰的に
// 巡回するために公開している。
func (d *Directory) Entries() []Entry {
	return d.entries
}

// Accept はJava版 Directory.accept(Visitor v) 相当。
// 「自分はDirectoryである」という具象型の情報をもとに v.VisitDirectory(this) を呼ぶ
// (=二重ディスパッチの1段目)。
func (d *Directory) Accept(v Visitor) {
	v.VisitDirectory(d)
}

// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (d *Directory) String() string {
	return entryString(d)
}
list_visitor.go
package main

import "fmt"

// ---- ListVisitor(Java版 ListVisitor.java 相当)----
//
// ConcreteVisitor。ファイルツリーを巡回しながら全エントリを一覧表示する。

// ListVisitor はJava版 ListVisitor(Visitorを継承)相当。
type ListVisitor struct {
	currentdir string // 現在注目しているディレクトリ名
}

// NewListVisitor はコンストラクタ相当(Java版はフィールド初期値""のデフォルトコンストラクタ)。
func NewListVisitor() *ListVisitor {
	return &ListVisitor{}
}

// VisitFile はファイルを訪問したときに呼ばれる。
func (lv *ListVisitor) VisitFile(file *File) {
	fmt.Println(lv.currentdir + "/" + file.String())
}

// VisitDirectory はディレクトリを訪問したときに呼ばれる。
// 自分自身を表示したあと、currentdirを一段掘り下げて、子エントリそれぞれに
// Accept(lv)させて再帰的に巡回する(Java版と同じ「訪問者が構造を歩く」流れ)。
func (lv *ListVisitor) VisitDirectory(directory *Directory) {
	fmt.Println(lv.currentdir + "/" + directory.String())
	savedir := lv.currentdir
	lv.currentdir = lv.currentdir + "/" + directory.Name()
	for _, entry := range directory.Entries() {
		entry.Accept(lv)
	}
	lv.currentdir = savedir
}
main.go
package main

import "fmt"

// 実行: go run ./GoF/patterns/Visitor/go
//
// Java版Main.javaと同じツリー(root/bin,tmp,usr以下)を組み立てて、ListVisitorに
// Accept()させて2回一覧表示する(Composite/go版のPrintList()呼び出しがAccept(v)に
// 置き換わっている点だけが違う)。
// 末尾に「Fileにadd()するとエラーになる」ことの確認を追加している
// (Composite/go版と同じ理由。Java版はtry/catchで全体を囲むだけで、実際には
// サンプル中で例外を踏まない)。
func main() {
	fmt.Println("Making root entries...")
	rootdir := NewDirectory("root")
	bindir := NewDirectory("bin")
	tmpdir := NewDirectory("tmp")
	usrdir := NewDirectory("usr")
	mustAdd(rootdir, bindir)
	mustAdd(rootdir, tmpdir)
	mustAdd(rootdir, usrdir)
	vi := NewFile("vi", 10000)
	mustAdd(bindir, vi)
	mustAdd(bindir, NewFile("latex", 20000))
	rootdir.Accept(NewListVisitor())

	fmt.Println()
	fmt.Println("Making user entries...")
	yuki := NewDirectory("yuki")
	hanako := NewDirectory("hanako")
	tomura := NewDirectory("tomura")
	mustAdd(usrdir, yuki)
	mustAdd(usrdir, hanako)
	mustAdd(usrdir, tomura)
	mustAdd(yuki, NewFile("diary.html", 100))
	mustAdd(yuki, NewFile("Composite.java", 200))
	mustAdd(hanako, NewFile("memo.tex", 300))
	mustAdd(tomura, NewFile("game.doc", 400))
	mustAdd(tomura, NewFile("junk.mail", 500))
	rootdir.Accept(NewListVisitor())

	fmt.Println()
	fmt.Println("Trying to add a file under a file (should fail)...")
	if _, err := vi.Add(NewFile("evil", 1)); err != nil {
		fmt.Println("error:", err)
	}
}

// mustAdd はサンプルツリーの組み立て専用ヘルパー。ここで呼ぶAddは全部Directoryへの
// 追加なので本来失敗しないが、Entry.Add()が返すエラーを黙って握りつぶさず一応チェックする。
func mustAdd(parent Entry, child Entry) {
	if _, err := parent.Add(child); err != nil {
		panic(err)
	}
}
PHP
index.php
<?php

/**
 * データ構造と処理を分けて保守性を高める
 * とはいえPHPでは型もオーバーロードもないので
 * 保守性云々の議論は難しい。
 * しかし一つのデータ構造に対する
 * 処理が複数あるときには効果大
 * 「ORマッパーのカスケード処理」
 * 「退会による連鎖削除」
 * 「アカウント凍結」
 */



/**
 * データ構造とアルゴリズムを分離することで
 * 保守性を高める
 * 様々なデータ構造を一度に処理するとき、
 * その一連の処理を一つのVisitorに実装すれば
 * 単にトラバース(横断)するだけで
 * 適切な処理へとディスパッチできる
 * 
 */


/**
 * 訪問者
 */


interface Visitor
{
  public function visit($acceptor);
}


/***
 * 訪問者を受け入れるメソッドだけ用意する
 */

interface Element
{
  public function accept($visitor);
}


/**
 * 具体的な訪問者のビジネスマンで
 * 色々な会社の色々な部署を訪問
 */


class BusinessMan  implements Visitor
{
  /**
   * phpではオーバーロードがないので
   * visit+acceptorのクラス名で呼ばれるメソッドが決定
   * 
   */

  public function visit($acceptor)
  {
    $method = "visit" . get_class($acceptor);
    $this->$method($acceptor);
  }

  /**
   * ラジオ局そのものに訪問したとき
   *
   * @param [type] $acceptor
   * @return void
   */
  public function visitRadioStation($acceptor)
  {
    print $acceptor . "を訪問して色々やります<br>";
    $acceptor->next->accept($this);
  }

  /**
   * ラジオ局のニュース番組部を訪問したときに呼ばれます
   */
  public function visitRadioStation_News($acceptor)
  {
    print $acceptor . "を訪問して色々やります<br>";
  }
}


/**
 * ラジオ局
 * visitorを受け入れる
 */

class RadioStation   implements Element
{
  public function __toString()
  {
    return "ラジオ局";
  }

  /**
   * 訪問者を受け入れる
   */

  public function accept($visitor)
  {
    $visitor->visit($this);
  }
}



/**
 * ラジオ局のニュース番組ぶ
 */




class RadioStation_News  implements Element
{

  public function __toString()
  {
    return "ラジオ局のニュース番組担当";
  }
  public function accept($visitor)
  {
    $visitor->visit($this);
  }
}


$radio = new RadioStation();
$radio->next = new RadioStation_News();
$radio->accept(new BusinessMan());
TypeScript

Go版と同じ理由(TypeScript/JavaScriptのメソッドは実行時に引数型でオーバーロード解決されない)で、VisitorインタフェースはvisitFile/visitDirectoryという別名メソッドに分けている。それ以外はComposite/typescript版のabstract class Entryをベースにaccept()を追加した素直な移植。

実行: npx tsx GoF/patterns/Visitor/typescript/main.ts

$ npx tsx GoF/patterns/Visitor/typescript/main.ts
entry.ts
// Visitorパターン: エントリ層 (Java版の Entry.java / Element.java / FileTreatmentException.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Composite/typescript版のEntryをベースに、Visitorパターンの主役であるaccept()
// (Java版Element.accept(Visitor v)相当)を追加した版。Composite版にあった
// printList()/printListRec()は、Visitor版のJava Entry.java(Visitor/Sample/Entry.java)には
// 存在しない(一覧表示の責務がEntry自身からListVisitorへ完全に移ったため)ので、
// ここでも同様に削除している。

import { Visitor } from "./visitor";

// FileTreatmentError はJava版 FileTreatmentException 相当。
export class FileTreatmentError extends Error {
  constructor(message = "this entry cannot contain other entries") {
    super(message);
    this.name = "FileTreatmentError";
  }
}

export abstract class Entry {
  abstract getName(): string;
  abstract getSize(): number;

  // Java版Entry.add()のデフォルト実装(常に例外)相当。Directoryだけがoverrideする。
  add(_entry: Entry): Entry {
    throw new FileTreatmentError();
  }

  // Java版 Element.accept(Visitor v) 相当。二重ディスパッチの起点。
  abstract accept(visitor: Visitor): void;

  // Java版 toString() 相当。
  toString(): string {
    return `${this.getName()} (${this.getSize()})`;
  }
}
visitor.ts
// Visitorパターン: visitor層 (Java版 Visitor.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Java版は
//
//   public abstract void visit(File file);
//   public abstract void visit(Directory directory);
//
// という同名メソッドのオーバーロードで二重ディスパッチを表現する。TypeScript(JavaScript)の
// メソッドは実行時に引数型でオーバーロード解決されない(単一の実装関数にせざるを得ない)ため、
// Go版と同じ方針でvisitFile/visitDirectoryという別名メソッドに分けている。
//
// それでも二重ディスパッチという骨格自体はJava版と変わらない。
//   - File.accept(v)/Directory.accept(v) の呼び出しは、Entryの具象型(File or Directory)
//     によって動的に決まる(=1段目のディスパッチ)。
//   - その中で呼ぶ v.visitFile(this)/v.visitDirectory(this) は、Visitorの具象型
//     (ListVisitor等)によって動的に決まる(=2段目のディスパッチ)。

import { Directory } from "./directory";
import { File } from "./file";

export interface Visitor {
  visitFile(file: File): void;
  visitDirectory(directory: Directory): void;
}
file.ts
// File層 (Java版の File.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。

import { Entry } from "./entry";
import { Visitor } from "./visitor";

// File: EntryのConcreteComponent(Leaf = 子を持てない末端)。
export class File extends Entry {
  constructor(
    private readonly name: string,
    private readonly size: number,
  ) {
    super();
  }

  getName(): string {
    return this.name;
  }

  getSize(): number {
    return this.size;
  }

  // add()はoverrideしない = Entryのデフォルト実装(常に例外)をそのまま使う。
  // Java版で「Fileがadd()をオーバーライドしていない」のと同じ意図。

  // Java版 File.accept(Visitor v) 相当。
  // 「自分はFileである」という具象型の情報をもとに visitor.visitFile(this) を呼ぶ
  // (=二重ディスパッチの1段目)。
  accept(visitor: Visitor): void {
    visitor.visitFile(this);
  }
}
directory.ts
// Directory層 (Java版の Directory.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。

import { Entry } from "./entry";
import { Visitor } from "./visitor";

// Directory: EntryのConcreteComponent(Composite = 子Entryの集合を持てる)。
export class Directory extends Entry {
  private readonly entries: Entry[] = [];

  constructor(private readonly name: string) {
    super();
  }

  getName(): string {
    return this.name;
  }

  // 自分が抱える全エントリのサイズ合計(再帰的に集計される)。
  getSize(): number {
    return this.entries.reduce((total, entry) => total + entry.getSize(), 0);
  }

  // エントリの追加。Java版と同様、追加後は自分自身を返す。
  add(entry: Entry): Entry {
    this.entries.push(entry);
    return this;
  }

  // Java版 Directory.iterator() 相当。ListVisitorが子エントリを再帰的に
  // 巡回するために公開している。
  getEntries(): readonly Entry[] {
    return this.entries;
  }

  // Java版 Directory.accept(Visitor v) 相当。
  // 「自分はDirectoryである」という具象型の情報をもとに visitor.visitDirectory(this) を呼ぶ
  // (=二重ディスパッチの1段目)。
  accept(visitor: Visitor): void {
    visitor.visitDirectory(this);
  }
}
list_visitor.ts
// ListVisitor層 (Java版 ListVisitor.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// ConcreteVisitor。ファイルツリーを巡回しながら全エントリを一覧表示する。

import { Directory } from "./directory";
import { File } from "./file";
import { Visitor } from "./visitor";

export class ListVisitor implements Visitor {
  // 現在注目しているディレクトリ名。Java版のフィールド初期値""をそのまま踏襲。
  private currentdir = "";

  // ファイルを訪問したときに呼ばれる。
  visitFile(file: File): void {
    console.log(`${this.currentdir}/${file}`);
  }

  // ディレクトリを訪問したときに呼ばれる。
  // 自分自身を表示したあと、currentdirを一段掘り下げて、子エントリそれぞれに
  // accept(this)させて再帰的に巡回する(Java版と同じ「訪問者が構造を歩く」流れ)。
  visitDirectory(directory: Directory): void {
    console.log(`${this.currentdir}/${directory}`);
    const savedir = this.currentdir;
    this.currentdir = `${this.currentdir}/${directory.getName()}`;
    for (const entry of directory.getEntries()) {
      entry.accept(this);
    }
    this.currentdir = savedir;
  }
}
main.ts
// Visitorパターン: ファイルシステムのツリー構造を巡回・一覧表示する (Java版Main.javaと同じお題)
//
// 実行: npx tsx GoF/patterns/Visitor/typescript/main.ts
//
// Java版と同じツリー(root/bin,tmp,usr以下)を組み立てて、ListVisitorにaccept()させて
// 2回一覧表示する(Composite/typescript版のprintList()呼び出しがaccept(visitor)に
// 置き換わっている点だけが違う)。末尾に「Fileにadd()するとエラーになる」ことの確認を
// 追加している(Java版はtry/catchで全体を囲むだけで、実際にはサンプル中で例外を踏まない)。

import { Directory } from "./directory";
import { FileTreatmentError } from "./entry";
import { File } from "./file";
import { ListVisitor } from "./list_visitor";

function main(): void {
  console.log("Making root entries...");
  const rootdir = new Directory("root");
  const bindir = new Directory("bin");
  const tmpdir = new Directory("tmp");
  const usrdir = new Directory("usr");
  rootdir.add(bindir);
  rootdir.add(tmpdir);
  rootdir.add(usrdir);
  const vi = new File("vi", 10000);
  bindir.add(vi);
  bindir.add(new File("latex", 20000));
  rootdir.accept(new ListVisitor());

  console.log("");
  console.log("Making user entries...");
  const yuki = new Directory("yuki");
  const hanako = new Directory("hanako");
  const tomura = new Directory("tomura");
  usrdir.add(yuki);
  usrdir.add(hanako);
  usrdir.add(tomura);
  yuki.add(new File("diary.html", 100));
  yuki.add(new File("Composite.java", 200));
  hanako.add(new File("memo.tex", 300));
  tomura.add(new File("game.doc", 400));
  tomura.add(new File("junk.mail", 500));
  rootdir.accept(new ListVisitor());

  console.log("");
  console.log("Trying to add a file under a file (should fail)...");
  try {
    vi.add(new File("evil", 1));
  } catch (e) {
    if (e instanceof FileTreatmentError) {
      console.log(`error: ${e.message}`);
    } else {
      throw e;
    }
  }
}

main();
Python

同様にVisitor(ABC)visit_file/visit_directoryという別名メソッドに分けている(Pythonも同名メソッドのオーバーロードは持てないため)。Composite/python版のEntry(ABC)をベースにaccept()を追加。

実行: python3 GoF/patterns/Visitor/python/main.py

$ python3 GoF/patterns/Visitor/python/main.py
entry.py
"""Visitorパターン: エントリ層 (Java版の Entry.java / Element.java / FileTreatmentException.java 相当)

単体では実行不可。エントリポイントは main.py (python3 main.py)。

Composite/python版のEntryをベースに、Visitorパターンの主役であるaccept()
(Java版Element.accept(Visitor v)相当)を追加した版。Composite版にあった
_print_list_rec()は、Visitor版のJava Entry.java(Visitor/Sample/Entry.java)には
存在しない(一覧表示の責務がEntry自身からListVisitorへ完全に移ったため)ので、
ここでも同様に削除している。
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from visitor import Visitor


class FileTreatmentError(Exception):
    """Java版 FileTreatmentException 相当。"""

    def __init__(self, message: str = "this entry cannot contain other entries") -> None:
        super().__init__(message)


class Entry(ABC):
    @abstractmethod
    def get_name(self) -> str: ...

    @abstractmethod
    def get_size(self) -> int: ...

    def add(self, entry: Entry) -> Entry:
        """Java版Entry.add()のデフォルト実装(常に例外)相当。Directoryだけがoverrideする。"""
        raise FileTreatmentError()

    @abstractmethod
    def accept(self, visitor: Visitor) -> None:
        """Java版 Element.accept(Visitor v) 相当。二重ディスパッチの起点。"""
        ...

    def __str__(self) -> str:
        """Java版 toString() 相当。"""
        return f"{self.get_name()} ({self.get_size()})"
visitor.py
"""Visitorパターン: visitor層 (Java版 Visitor.java 相当)

単体では実行不可。エントリポイントは main.py (python3 main.py)。

Java版は

    public abstract void visit(File file);
    public abstract void visit(Directory directory);

という同名メソッドのオーバーロードで二重ディスパッチを表現する。Pythonのメソッドは
引数型でオーバーロード解決されない(同名メソッドは1つしか定義できない)ため、
Go/TypeScript版と同じ方針でvisit_file/visit_directoryという別名メソッドに分けている。

それでも二重ディスパッチという骨格自体はJava版と変わらない。
  - File.accept(v)/Directory.accept(v) の呼び出しは、Entryの具象型(File or Directory)
    によって動的に決まる(=1段目のディスパッチ)。
  - その中で呼ぶ v.visit_file(self)/v.visit_directory(self) は、Visitorの具象型
    (ListVisitor等)によって動的に決まる(=2段目のディスパッチ)。
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from directory import Directory
    from file import File


class Visitor(ABC):
    @abstractmethod
    def visit_file(self, file: File) -> None: ...

    @abstractmethod
    def visit_directory(self, directory: Directory) -> None: ...
file.py
"""File層 (Java版の File.java 相当)

単体では実行不可。エントリポイントは main.py (python3 main.py)。
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from entry import Entry

if TYPE_CHECKING:
    from visitor import Visitor


class File(Entry):
    """File: EntryのConcreteComponent(Leaf = 子を持てない末端)。"""

    def __init__(self, name: str, size: int) -> None:
        self._name = name
        self._size = size

    def get_name(self) -> str:
        return self._name

    def get_size(self) -> int:
        return self._size

    # add()はoverrideしない = Entryのデフォルト実装(常に例外)をそのまま使う。
    # Java版で「Fileがadd()をオーバーライドしていない」のと同じ意図。

    def accept(self, visitor: Visitor) -> None:
        """Java版 File.accept(Visitor v) 相当。
        「自分はFileである」という具象型の情報をもとに visitor.visit_file(self) を呼ぶ
        (=二重ディスパッチの1段目)。
        """
        visitor.visit_file(self)
directory.py
"""Directory層 (Java版の Directory.java 相当)

単体では実行不可。エントリポイントは main.py (python3 main.py)。
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from entry import Entry

if TYPE_CHECKING:
    from visitor import Visitor


class Directory(Entry):
    """Directory: EntryのConcreteComponent(Composite = 子Entryの集合を持てる)。"""

    def __init__(self, name: str) -> None:
        self._name = name
        self._entries: list[Entry] = []

    def get_name(self) -> str:
        return self._name

    def get_size(self) -> int:
        """自分が抱える全エントリのサイズ合計(再帰的に集計される)。"""
        return sum(entry.get_size() for entry in self._entries)

    def add(self, entry: Entry) -> Entry:
        """エントリの追加。Java版と同様、追加後は自分自身を返す。"""
        self._entries.append(entry)
        return self

    def get_entries(self) -> list[Entry]:
        """Java版 Directory.iterator() 相当。ListVisitorが子エントリを再帰的に
        巡回するために公開している。
        """
        return self._entries

    def accept(self, visitor: Visitor) -> None:
        """Java版 Directory.accept(Visitor v) 相当。
        「自分はDirectoryである」という具象型の情報をもとに visitor.visit_directory(self) を呼ぶ
        (=二重ディスパッチの1段目)。
        """
        visitor.visit_directory(self)
list_visitor.py
"""ListVisitor層 (Java版 ListVisitor.java 相当)

単体では実行不可。エントリポイントは main.py (python3 main.py)。

ConcreteVisitor。ファイルツリーを巡回しながら全エントリを一覧表示する。
"""

from __future__ import annotations

from directory import Directory
from file import File
from visitor import Visitor


class ListVisitor(Visitor):
    def __init__(self) -> None:
        self._currentdir = ""  # 現在注目しているディレクトリ名

    def visit_file(self, file: File) -> None:
        """ファイルを訪問したときに呼ばれる。"""
        print(f"{self._currentdir}/{file}")

    def visit_directory(self, directory: Directory) -> None:
        """ディレクトリを訪問したときに呼ばれる。

        自分自身を表示したあと、_currentdirを一段掘り下げて、子エントリそれぞれに
        accept(self)させて再帰的に巡回する(Java版と同じ「訪問者が構造を歩く」流れ)。
        """
        print(f"{self._currentdir}/{directory}")
        savedir = self._currentdir
        self._currentdir = f"{self._currentdir}/{directory.get_name()}"
        for entry in directory.get_entries():
            entry.accept(self)
        self._currentdir = savedir
main.py
"""Visitorパターン: ファイルシステムのツリー構造を巡回・一覧表示する (Java版Main.javaと同じお題)

実行: python3 main.py
      (もしくはリポジトリルートから python3 GoF/patterns/Visitor/python/main.py)

Java版と同じツリー(root/bin,tmp,usr以下)を組み立てて、ListVisitorにaccept()させて
2回一覧表示する(Composite/python版のprint_list()呼び出しがaccept(visitor)に
置き換わっている点だけが違う)。末尾に「Fileにadd()するとエラーになる」ことの確認を
追加している(Java版はtry/exceptで全体を囲むだけで、実際にはサンプル中で例外を踏まない)。
"""

from __future__ import annotations

from directory import Directory
from entry import FileTreatmentError
from file import File
from list_visitor import ListVisitor


def main() -> None:
    print("Making root entries...")
    rootdir = Directory("root")
    bindir = Directory("bin")
    tmpdir = Directory("tmp")
    usrdir = Directory("usr")
    rootdir.add(bindir)
    rootdir.add(tmpdir)
    rootdir.add(usrdir)
    vi = File("vi", 10000)
    bindir.add(vi)
    bindir.add(File("latex", 20000))
    rootdir.accept(ListVisitor())

    print()
    print("Making user entries...")
    yuki = Directory("yuki")
    hanako = Directory("hanako")
    tomura = Directory("tomura")
    usrdir.add(yuki)
    usrdir.add(hanako)
    usrdir.add(tomura)
    yuki.add(File("diary.html", 100))
    yuki.add(File("Composite.java", 200))
    hanako.add(File("memo.tex", 300))
    tomura.add(File("game.doc", 400))
    tomura.add(File("junk.mail", 500))
    rootdir.accept(ListVisitor())

    print()
    print("Trying to add a file under a file (should fail)...")
    try:
        vi.add(File("evil", 1))
    except FileTreatmentError as e:
        print(f"error: {e}")


if __name__ == "__main__":
    main()