はじめに
本エントリーは某社内で実施するデザインパターン勉強会向けの資料となります。
本エントリーで書籍「Java言語で学ぶデザインパターン入門」をベースに学習を進めますが、サンプルコードはC#に置き換えて解説します。
第1回:Iteratorパターン
第2回:Adapterパターン
第3回:Template Methodパターン
第4回:Factory Methodパターン
第5回:Singletonパターン
第6回:Prototypeパターン
第7回:Builderパターン
第8回:Abstract Factoryパターン
第9回:Bridgeパターン
第10回:Strategyパターン
第11回:Compositeパターン
第12回:Decoratorパターン
Visitorパターンとは
データ構造の中にたくさんの要素が格納され、その各要素に対して何らかの「処理」をしていくとします。
Visitorパターンでは、データ構造と処理を分離し、あるクラスに処理を任せます。処理を行う新しいクラスを作成することで、データ構造のクラスには手を加えずに、必要に応じて処理の追加ができます。
サンプルプログラムのクラス図
ここでは、ファイルの一覧を表示するサンプルプログラムを紹介します。
各クラスの役割
| クラス名 | 役割 | 
|---|---|
| Visitor | ファイルやディレクトリを訪れる訪問者を表す抽象クラス | 
| Element | Visitorクラスのインスタンスを受け入れるデータ構造を表すインターフェース | 
| ListVisitor | Visitorクラスのサブクラスで、ファイルやディレクトリの一覧を表示するクラス | 
| Entry | FileとDirectoryのスーパークラスとなる抽象クラスで、インターフェースElementの実装を行う | 
| File | ファイルを表すクラス | 
| Directory | ディレクトリを表すクラス | 
| FileTreatmentException | Fileに対してaddしたときに発生する例外クラス | 
| Program | 動作テスト用のクラス | 
Visitorクラス
「訪問者」を表す抽象クラスで、訪問先のデータ構造(FileとDirectory)に依存しています。
Visit(File file)は、Fileを訪問した時にFileクラスが呼び出すメソッドです。
Visit(Directory directory)は、Directoryを訪問した時にDirectoryクラスが呼び出すメソッドです。
namespace VisitorPattern
{
    public abstract class Visitor
    {
        public abstract void Visit(File file);
        public abstract void Visit(Directory directory);
    }
}
Elementインターフェース
Visitorクラスが「訪問者」を表すのに対し、Elementインターフェースは訪問者を受け入れるインターフェースです。
namespace VisitorPattern
{
    public interface Element
    {
        void Accept(Visitor v);
    }
}
Entryクラス
Elementインターフェースを実装するクラスです。
各メソッドの定義は以下の通りです。
| メソッド名 | 定義 | 
|---|---|
| GetName | ディレクトリとファイルをまとめた「ディレクトリエントリ」の名前を得るための抽象メソッド | 
| GetSize | ディレクトリエントリのサイズを得るための抽象メソッド | 
| Add | Directoryメソッドだけで有効のため、エラーとする | 
| Enumerator | Directoryメソッドだけで有効のため、エラーとする | 
| ToString | ファイル名とサイズを並べて文字列表現を定義する | 
| Accept | 抽象メソッドっであり、実装はサブクラスであるFileクラスやDierectoryメソッドで行う | 
namespace VisitorPattern
{
    public abstract class Entry : Element
    {
        public abstract string GetName();
        public abstract int GetSize();
        public Entry Add(Entry entry)
        {
            throw new FileTreatmentException();
        }
        public IEnumerator Enumerator()
        {
            throw new FileTreatmentException();
        }
        public override string ToString()
        {
            return GetName() + " (" + GetSize() + ")";
        }
        public abstract void Accept(Visitor v);
    }
}
FIleクラス
Entryクラスのサブクラスであり、ファイルを表すクラスです。
Acceptメソッドの中からVisitorクラスのVisitメソッドを呼び出します。
Visitメソッドはオーバーロードされていますが、ここではVisit(File)メソッドが呼び出されます。
Visitメソッドを呼び出すことで、訪問者Visitorに対し訪問したファイルのインスタンスを教えています。
namespace VisitorPattern
{
    public class File :Entry
    {
        private string name;
        private int size;
        public File(string name,int size)
        {
            this.name = name;
            this.size = size;
        }
        public override string GetName()
        {
            return name;
        }
        public override int GetSize()
        {
            return size;
        }
        public override void Accept(Visitor v)
        {
            v.Visit(this);
        }
    } 
}
Directoryクラス
Entryクラスのサブクラスであり、ディレクトリを表すクラスです。
ディレクトリのサイズは、GetSizeメソッド内で動的に計算して求めています。ディレクトリの要素一つ一つを取り出して、そのサイズを合計したものが戻り値となります。
size += entry.GetSize(); では、変数sizeにentryのサイズを加えていますが、このentryはFileクラスまたはDirectoryクラスのインスタンスを示します。どちらにしても同じメソッドGetSizeを呼び出してサイズを取得しています。
この部分に関しては、将来Entryのサブクラスが新たに作成された場合でも、その中でGetSizeメソッドが実装されているため、修正は不要です。(Compositeパターンの特徴)
Addメソッドではエントリの追加を行い、GetEnumeratorメソッドではディレクトリに含まれているファイルやディレクトリの一覧を得るためのEnumeratorを返します。
AcceptメソッドではVisit(Directory)メソッドを呼び出し、訪問者に「訪問したのはこのDirectoryのインスタンスだ」と伝えています。
namespace VisitorPattern
{
    public class Directory:Entry
    {
        private string name;
        private ArrayList dir = new ArrayList();
    
