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


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

■ Composite(コンポジット・パターン)

Compositeパターン(コンポジット・パターン)とは、GoF (Gang of Four; 4人のギャングたち) によって定義された デザインパターンの1つである。「構造に関するパターン」に属する。Compositeパターンを用いるとディレクトリとファイルなどのような、木構造を伴う再帰的なデータ構造を表すことができる。
Composite パターンにおいて登場するオブジェクトは、「枝」と「葉」であり、これらは共通のインターフェースを実装している。そのため、枝と葉を同様に扱えるというメリットがある。

UML class and object diagram

UML class diagram


(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

書籍「増補改訂版Java言語で学ぶデザインパターン入門」の引用ですが、腹落ちしました。

ディレクトリの中には、ファイルが入っていたり、別のディレクトリ(サブディレクトリ)が入っていたりします。そしてまた、そのサブディレクトリの中には他のファイルやサブディレクトリが入っていることもあります。
ディレクトリは、そのような「入れ子」になった構造、再帰的な構造を作り出しています。
... (snip)
Compositeパターンは、このような構造を作るためのものであり、容器と中身を同一視し、再帰的な構造を作るデザインパターン

■ "Composite"のサンプルプログラム

実際に、Compositeパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • ルートエントリのディレクトリに、サブディレクトリおよびファイルを追加してみる
  • ルートエントリのディレクトリに、ユーザエントリのディレクトリを追加して、さらに、 サブディレクトリおよびファイルを追加してみる
  • 敢えて、ファイルに、ディレクトリを追加して、失敗することを確認する
$ 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.java (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/Composite

  • ディレクトリ構成
.
├── Main.py
└── entry.py

(1) Leaf(葉)の役

「中身」を表す役です。この役の中には、他のものを入れることができません。
サンプルプログラムでは、Fileクラスが、この役を努めます。

entry.py
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 _printList(self, prefix=''):
        print("{0}/{1}".format(prefix, self))

(2) Composite(複合体)の役

「容器」を表す役です。Leaf役や、Composite役を入れることができます。
サンプルプログラムでは、Directoryクラスが、この役を努めます。

entry.py
class Directory(Entry):
    def __init__(self, name):
        self.name = name
        self.directory = []

    def getName(self):
        return self.name

    def getSize(self):
        size = 0
        for d in self.directory:
            size += d.getSize()
        return size

    def add(self, entry):
        entry.path = self.name
        self.directory.append(entry)

    def _printList(self, prefix=''):
        print(prefix + "/" + str(self))
        for e in self.directory:
            e._printList(prefix + '/' + self.name)

(3) Componentの役

Leaf役とComposite役を同一視するための役です。Component役は、Leaf役とComposite役に共通のスーパークラスとして実現します。
サンプルプログラムでは、Entryクラスが、この役を努めます。

entry.py
from abc import ABCMeta, abstractmethod

... (snip)

class Entry(metaclass=ABCMeta):
    @abstractmethod
    def getName(self):
        pass

    @abstractmethod
    def getSize(self):
        pass

    def add(self, entry):
        raise FileTreatmentException

    def printList(self):
        self._printList()

    @abstractmethod
    def _printList(self, prefix=''):
        pass

    def __str__(self):
        return "{0} ({1})".format(self.getName(), self.getSize())

(4) Client(依頼人)の役

サンプルプログラムでは、startMainメソッドが、この役を努めます。

Main.py
from abc import ABCMeta, abstractmethod
from entry import Directory, File, 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.printList()

        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.printList()

        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()

(5) その他

例外クラスを追加します

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

■ 参考URL