3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Pythonで、デザインパターン「Visitor」を学ぶ

Last updated at Posted at 2020-02-02

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。

■ Visitorパターン(ビジター・パターン)

Visitorパターンは、オブジェクト指向プログラミング およびソフトウェア工学 において、 アルゴリズムをオブジェクトの構造から分離するためのデザインパターンである。分離による実用的な結果として、既存のオブジェクトに対する新たな操作を構造を変更せずに追加することができる。
基本的には Visitorパターンは一群のクラスに対して新たな仮想関数をクラス自体を変更せずに追加できるようにする。そのために、全ての仮想関数を適切に特化させた Visitor クラスを作成する。Visitorはインスタンスへの参照を入力として受け取り、ダブルディスパッチを用いて目的を達する。
Visitor は強力であるが、既存の仮想関数と比較して制限もある。各クラス内に小さなコールバックメソッドを追加する必要があり、各クラスのコールバックメソッドは新たなサブクラスで継承することができない。

UML class and sequence diagram

W3sDesign_Visitor_Design_Pattern_UML.jpg

UML class diagram

Visitor_design_pattern.svg.png

□ 備忘録

書籍「増補改訂版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クラスが、この役を努めます。

visitor/visitor.py
from abc import ABCMeta, abstractmethod

class Vistor(metaclass=ABCMeta):
    @abstractmethod
    def visit(self, directory):
        pass

(2) ConcreteVisitor(具体的訪問者)の役

ConcreteVisitor役は、Visitor役のインタフェースを実装します。visitor(xxxx)という形のメソッドを実装し、個々のConcreteElement役ごとの処理を記述します。
サンプルプログラムでは、ListVistorクラスが、この役を努めます。

visitor/visitor.py
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クラスが、この役を努めます。

visitor/element.py
from abc import ABCMeta, abstractmethod

class Element(metaclass=ABCMeta):
    @abstractmethod
    def accept(self, v):
        pass

(4) ConcreteElement(具体的要素)の役

ConcreteElement役は、Element役のインタフェースを実装する役です。
サンプルプログラムでは、Entryクラス, FileクラスとDirectoryクラスが、この役を努めます。

visitor/element.py
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メソッドが、この役を努めます。

Main.py
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) その他

例外クラスを追加します

visitor/element.py
class FileTreatmentException(Exception):
    def __init__(self,*args,**kwargs):
        self.message = "FileTreatmentException"

■ 参考URL

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?