Update 2023.11.18 2017.05.05

Python デザインパターン サンプルコード Template Method
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

◆◆Composite パターンの使われる場面◆◆

Composite とは合成物や複合体のようなニュアンスがある。Composite パターンはツリー構造の一種であるが,ノードが2種類あるので Composite パターンと言われる。

Java サンプルはディレクトリエントリを対象としている。Windows で言えば,フォルダ(ディレクトリ)とファイルである。一般的なツリー構造と違う特徴は,フォルダは中身がなくてもフォルダであり将来に中身が入る可能性があり,ファイルは中身を入れることができない。

Java サンプルの出力のツリーを Windows Explorer 風に表現したもの(カッコ内はサイズ)
root
  = bin
      = vi(10000)
      = latex(20000)
  = tmp
  = usr
      = yuki
          = diary.html(100)
          = Composite.java(200)
      = hanako
          = memo.tex(300)
      = tomura
          = game.doc(400)
          = junk.mail(500)
このような特徴のあるツリー構造を従来からあるグラフ理論のアルゴリズムではなくオブジェクト指向の考えで実装すると,GoF が言うようにフォルダ(ディレクトリ)とファイルを区別することなく扱うことができるようになる。結城氏はこのことを容器と中身の同一視と表現した。

GoFのC++サンプルは製品と部品(と中間にサブアッシー)を部分―全体階層(子が次に親になる階層のこと)に適用している。

ツリー構造の従来のアルゴリズムは,ノードの表現をリストや dict 辞書やその他であってもネスト(入れ子)により再帰定義を表現するのが普通である。Composite パターンでは,メソッドの中で自分自身を呼ぶ再帰呼び出しを使い,さらにインスタンスを巧みに使い分けることによって容器と中身の扱いを同じにしているのである。

この発想は個人が独力で思いつくことが難しいと思われ,この再帰呼び出しを応用するだけでも十分に役に立つと考えられる。


(2023-11-18)Python3.11で動作確認済み


◆◆Composite パターンとは◆◆

GoFによれば,Composite パターンの目的は, 「部分―全体階層を表現するために,オブジェクトを木構造に組み立てる。Composite パターンにより,クライアントは,個々のオブジェクトとオブジェクトを合成したものを一様に扱うことができるようになる。」

GoFによれば,次のような場合に,Composite パターンを使用する。
・オブジェクトの部分―全体階層を表現したい場合。
・クライアントが,オブジェクトを合成したものと個々のオブジェクトの違いを無視できるようにしたい場合。このパターンを用いることで,クライアントは,composite 構造内のすべてのオブジェクトを一様に扱うことができるようになる。

GoFによれば,Composite パターンの関連するパターンは次のようなものである。
Chain Of Responsibility パターン:親子関係にあるオブジェクト間のリンクは,Chain Of Responsibility パターンでしばしば使われる。
Decorator パターン:しばしば Composite パターンとともに使われる。decorator と composite を同時に使う場合,通常,これらは共通の親クラスを持つ。そのため decorator は,Add,Remove,GetChild のようなオペレーションで Component クラスのインタフェースをサポートしなければならなくなる。
Flyweight パターン:このパターンにより,component を共有できるようになる。しかし,共有されるオブジェクトは親オブジェクトを参照できなくなる。
Iterator パターン:composite を走査するために使われる。
Visitor パターン:Composite クラスや Leaf クラスに分散しているオペレーションや振る舞いを局所化する。

◆◆Composite パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
                                 Entry
                      (getName, getSize, printList)

                      ↑                 ↑
                      File             Directory
 (getName, getSize, printList)   (getName, getSize, printList, add)
class Entry は,抽象メソッドでありテンプレートです。
class File は,ファイルを取り扱います
class Directory は,フォルダ(ディレクトリ)を取り扱います。また,ファイルとフォルダ(ディレクトリ)をツリーに登録することができます。

◆◆Composite パターンの実装◆◆

この実装の第1の特徴は if 文がないということです(Java サンプルも同じ)。最後にある if は,自分がトップレベルにあるか判断するものです。オブジェクト指向を徹底すると if 文を減らせます。そうすると,アルゴリズムが簡単になりミスもなくなります。Composite パターンのオブジェクトの使い方は独力で思いつくようなものではないです。そのあたりを詳細に説明します。

class Entry は,抽象メソッドであり詳細は省略します。
def __str__はインスタンスを print すると呼ばれます。これがないままインスタンスを print するとメモリアドレスが表示されるだけです。デザインパターンではインスタンスを自在に使いこなすことが多いのでたいへん便利な特殊関数です。

メソッド getNameとメソッド getSizeはメソッド printList から呼ばれます。これらのすべてのメソッドはクライアント;メインルーチンのたった1行から呼ばれます。そのようなことができるのはツリー構造がオブジェクト指向の考えで出来上がっているからです。

Java と違って Python にはオーバロード(シグニチャが違う同名メソッド)がないのですが printList ではオーバロードもどきようなことをしています。

