DesignPattern

デザインパターン勉強会 第13回:Visitorパターン

More than 1 year has passed since last update.

はじめに

本エントリーは某社内で実施するデザインパターン勉強会向けの資料となります。

本エントリーで書籍「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();
            }
        }
    }
}

実行結果

実行結果は以下の通りです。

実行結果.PNG


Visitorパターンの特徴

  • データ構造と処理とを分離することで、新たな処理が必要になった際でもデータ構造側は修正不要
  • データ構造側と処理側のクラスが相互のメソッドを呼び出して動作する(ダブルディスパッチ)

サンプルコード

以下に公開しています。

https://github.com/ayayo/Visitor-Pattern