        public Directory(string name)
        {
            this.name = name;
        }
        public override string GetName()
        {
            return name;
        }
        public override int GetSize()
        {
            int size = 0;
            IEnumerator it = dir.GetEnumerator();
            while (it.MoveNext())
            {
                Entry entry = (Entry)it.Current;
                size += entry.GetSize();
            }
            return size;
        }
         public new Entry Add(Entry entry)
        {
            dir.Add(entry);
            return this;
        }
        public IEnumerator GetEnumerator()
        {
            return dir.GetEnumerator();
        }
        public override void Accept(Visitor v)
        {
            v.Visit(this);
        }
    }
}
ListVisitorクラス
Visitorクラスのサブクラスで、データ構造の一覧を表示するためのクラスです。
currentdirフィールドは、現在注目しているディレクトリ名を保持しています。
Visit(File file)メソッドは、ファイルを訪問した時にFileクラスのAcceptメソッドの中から呼び出され、「Fileクラスのインスタンスに対して行うべき処理」を記述します。ここでは、現在のディレクトリ名の後にスラッシュ区切りでファイル名を表示しています。
Visit(Directory directory)メソッドは、ディレクトリを訪問した時にDirectoryクラスのAcceptメソッドの中から呼び出され、「Directoryクラスのインスタンスに対して行うべき処理」を記述します。ここでは、ディレクトリを表示した後に、ディレクトリ内にあるたくさんのディレクトリエントリ各々に対してAcceptメソッドを呼び出すことで、一つ一つを訪問しています。
VisitメソッドとAcceptメソッドが互いに相手を呼び出す構造も、Visitorパターンの特徴の一つです。
namespace VisitorPattern
{
    public class ListVisitor :Visitor
    {
        private string currentdir = "";
        public override void Visit(File file)
        {
            Console.WriteLine(currentdir + "/" + file);
        }
        public override void Visit (Directory directory)
        {
            Console.WriteLine(currentdir + "/" + directory);
            string savedir = currentdir;
            currentdir = currentdir + "/" + directory.GetName();
            IEnumerator it = directory.GetEnumerator();
            while (it.MoveNext())
            {
                Entry entry = (Entry)it.Current;
                entry.Accept(this);
            }
            currentdir = savedir;
        }
    }
}
ListVisitorクラス
Fileに対しAddメソッド、Enumeratorメソッドを呼び出したときに投げられる例外を定義したクラスです。
namespace VisitorPattern
{
    public class FileTreatmentException : Exception
    {
        public FileTreatmentException()
        {
        }
        public FileTreatmentException(string msg) : base(msg)
        {
            
        }
        
    }
}
Programクラス
ディレクトリ階層を作ります。
ディレクトリの内容を表示するのに、表示を行う訪問者であるListVisitorクラスのインスタンスを用いています。
namespace VisitorPattern
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("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());
                Console.WriteLine("");
                Console.WriteLine("Making user entries...");
                Directory aya = new Directory("aya");
                Directory hanako = new Directory("hanako");
                Directory taro = new Directory("taro");
                usrdir.Add(aya);
                usrdir.Add(hanako);
                usrdir.Add(taro);
                aya.Add(new File("diary.html", 100));
                aya.Add(new File("Composite.java", 200));
                hanako.Add(new File("memo.tex", 300));
                taro.Add(new File("game.doc", 400));
                taro.Add(new File("junk.mail", 500));
                rootdir.Accept(new ListVisitor());
            }
            catch(FileTreatmentException e)
            {
                e.ToString();
            }
        }
    }
}
実行結果
実行結果は以下の通りです。
Visitorパターンの特徴
- データ構造と処理とを分離することで、新たな処理が必要になった際でもデータ構造側は修正不要
- データ構造側と処理側のクラスが相互のメソッドを呼び出して動作する(ダブルディスパッチ)
サンプルコード
以下に公開しています。
https://github.com/ayayo/Visitor-Pattern

