GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。
■ Visitorパターン(ビジター・パターン)
Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。
UML class and sequence diagram
UML class diagram
□ 備忘録
書籍「増補改訂版Java言語で学ぶデザインパターン入門」の引用ですが、腹落ちしました。
Visitorとは、「訪問者」という意味です。データ構造の中にたくさんの要素が格納されており、その各要素に対して何らかの「処理」をしていくとしましょう。このとき、その「処理」のコードはどこに書くべきでしょうか?普通に考えれば、データ構造を表しているクラスの中に書きますね。でも、もし、その「処理」が一種類とは限らなかったらどうでしょう。その場合、新しい処理が必要になるたびに、データ構造のクラスを修正しなければならなくなります。
Visitor
パターンでは、データ構造と処理を分離します。そして、データ構造の中をめぐり歩く主体である「訪問者」を表すクラスを用意し、そのクラスに処理をまかせます。すると、新しい処理を追加したいときには新しい「訪問者」を作ればよいことになります。そして、データ構造の方は、戸を叩いてくる「訪問者」を受け入れてあげればよいのです。
■ "Visitor"のサンプルプログラム
実際に、Visitorパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ちなみに、Qiita記事「Pythonで、デザインパターン「Composite」を学ぶ」でのサンプルプログラムと挙動が同じになるので、実装を比較してみるとVisitor
パターンの理解がより深まります。
- ルートエントリの
ディレクトリ
に、サブディレクトリ
およびファイル
を追加してみる - ルートエントリの
ディレクトリ
に、ユーザエントリのディレクトリ
を追加して、さらに、サブディレクトリ
およびファイル
を追加してみる - 敢えて、
ファイル
に、ディレクトリ
を追加して、失敗することを確認する
$ python Main.py
Making root entries
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making user entries...
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/composite.py (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)
Occurring Exception...
FileTreatmentException
■ サンプルプログラムの詳細
Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Visitor
- ディレクトリ構成
.
├── Main.py
└── visitor
├── __init__.py
├── element.py
└── visitor.py
(1) Visitor(訪問者)の役
Visitor
役は、データ構造の具体的な要素(ConcreteElement
役)ごとに、「xxxxを訪問した」というvisit(xxxx)メソッドを宣言します。visit(xxxx)はxxxxを処理するためのメソッドです。実際のコードはConcreteVisitor
役の側に書かれます。
サンプルプログラムでは、Visitor
クラスが、この役を努めます。
from abc import ABCMeta, abstractmethod
class Vistor(metaclass=ABCMeta):
@abstractmethod
def visit(self, directory):
pass
(2) ConcreteVisitor(具体的訪問者)の役
ConcreteVisitor
役は、Visitor
役のインタフェースを実装します。visitor(xxxx)という形のメソッドを実装し、個々のConcreteElement
役ごとの処理を記述します。
サンプルプログラムでは、ListVistor
クラスが、この役を努めます。
class ListVistor(Vistor):
def __init__(self):
self.__currentdir = ''
def visit(self, directory):
print("{0}/{1}".format(self.__currentdir, directory))
if isinstance(directory, Directory):
savedir = self.__currentdir
self.__currentdir = self.__currentdir + '/' + directory.getName()
for f in directory:
f.accept(self)
self.__currentdir = savedir
(3) Element(要素)の役
Element
役は、Visitor
役の訪問先を表す役です。訪問先を受け入れるaccept
メソッドを宣言します。accept
メソッドの引数にはVisitor
役が渡されます。
サンプルプログラムでは、Element
クラスが、この役を努めます。
from abc import ABCMeta, abstractmethod
class Element(metaclass=ABCMeta):
@abstractmethod
def accept(self, v):
pass
(4) ConcreteElement(具体的要素)の役
ConcreteElement
役は、Element
役のインタフェースを実装する役です。
サンプルプログラムでは、Entry
クラス, File
クラスとDirectory
クラスが、この役を努めます。
class Entry(Element):
@abstractmethod
def getName(self):
pass
@abstractmethod
def getSize(self):
pass
def add(self, entry):
raise FileTreatmentException
def __str__(self):
return "{0} ({1})".format(self.getName(), self.getSize())
class File(Entry):
def __init__(self, name, size):
self.__name = name
self.__size = size
def getName(self):
return self.__name
def getSize(self):
return self.__size
def accept(self, v):
v.visit(self)
class Directory(Entry):
def __init__(self, name):
self.__name = name
self.__dir = []
def getName(self):
return self.__name
def getSize(self):
size = 0
for f in self.__dir:
size += f.getSize()
return size
def add(self, entry):
self.__dir.append(entry)
return self
def __iter__(self):
self.__index = 0
return self
def __next__(self):
if self.__index >= len(self.__dir):
raise StopIteration()
dir = self.__dir[self.__index]
self.__index += 1
return dir
def accept(self, v):
v.visit(self)
(5) ObjectStructure(オブジェクトの構造)の役
ObjectStructure
役は、Element
役の集合を扱う役です。ConcreteVisitor
役が個々のElement
役を扱えるようなメソッドを備えています。
サンプルプログラムでは、Directory
クラスがこの役を努めます。(一人二役です)
ConcreteVisitor
役が個々のElement
役を扱えるように、サンプルプログラムのDirectory
クラスには、iterator
が用意されています。
(6) Client(依頼人)の役
サンプルプログラムでは、startMain
メソッドが、この役を努めます。
from visitor.visitor import ListVistor
from visitor.element import File, Directory, FileTreatmentException
def startMain():
try:
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)
bindir.add(File("vi", 10000))
bindir.add(File("latex", 20000))
rootdir.accept(ListVistor())
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.py", 200))
hanako.add(File("memo.tex", 300))
tomura.add(File("game.doc", 400))
tomura.add(File("junk.mail", 500))
rootdir.accept(ListVistor())
print("")
print("Occurring Exception...")
tmpfile = File("tmp.txt", 100)
bindir = Directory("bin")
tmpfile.add(bindir)
except FileTreatmentException as ex:
print(ex.message)
if __name__ == '__main__':
startMain()
(7) その他
例外クラスを追加します
class FileTreatmentException(Exception):
def __init__(self,*args,**kwargs):
self.message = "FileTreatmentException"