class Directory のメソッド add はメインルーチンだけから呼ばれます。ファイルとフォルダ(ディレクトリ)をツリーに登録することができます。

ツリーの各ノード(ファイルとフォルダ(ディレクトリ))の登録はすべてクライアント;メインルーチンから行います。この登録の書式が Composite パターンを特徴づけています。応用するときはこの登録を自動化する必要があるかと考えます。

登録の方法は次のようにします。
    usrdir = Directory("usr")   # "usr"をインスタンス化してインスタンス「usrdir」を得る
    rootdir.add(usrdir)         # 「usrdir」をインスタンス「rootdir」のリストに加える

    yuki = Directory("yuki")    # "yuki"をインスタンス化してインスタンス「yuki」を得る
    usrdir.add(yuki)            # 「yuki」をインスタンス「usrdir」のリストに加える

    yuki.add(File("diary.html", 100))  # "diary.html"をインスタンス化して
                                       # インスタンス「yuki」のリストに加える
このようにしてすべてのノード(ファイルとフォルダ(ディレクトリ))はインスタンス化されます。そのうち各ディレクトリの各インスタンスはそれぞれリストを持っています。各ディレクトリの中身(子ノード)はそのリストに入ることになります。上の例はリストが3つあり最後のファイルも含めて4階層になっていることが判ると思います。

ツリー構造のアルゴリズムをまったく意識しなくても上のソースコードのように決まった書式で登録ができます。登録されるノードのインスタンスはメソッド add の引数となり,メソッド add の前に親ノードのインスタンスを付けるという簡単な書式です。

これで出来上がったリストは各インスタンスに所属しているだけであり,ネスト(入れ子)構造になっているわけではありません。リストの親子関係はそれの所属するインスタンスに記録されているわけです。

ファイルとフォルダ(ディレクトリ)の棲み分けはインスタンス化するときだけクラスを選ぶだけです。あとはすべて区別していません。そのことは同名メソッドで抽象メソッドをオーバーライドすることで実現しています。

メソッド getName は説明の必要はないと考えます。

class File のメソッド getSize も説明の必要はないと考えます。

class Directory のメソッド getSize では Iterator パターンの手法が使われています。属性もツリー構造になっているので再帰呼び出しが使われていますが,自分自身を呼び出すとき選ばれたインスタンス付きとなっています。このことにより,親子関係の再帰定義を意識することなく,また,ファイルとフォルダ(ディレクトリ)を意識することなく,単純な呼び出しで複雑なアルゴリズムを実現しています(Python の手法を使うとこれが1行で書けるそうです)。

【class Directory のメソッド getSize】
42
43
44
45
46
47
    def getSize(self):
        self.size = 0
        it = iter(self.directory)
        for entry in it:
            self.size += entry.getSize()
        return self.size

class Directory のメソッド printList も上と同じことが言えます。繰り返しますが,親子関係の再帰定義を意識することなく,また,ファイルとフォルダ(ディレクトリ)を意識することなく,単純な呼び出しで複雑なアルゴリズムを実現しています。

【class Directory のメソッド printList】
51
52
53
54
55
    def printList(self, prefix):
        sys.stdout.write("{0}/{1}\n".format(prefix, self))
        it = iter(self.directory)
        for entry in it:
            entry.printList("{0}/{1}".format(prefix, self.name))

さらに各ノードの属性も同じように扱うことができます。例えば,属性の size を個々に表示するのは簡単にできることは判ると思いますが,その合計を表示することはそう簡単ではありません。オブジェクト指向の考えがそれを簡単にしてしまうのです。

◆◆ソースコード◆◆

このWebページの左上隅からダウンロードできます。

ソースファイルは1つです。
・Composite;Composite パターンのPythonサンプル

【Composite】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
from abc import ABCMeta, abstractmethod

class Entry(metaclass = ABCMeta):

    @abstractmethod
    def getName(self):
        pass
    @abstractmethod
    def getSize(self):
        pass
    def add(self, entry):
        raise FileTreatmentException
    @abstractmethod
    def printList(self, prefix):
        pass
    def printListr(self):
        self.printList("")
    def __str__(self):
        return "{} ({})".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 printList(self, prefix):
        sys.stdout.write("{}/{}\n".format(prefix, self))

class Directory(Entry):
    def __init__(self, name):
        self.name = name
        self.directory = []
    def getName(self):
        return self.name
    def getSize(self):
        self.size = 0
        it = iter(self.directory)
        for entry in it:
            self.size += entry.getSize()
        return self.size
    def add(self, entry):
        self.directory.append(entry)
        return self
    def printList(self, prefix):
        sys.stdout.write("{}/{}\n".format(prefix, self))
        it = iter(self.directory)
        for entry in it:
            entry.printList("{}/{}".format(prefix, self.name))

class FileTreatmentException(Exception):
    pass

def main():
    sys.stdout.write("Making root entries...\n")
    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.printListr()

    sys.stdout.write("Making root entries...\n")
    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.printListr()

if __name__ == '__main__':
    main()
"""標準出力
Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making root 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)
"""

以上

トップページに戻る
inserted by FC2 system