Update 2023.11.18 2017.05.05

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

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

簡単に言うと,継承を使わないで,既存クラスをインスタンス化してそれのメソッドを呼び出すことによって機能を追加することが Decorator パターンです。

Web からの引用なのですが,Decorator パターンのようにメソッドを呼び出して,機能を追加するのをコンポジションと言います。スーパークラス・サブクラスの継承と似ていますが,前者は has-a 関係があるときに使い,後者は is-a 関係があるときに使うというような使い分けするのがオブジェクト指向であるということらしい。

さらに引用させてもらうと,コンポジションの対象である has-a 関係とは,「パソコンはCPUを含んでいる」「学校は先生を含んでいる」「自転車はサドルを含んでいる」など,継承の対象である is-a 関係とは,「犬は動物である」「猫は動物である」「人間は動物である」などである。

Pythonの言語仕様としての decorator があるが,それとは違うものです。


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


◆◆Decorator パターンとは◆◆

GoFによれば,Decorator パターンの目的は, 「オブジェクトに責任を動的に追加する。Decorator パターンは,サブクラス化よりも柔軟な機能拡張方法を提供する。」

GoFによれば,Decorator パターンの別名は,Wrapper パターンである。

GoFによれば,次のような場合に Decorator パターンを利用する。
・個々のオブジェクトに責任を動的,かつ透明に(すなわち,他のオブジェクトには影響を与えないように)追加する場合。
・責任を取りはずすことができるようにする場合。
・サブクラス化による拡張が非実用的な場合。非常に多くの独立した拡張が起こり得ることがある。このような場合,サブクラス化によりすべての組み合わせの拡張に対応しようとすると,莫大な数のサブクラスが必要になるだろう。また,クラス定義が隠ぺいされている場合や入手できない場合にも,このパターンを利用できる。

GoFによれば,Decorator パターンの関連するパターンは次のようなものである。
Adapter パターン: decorator は adapter とは異なる。decorator はそれが装飾しているオブジェクトの責任を変えるだけで,インタフェースまでは変えない。adapter はオブジェクトにまったく新しいインタフェースを与える。
Composite パターン: decorator は component を1つしか持たない退化した composite と見なすことができる。しかし,decorator は新たな責任を追加する。オブジェクトの集約が目的ではない。
Strategy パターン: decorator はオブジェクトの殻を変える。一方,strategy はオブジェクトの中身を変える。オブジェクトを変化させる方法には,この2通りが考えられる。

◆◆Decorator パターンのサンプルのクラス構成◆◆
                            Display
               (getColumns, getRows, getRowText, show)
                 ↑                 ↑
         StringDisplay             Border
(getColumns, getRows, getRowText)
                                  ↑     ↑
                         SideBorder   FullBorder
(getColumns, getRows, getRowText) (getColumns, getRows, getRowText, _makeLine)
サンプルは文字列の周りに飾り枠を付けるものです。文字列を表す具象クラス StringDisplay,飾り枠を表す 具象クラス SideBorder,FullBorderとこれらの抽象クラス Displayには同じメソッドがあります。抽象クラス Border のことを Decorator と言うそうです。もちろんそのサブクラスも Decorator です。

飾り枠を使って中身を包んでも,メソッドは他のクラスから見ることができるので,インタフェースが「透過的」になっています。包むことによって機能が追加されていくのですが,包まれるものを変更しないことがポイントです。

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

サンプルのクライアント;メインルーチンと出力を見ればサンプルのやっていることはほぼ判ります。次のように具象クラス StringDisplay,SideBorder,FullBorderだけで文字列と飾り枠を表します。StringDisplayのインスタンスをSideBorderの引数にしています。そしてSideBorderのインスタンスをFullBorderの引数にしています。これが包むということ(Wrapper)です。呼ぶのは抽象クラス Display のメソッド show だけであることがポイントです。
    b1 = StringDisplay(u"Hello, world")
    b2 = SideBorder(b1, '#')
    b3 = FullBorder(b2)
    b1.show()
    b2.show()
    b3.show()
メインルーチンの4番目の表示の例は,引数をネスト(入れ子)にすることによりインスタンス化しています。判り易いように Java 風にインデントしています。

具象クラスのメソッドは,引数のクラス(のインスタンス)のメソッドを呼んでいてそれに機能を追加するようになっています。そのため包まれるクラスを変更することなく自由にいくらでも機能を追加できます。

包むものと包まれるものが同一視されているので,同じパターンで何重にも包むことができるのがポイントです。

ちなみに,GoF の C++ のサンプルは,CAD やペイントツールの画層を追加するのに Decorator パターンを使うものです。クラスの構成は Java サンプルと完全に同じです。

◆◆ソースコード◆◆

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

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

【Decorator.py】
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import unicodedata
from abc import ABCMeta, abstractmethod

class Display(metaclass=ABCMeta):

    @abstractmethod
    def getColumns(self):
        pass
    @abstractmethod
    def getRows(self):
        pass
    @abstractmethod
    def getRowText(self):
        pass
    def show(self):
        for i in range(self.getRows()):
            sys.stdout.write("{0}\n".format(self.getRowText(i)))

class StringDisplay(Display):
    def __init__(self, string):
        self.string = string
    def getColumns(self):
        #self.width = len(self.string)      # 全角を1文字に数える
        def str_len(str):
            length = 0
            for ch in str:
                ch_width = unicodedata.east_asian_width(ch)
                if ch_width in "WFA":
                    length += 2             #戻り値:WとFとAが全角
                else:
                    length += 1             #戻り値:NaとHが半角
            return length
        self.width = str_len(self.string)   # 全角を2文字に数える
        return self.width
    def getRows(self):
        return 1
    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None

class Border(Display):
    def __init__(self, display):
        self.display = display

class SideBorder(Border):
    def __init__(self, display, ch):
        super(SideBorder, self).__init__(display)
        self.borderChar = ch
    def getColumns(self):
        return 1 + self.display.getColumns() + 1
    def getRows(self):
        return self.display.getRows()
    def getRowText(self, row):
        return self.borderChar + \
            self.display.getRowText(row) + \
            self.borderChar

class FullBorder(Border):
    def __init__(self, display):
        super(FullBorder, self).__init__(display)
    def getColumns(self):
        return 1 + self.display.getColumns() + 1
    def getRows(self):
        return 1 + self.display.getRows() + 1
    def getRowText(self, row):
        if row == 0:
            return '+' + self._makeLine('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self._makeLine('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'
    def _makeLine(self, ch, count):
        buf = []
        for i in range(0, count):
            buf.append(ch)
        return "".join(buf)

def main():
    # x = Display()
    # Test : Can't instantiate abstract class Display with abstract methods
    b1 = StringDisplay("Hello, world")
    b2 = SideBorder(b1, '#')
    b3 = FullBorder(b2)
    b1.show()
    b2.show()
    b3.show()
    b4 = SideBorder(
        FullBorder(
            FullBorder(
                SideBorder(
                    FullBorder(
                        StringDisplay("こんにちは。")
                    ), '*'
                )
            )
        ), '/'
    )
    b4.show()

if __name__ == "__main__":
    main()
"""標準出力
Hello, world
#Hello, world#
+--------------+
|#Hello, world#|
+--------------+
/+------------------+/
/|+----------------+|/
/||*+------------+*||/
/||*|こんにちは。|*||/
/||*+------------+*||/
/|+----------------+|/
/+------------------+/
"""

以上

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