Visitor
構造を渡り歩きながら仕事をする
概要
データ構造と処理を分離
マンガでわかる Visitor
マンガでわかる Visitor #デザインパターン - Qiita
でざぱたんで覚える Visitor
ちびキャラは「ビジターたん」。ファサード商会のトップ訪問販売員で、客は家から動かず、彼女の方が一軒ずつ訪ねて相手ごとに違う業務をこなす。データ構造(客)は据え置き、処理(営業)の方を訪問者として持ち回らせる——同じ巡回で実施内容だけが異なるケースに効く、というVisitorの分離構造を訪問販売で覚える。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.135〜)
登場人物
- Visitor: ConcreteElementごと訪問するvisitメソッドを宣言するインタフェース
- ConcreteVisitor: Visitorを実装
- Element:Visitorの訪問先インタフェース
- Visitorを引数にもち受け入れるacceptメソッドを持つ
- ConcreteElement:Elementを実装
- ObjectStructure: Elementの集合
クラス図

このサイトの実装(ファイル/ディレクトリの一覧表示)での対応関係:
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
public interface Element {
public abstract void accept(Visitor v);
}
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;
}
}
public abstract class Visitor {
public abstract void visit(File file);
public abstract void visit(Directory directory);
}
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);
}
}
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);
}
}
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();
}
}
}
public class FileTreatmentException extends RuntimeException {
public FileTreatmentException() {
}
public FileTreatmentException(String msg) {
super(msg);
}
}
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
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())
}
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)
}
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)
}
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)
}
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
}
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
<?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
// 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パターン: 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層 (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層 (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);
}
}
// 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;
}
}
// 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
"""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パターン: 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層 (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層 (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)
"""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
"""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()