デザインパターン(Design Pattern)#Decorator


設計を意識したコードが書けるようになる為に、デザインパターン修行しました。
他のDesign Patternもちょくちょく出していきます。

前置き

デザインパターンをどういう時に、何を、どう使うのかを理解することが一先ずの目標。
(Javaというか静的型付言語は初めてで、且つpython歴もそんなに長くないので、Pythonistaぽっくないところがあると思います。ご指摘ございましたらご教授ください。)

今回は、構造に関するパターンDecorator。

Decoratorとは

透過的なインタフェース(API)を保ったまま、既存のオブジェクトを新しいDecoratorオブジェクトでかぶせるすることで既存の関数やクラスの中身を直接触ることなく、その外側から機能を追加したり書き換えたりする。また、既存のクラスを拡張する際にクラスの継承の代替手段として用いられる。

ここで作るサンプルプログラムは、文字列の周りに飾り枠をつけて表示するものです。ここでいう飾り枠とは、-, +, |という文字で書いたものです。

全体のクラス図

display.py
from abc import ABCMeta, abstractmethod


class Display(metaclass=ABCMeta):

    @abstractmethod
    def get_columns(self):
        pass

    @abstractmethod
    def get_rows(self):
        pass

    @abstractmethod
    def get_row_text(self):
        pass

    def show(self):
        for i in range(self.get_rows()):
            print(self.get_row_text(i))

Displayクラスは複数行からなる文字列を表示する抽象クラスです。

get_columns, get_rows, get_row_textは抽象メソッドなので宣言のみ、サブクラスに実装を任せてます。それぞれの役割はget_columnsは横の文字数、get_rowsは縦の行数、get_row_textは指定した行の文字列を得るメソッドです。

showはget_rowsとget_row_textという抽象メソッドを使ったTemplateMethodパターンになってます。get_rowsとget_row_textで表示する文字列を取得し、forループで全ての行を表示するメソッドです。

string_display.py

from display import Display


class StringDisplay(Display):

    def __init__(self, string):
        self.__string = string

    def get_columns(self):
        return len(self.__string)

    def get_rows(self):
        return 1

    def get_row_text(self, row):
        if row == 0:
            return self.__string
        else:
            return None

StringDisplayクラスは1行の文字列を表示するクラスで、Displayクラスのサブクラスです。Displayクラスで宣言されている抽象メソッドを実装しています。
stringフィールドは表示する文字列を保持。StringDisplayクラスで表示するのはstringフィールドの内容1行だけなので、get_columnsは文字列の長さを返し、get_rowsは1を返します。get_row_textは0行目の値をとるときのみstringフィールドを返します。

border.py
from abc import ABCMeta
from display import Display


class Border(Display):

    __metaclass__ = ABCMeta

    def _border(self, display):
        self._display = display

Borderクラスは「飾り枠」を表す抽象クラスです。
ですが、文字列表示を行うDisplayクラスのサブクラスとして定義されています。つまり、継承によって飾り枠は中身と同じメソッドを持つということです。Borderクラスはget_columns, get_rows, get_row_text, showの各メソッドを継承しているということです。

side_border.py
from border import Border


class SideBorder(Border):

    def __init__(self, display, ch):
        self.display = display
        self.__border_char = ch

    def get_columns(self):
        return 1 + self.display.get_columns() + 1

    def get_rows(self):
        return self.display.get_rows()

    def get_row_text(self, row):
        return self.__border_char + \
            self.display.get_row_text(row) + \
            self.__border_char

SideBorderクラスは具体的な飾り枠の一種で、Borderクラスのサブクラスです。SideBorderクラスは、文字列の左右に決まった文字(borderChar)で飾りをつけるものです。そしてスーパークラスで宣言されていた抽象メソッドがすべてここで実装されています。

get_columnsは表示文字の横の文字数を得るメソッドです。この飾り枠がくるんでいる「中身」の文字数に、左右の飾り文字分を加えてたものが文字数になります。

SideBorderクラスは上下方向には手を加えないので、get_rowsメソッドはdisplay.get_rowsがそのまま戻り値になります。

get_row_textメソッドは、引数で指定した行の文字列を得るものです。display.get_row_text(row)という中身の文字列の両側に、bodrder_charとい飾り文字を付け加えたものが戻り値になります。

full_border.py
from border import Border


class FullBorder(Border):

    def __init__(self, display):
        self.display = display

    def get_columns(self):
        return 1 + self.display.get_columns() + 1

    def get_rows(self):
        return 1 + self.display.get_rows() + 1

    def get_row_text(self, row):
        if row == 0:
            return '+' + self._make_line('-', self.display.get_columns()) + '+'
        elif row == self.display.get_rows() + 1:
            return '+' + self._make_line('-', self.display.get_columns()) + '+'
        else:
            return '|' + self.display.get_row_text(row - 1) + '|'

    def _make_line(self, ch, count):
        buf = []
        for i in range(0, count):
            buf.append(ch)
        return ' '.join(buf)

FullBorderクラスも、SideBorderクラスど同様、Borderサブクラスの1つです。FullBorderクラスは上下左右に飾りをつけています。

make_lineメソッドは、指定した文字を連続させた文字列を作る、補助用のメソッドです。

main.py
from string_display import StringDisplay
from side_border import SideBorder
from full_border import FullBorder


def main():
    b1 = StringDisplay('Hello, world')
    b2 = SideBorder(b1, '#')
    b3 = FullBorder(b2)
    b4 = SideBorder(
        FullBorder(
            FullBorder(
                SideBorder(
                    FullBorder(
                        StringDisplay('こんにちは。')
                    ), '*'
                )
            )
        ), '/'
    )

    b1.show()
    b2.show()
    b3.show()
    b4.show()

if __name__ == "__main__":
    main()

実行結果(形が歪だけど・・・)

Hello, world
#Hello, world#
+- - - - - - - - - - - - - -+
|#Hello, world#|
+- - - - - - - - - - - - - -+
/+- - - - - - - - - - - -+/
/|+- - - - - - - - - -+|/
/||*+- - - - - -+*||/
/||*|こんにちは。|*||/
/||*+- - - - - -+*||/
/|+- - - - - - - - - -+|/
/+- - - - - - - - - - - -+/

まとめ

Decoratorパターンでは、飾り枠と中身を同一視しています。

サンプルプログラムで、飾り枠を表すBorderクラスが、中身を表すDisplayクラスのサブクラスになっているところで、その同一視が表現されています。つまり、Borderクラス(Borderクラスのサブクラス達)は、中身を表すDisplayクラスと同じインタフェースを持ってます。

飾り枠を使って中身を包んでも、インタフェースは隠蔽されないので、get_columns, get_rows, get_row_text, showというメソッドは他のクラスから見ることができます。このことをインタフェースが「透過的」といっています。

Decoratorパターンは包めば包むほど、機能が追加されていきます。包まれるものを変更することなく、機能の追加を行うことができました。

pythonにはそもそも、デコレータがありますね

参考