4
7

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 5 years have passed since last update.

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

Last updated at Posted at 2020-01-19

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

■ Decorator(デコレータ・パターン)

Decoratorパターン(デコレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

UML class and sequence diagram

W3sDesign_Decorator_Design_Pattern_UML.jpg

UML class diagram

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

□ 備忘録

オブジェクトにどんどんデコレーション(飾り付け)を施していくようなデザインパターンとのことです。
スポンジケーキに、クリームを塗り、イチゴを載せればストロベリーショートケーキになるような感じです。
Pythonプログラミングに携わっていると、よくお目にかかるやつですね。

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

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

  • 文字列Hello, world.をそのまま表示する
  • 文字列Hello, world.の前後に、#文字を差し込んで表示する
  • 文字列Hello, world.の前後に、#文字を差し込んで、さらに、 枠線で囲んで表示する
  • 文字列HELLOを 枠線で囲んで、前後に、*文字を差し込んで、さらに、 枠線で2回囲んで、、、(以下、略)
$ python Main.py 
Hello, world.
# Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

/+-----------+/
/|+---------+|/
/||*+-----+*||/
/||*|HELLO|*||/
/||*+-----+*||/
/|+---------+|/
/+-----------+/

見た目、ハノイの塔みたいな表示になりました。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step1

  • ディレクトリ構成
.
├── Main.py
└── decorator
    ├── __init__.py
    ├── border.py
    └── display.py

(1) Componentの役

機能を追加するときの核に役です。Component役は。スポンジケーキのインタフェースだけを定めます。
サンプルプログラムでは、Displayクラスが、この役を努めます。

decorator/display.py
from abc import ABCMeta, abstractmethod

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

... (snip)

(2) ConcreteComponentの役

Component役のインタフェースを実装している具体的なスポンジケーキです。
サンプルプログラムでは、StringDisplayクラスが、この役を努めます。

decorator/display.py
class StringDisplay(Display):
    def __init__(self, string):
        self.string = string

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None

(3) Decorator(装飾者)の役

Component役と同じインタフェースを持ちます。そして、さらに、このDecorator役が飾る対象となるComponent役を持っています。この役は、自分が飾っている対象を「知っている」わけです。
サンプルプログラムでは、Borderクラスが、この役を努めます。

decorator/border.py
from decorator.display import Display

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

(4) ConcreteDecorator(具体的な装飾者)の役

具体的なDecoratorの役です。
サンプルプログラムでは、SideBorderクラスと、FullBorderクラスが、この役を努めます。

decorator/border.py
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
decorator/border.py
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.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

(5) Client(依頼人)の役

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

Main.py
from decorator.display import StringDisplay
from decorator.border import SideBorder, FullBorder

def startMain():
    b1 = StringDisplay('Hello, world.')
    b2 = SideBorder(b1, '#')
    b3 = FullBorder(b2)
    b1.show()
    b2.show()
    b3.show()
    print("")
    b4 = SideBorder(
        FullBorder(
            FullBorder(
                SideBorder(
                    FullBorder(
                        StringDisplay('HELLO')
                    ),
                    '*'
                )
            )
        ),
        '/'
    )
    b4.show()

if __name__ == '__main__':
    startMain()

□ 備忘録 (Pythonデコレータを活用してみる!) -その1-

Pythonプログラミングで、よく、デコレータを見かける機会が多いので、startMainメソッドの部分をPythonデコレータで書き換えてみたいと思います。

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step2

decorator/display.py
from abc import ABCMeta, abstractmethod

className = []

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

class StringDisplay(Display):
    def __init__(self, func):
        self.string = "Hello, world."
        self.func = func
        super(StringDisplay, self).__init__()

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()
decorator/border.py
from decorator.display import Display, className

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

class SideBorder(Border):
    def __init__(self, func):
        self.func = func
        self.__borderChar = '#'
        super(SideBorder, self).__init__(func)

    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
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

class FullBorder(Border):
    def __init__(self, func):
        self.func = func
        super(FullBorder, self).__init__(func)

    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.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

    def __call__(self):
        global className
        className.append(self)
        return self.func()
Main.py
from decorator.display import StringDisplay, className
from decorator.border import FullBorder, SideBorder

def startMain():
    @FullBorder
    @SideBorder
    @StringDisplay
    def dummy1():
        className[2].show()
        className[1].show()
        className[0].show()
    dummy1()
    print("")
    className.clear()

    @SideBorder
    @FullBorder
    @FullBorder
    @SideBorder
    @FullBorder
    @StringDisplay
    def dummy2():
        className[0].show()
    dummy2()

if __name__ == '__main__':
    startMain()

動かしてみます。

$ python Main.py 
Hello, world.
# Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

# +-------------------+#
# |+-----------------+|#
# ||#+-------------+#||#
# ||#|Hello, world.|#||#
# ||#+-------------+#||#
# |+-----------------+|#
# +-------------------+#

ここでのやり方では、枠線の文字を#と、文字列Hello, world.を埋め込みにしてしまったので、プログラミングの柔軟性が損なってしまった結果になってしまいました。

□ 備忘録 (Pythonデコレータを活用してみる!) -その2-

枠線の文字や、文字列を適時、指定できるように、Pythonデコレータのやり方を、さらに改善してみます。
こちらのQiita記事「OpenStackを活用して、WSGI アプリケーションの仕組みを理解するを参考にしています。

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step3

decorator/display.py
from abc import ABCMeta, abstractmethod

className = []

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

class StringDisplay(Display):
    def __init__(self, func, string):
        self.string = string
        self.func = func
        super(StringDisplay, self).__init__()

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

def StringDisplay_filter(string):
    def filter(func):
        return StringDisplay(func, string)
    return filter
decorator/border.py
from decorator.display import Display, className

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

class SideBorder(Border):
    def __init__(self, func, ch):
        self.func = func
        self.__borderChar = ch
        super(SideBorder, self).__init__(func)

    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
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

def SideBorder_filter(ch):
    def filter(func):
        return SideBorder(func, ch)
    return filter

class FullBorder(Border):
    def __init__(self, func):
        self.func = func
        super(FullBorder, self).__init__(func)

    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.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

    def __call__(self):
        global className
        className.append(self)
        return self.func()
Main.py
from decorator.display import StringDisplay_filter, className
from decorator.border import SideBorder_filter, FullBorder

def startMain():
    @FullBorder
    @SideBorder_filter('#')
    @StringDisplay_filter("Hello, world.")
    def dummy1():
        className[2].show()
        className[1].show()
        className[0].show()
    dummy1()
    print("")
    className.clear()

    @SideBorder_filter('/')
    @FullBorder
    @FullBorder
    @SideBorder_filter('*')
    @FullBorder
    @StringDisplay_filter("HELLO")
    def dummy2():
        className[0].show()
    dummy2()

if __name__ == '__main__':
    startMain()

動かしてみます。

$ python Main.py 
Hello, world.
# Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

/+-----------+/
/|+---------+|/
/||*+-----+*||/
/||*|HELLO|*||/
/||*+-----+*||/
/|+---------+|/
/+-----------+/

最初の動作結果と同じになりましたので、これで、ひとまず、完成とします。

■ 参考URL

4
7
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
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?