pythonによるデザインパターン[Decorator]

# Intro

この文章は結城 浩さんの「Java言語で学ぶデザインパターン入門」を、pythonで実装してみたサンプルコードです。

筆者の環境は以下の通りです。Python 3.6.3

まだ修行中の身なので間違いがあると思いますがご了承ください。 今回は「Decorator」です。

# Summarise

本書のp154によると、 Decoratorとは

オブジェクトにどんどんデコレーション(飾り付け)を施していくようなデザインパターン

とあります。この「どんどん」という部分が重要で、様々なデコレーション(class)を様々な組み合わせで重ねる時に、どのように構成するかを学びます。

# Practice

## Specification

文字列(ここでは Hello, world. とする)を入力すると、主に2つの方法でデコレーションして表示するようにしてください。

前提として、 class StringDisplay のインスタンスを作成し、 def show を実行すると普通に表示します。これはデコレーションしない、核となる文字列を意味しています。

b1 = StringDisplay('Hello, world.')
b1.show()
Hello, world.

1つ目のデコレーションは下のように、 class SideBorderのインスタンスを作成し、 def show を実行すると、引数に与えられた文字で左右をデコレーションします。このときclass SideBorderのインスタンスを生成するときは、デコレーションする中身のインスタンス(今回はb1)を渡します。

b2 = SideBorder(b1, '#')
b2.show()
#Hello, world.#

2つ目のデコレーションは下のように、 class FullBorderのインスタンスを作成し、 def show を実行すると、上下左右をデコレーションします。このときclass FullBorderのインスタンスを生成するときは、デコレーションする中身のインスタンス(今回はb1)を渡します。

b3 = FullBorder(b1)
b3.show()
+-------------+
|Hello, world.|
+-------------+

重要なのは、何度もデコレーションできることです。文字列をデコレーションするだけでなく、デコレーションしたものをさらにデコレーションできることです。

具体例を挙げると、以下はHello, world.class SideBorder でデコレーションし、さらにそのデコレーションしたものをclass FullBorder でデコレーションします。このデコレーションの方法はどんな組み合わせでも表示できるようにしてください。

b1 = StringDisplay('Hello, world.')
b2 = SideBorder(b1, '#')
b3 = FullBorder(b2)
b3.show()
+---------------+
|#Hello, world.#|
+---------------+

## Explanation

最初に class Display を作成します。これは複数行にわたる文字列を表示するための抽象クラスとで、以下のメソッドを持ちます。

  • 横の文字数を数える(def get_columns )
  • 縦の文字の行数を数える(def get_rows )
  • 引数で与えられた行の文字列を返す(def get_row_text)
  • 文字を全て表示する(def show)

なぜ、こんな抽象クラスが必要なんだと思いますが、後々必要性がわかるので今回は説明を省きます。

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

次にデコレーションしない核となる文字列を表すclass StringDisplay を作成します。

ここで、先ほど作った抽象クラスclass Display を継承します。ただ文字列を表示するだけなので、縦の文字の行数を数える(def get_rows )時は1、引数で与えられた行の文字列を返す(def get_row_text)時は0行目だけ文字列を返します。横の文字数を数える(def get_columns )時に def count_chara というメソッドを呼び出していますが、これは文字列を返すと文字の長さを返す関数を定義しているだけなので、無視して大丈夫です。

次にデコレーションを表す、抽象クラスclass Border を作成します。

ここで重要なのは、デコレーションを表すclass Border も核となる文字列を表すclass StringDisplay と同様にclass Display を継承していることです。これによって、デコレーションと核となる文字列を区別しなくても良いという利点があります。

余談ですが、区別といえばCompositeも容器と中身と同一視しているので確認してみてください。

もう一つ重要なのは委譲(delegation)を用いている点です。このdisplayというFieldは、class Display のインスタンスを示しています。よって委譲(delegation)によってclass Display のインスタンスにアクセスしています。

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

では次に個別のデコレーションの具体的なクラスを作成しましょう。まずは左右をデコレーションするclass SideBorder を作成します。

このクラスはデコレーションを表す、抽象クラスclass Border を継承しています。よってインスタンスを生成する時は、スーパークラスの def __init__ で、class Display のインスタンスをdisplayに代入しましょう。

class SideBorderclass Borderを継承し、またclass Borderclass Display を継承しているので、以下のメソッドを定義します。

  • 横の文字数を数える(def get_columns )
  • 縦の文字の行数を数える(def get_rows )
  • 引数で与えられた行の文字列を返す(def get_row_text)

それぞれの実装は注意深く読めばわかると思いますが、重要なのは self.display を何度も呼び出している点です。委譲(delegation)により displayを呼び出すことができ、displayはデコレーションもしくは核となる文字列どちらかを表します。

これによって、何度もデコレーションできるようになります。 display の中身をデコレーションと核となる文字列を区別しなくても良いので、例えば横の文字数を数える(def get_columns )ときは

1 + (displayの中身の横の文字の長さ) + 1

という風に考えればいいのです。再帰的な処理によって、プログラムが簡単に記述できますね。

最後は左右をデコレーションするclass FullBorder ですが、説明はclass SideBorder と同一なので省きます。

## Sample Code

# Conclusion

このパターンは抽象クラスによる継承と委譲(delegation)を上手く利用したパターンだと思います。それによって、一度デコレーションしても中身のインターフェイスが変わらないので、またデコレーションができるようになります。このように何度も委譲や継承を行なっても、中身が変更されないことをインターフェースが透過的と言います(p180)。

最初は何をやっているのかよくわからなかったのですが、読んでいくうちにわかってきました。これによって柔軟に機能が追加できるので、覚えておきたいです。

# ref

--

--