Decoratorパターン

Rubyによるデザインパターン』(ラス・オルセン著 ピアソン・エデュケーション刊)の例をPythonに変換して書いています。目次


Strategyパターンでは、他のオブジェクトに委譲する事で、アルゴリズムを動的に変更することをしました。たとえば、HTMLを出力するか、プレーンテキストで出力するかを、実行時に切り替えることを。

しかし、アルゴリズムを丸ごと入れ替えるのではなく、行番号も出力するようにしたり、書き込みのチェックサムと取るようにするなど、機能を追加したいこともあります。

これを実現するのが、Decoratorパターンです。


Decoratorでは、既存のオブジェクトと新機能を追加するデコレータオブジェクトに同じインターフェイスを持たせる事で、機能の動的な追加・削除を実現します。

唐突ですが、ファイルに行を書き込むためのクラスを作りました。

class SimpleWriter(object):
    def __init__(self, path):
        self.file = open(path, "w")
    
    def writeline(self, line):
        print >> self.file, line
    
    def pos(self):
        return self.file.tell()
    
    def rewind(self):
        self.file.seek(0) 
    
    def close(self):
        self.file.close()

単に書き込むだけでは、ファイルオブジェクトと同じなので、
行番号や行の書き込み時刻を行頭に出力したり、チェックサム機能を付けたいと思います。

しかし考えてみれば、この3つの機能は、使う組み合わせが複数あります。
行番号だけを出力する場合と、時刻だけを出力する場合、というだけではなく、


  • 行番号の後に書き込み時刻を出力する

  • 書き込み時刻の後に行番号を出力する

  • 行のチェックサムを計算して、行番号を出力し、その次に時刻を出力する

  • 行頭に行番号を追加したデータのチェックサムを計算した上で、時刻を出力する

と、組み合わせと順番の場合わけは、たくさんあります。

numbering_checksumming_timestamping_writelineChecksummingNumberingTimestampingWriterのように、
組み合わせごとにメソッドやクラスを定義するわけには行きません。

そこで、既存のWriterを強化するデコレータを、各機能毎につくり、
デコレートする順番を入れ替えるようにします。

class WriterDecorator(object):
    """
        デコレータの基底クラス
        SimpleWriterと同じインターフェイスを持つのがミソ
    """
    def __init__(self, writer):
        self.writer = writer
    
    #以下のメソッドは単にwriterに丸投げ
    def writeline(self, line):
        return self.writer.writeline(line)
    
    def pos(self):
        return self.writer.pos()
    
    def rewind(self):
        return self.writer.rewind()
    
    def close(self):
        return self.writer.close()

class NumberingWriter(WriterDecorator):
    """行番号出力用デコレータ"""
    def __init__(self, *a, **kw):
        WriterDecorator.__init__(self, *a, **kw)
        self.line_number = 0
    
    def writeline(self, line):
        self.line_number += 1
        return self.writer.writeline("%d %s"%(self.line_number, line))
    
    
class ChecksummingWriter(WriterDecorator):
    """チェックサム計算用デコレータ"""
    def __init__(self, *a, **kw):
        WriterDecorator.__init__(self, *a, **kw)
        self._checksum = 0
    
    def writeline(self, line):
        for c in line:
            self._checksum = (self._checksum + ord(c)) % 256
        return self.writer.writeline(line)
    
    def checksum(self):
        return self._checksum
    
    
class TimestumpingWriter(WriterDecorator):
    """時刻出力用デコレータ"""
    def writeline(self, line):
        from datetime import datetime
        return self.writer.writeline("%s %s"%(datetime.now(), line))
    
if __name__ == "__main__":
    real_writer = SimpleWriter("test.txt")
    checksumming_writer = ChecksummingWriter(real_writer)
    
    w = TimestumpingWriter(NumberingWriter(checksumming_writer))
    w.writeline("aaa")
    w.writeline("bbbb")
    w.writeline("ccccc")
    w.writeline("dddddd")
    
    print "checksum = %s"%(checksumming_writer.checksum())


これで、行番号と時刻の順番を入れ替えることも容易になりましたし、チェックサムも取れます。

重要なのはデコレータのインターフェイスが、SimpleWriterと同じである事です。インターフェイスが同じなので、デコレータを何枚も重ね着できます。

デコレータパターンの欠点としては、実行速度があります。3枚重ね程度ならまだしも、20枚も30枚も重ねた場合、一番上のwritelineを呼ぶと、29回も下層のwritelineに丸投げするコードが実行される事になります。

速度が重要な場合は、デコレータの積み重ねではなく、
一枚岩のクラス(UltraSuperDeluxeWriterとか?)を作った方が良いです。

ところで、WriterDecoratorでは、全てのメソッドをself.writerに丸投げしていますが、
ほとんど同じコードを4回も書くのは大変です。

それについては、

で書きます。