Strategyパターン

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


前回のTemplateMethodパターンではレポートのフォーマットの切り替えに、
抽象基底クラスを作成し、詳細な点はサブクラスに任せます。

TemplateMethodパターンの欠点は、継承を用いていることです。サブクラスは基底クラスに依存してしまうのです。一度基底クラスがコケると全ての子孫が皆コケてしまいますし、
互いにくっついて離すことが出来ないのです。

また、TemplateMethodパターンは柔軟性に問題があります。

まず、実行時のHTMLで出力するか・プレーンテキストで出力するかを切り替えづらいのです。
そして、新しいフォーマットを作るたびに、一々サブクラスを作るのは大げさな場合があります。


そこで登場するのがStrategyパターンです。

Strategyパターンでは、

変化するアルゴリズムを、サブクラスではなく第3者のオブジェクトに移譲します。

#strategy1.py
class Report(object):
    def __init__(self, title, text, formatter):
        self.title = title
        self.text  = text
        self.formatter = formatter
    
    def output_report(self):
        self.formatter.output_report(self.title, self.text)

class Formatter(object):
    def output_report(self, title, text):
        assert False
    
class HTMLFormatter(Formatter):
    def output_report(self, title, text):
        print("<html>")
        print("<head>")
        print("<title>%s</title>"%(title,))
        print("</head>")
        print("<body>")
        for line in text:
            print("<p>%s</p>"%(line,))
        print("</body>")
        print("</html>")

class PlainTextFormatter(Formatter):
    def output_report(self, title, text):
        print("***%s***"%(title,))
        for line in text:
            print(line)

if __name__ == "__main__":
    report = Report(u"月次報告", [u"順調!", u"最高です!"], 
                    PlainTextFormatter())
    report.output_report()
    print("-"*70)
    report = Report(u"月次報告", [u"順調!", u"最高です!"],
                    HTMLFormatter())
    report.output_report()
    print("-"*70)

このとき、HTMLFormatter・PlainTextFormatterはそれぞれ、
「レポートをフォーマットする」という同じ目的のストラテジ Strategyを定義しています。

ストラテジオブジェクトは、同じインターフェイスと動作を備えている必要があります。
今回の場合はtitleとtextを引数に取りレポートを出力するoutput_reportメソッドです。
同じインターフェイスを持っているので、ストラテジの利用者(コンテキスト Context
は実行時にストラテジを切り替えることが容易になるのです。

また、「レポートをフォーマットする」という機能の実装をformatterに任せて、
Reportからその部分を取り除くことで「関心の分離」を行うことが出来ます。

ところで、HTMLFormatterもPlainTextFormatterもFormatterから継承しているのは、
output_reportメソッドだけです。特別なアルゴリズムやメンバ変数を受け継いでいるわけではありません。
すると、Formatter基底クラスなんか要らないんじゃないか、と考えるのは自然な発想です。
Pythonは『ダックタイピング』を採用しています。オブジェクトの出自がどうであろうが、output_reportメソッドがあればそれで良いのです。さらに言えば、HTMLFormatterも、PlainTextFormatterも、output_report以外にメソッドがありません。メンバ変数すらないのです。これじゃあ、ただの関数と変わりません。

ならば、本当に関数にしてしまえばいいじゃありませんか!

#strategy2.py
class Report(object):
    def __init__(self, title, text, formatter):
        self.title = title
        self.text  = text
        self.formatter = formatter
    
    def output_report(self):
        # self.formatter.output_report(self.title, self.text)
        # formatter として、呼び出し可能型を取るように変更
        self.formatter(self.title, self.text) 

#関数版のHTMLFormatter
def html_formatter(title, text):
    print("<html>")
    print("<head>")
    print("<title>%s</title>"%(title,))
    print("</head>")
    print("<body>")
    for line in text:
        print("<p>%s</p>"%(line,))
    print("</body>")
    print("</html>")

#Callable版 PlainTextFormatter 
class PlainTextFormatter(object):
    def __init__(self, decoration="***"):
        self.decoration = decoration
    
    def __call__(self, title, text):
        print("%s%s%s"%(self.decoration, title, self.decoration))
        for line in text:
            print(line)

if __name__ == "__main__":
    report = Report(u"月次報告", [u"順調!", u"最高です!"], 
                     html_formatter)
    report.output_report()
    report = Report(u"月次報告", [u"順調!", u"最高です!"], 
                     PlainTextFormatter())
    report.output_report()
    report = Report(u"月次報告", [u"順調!", u"最高です!"], 
                     PlainTextFormatter("==="))
    report.output_report()

Reportクラスにも多少の変更しました。これで関数を渡すことが出来ます。

また、PlainTextFormatterもタイトルの飾り部分を変更できるようにし、
関数のように呼び出せるようにしました。

__call__メソッドを定義すれば、インスタンスを関数のように呼び出せるようになります。

ところで、今Report.output_reportでformatterに渡されているのは、
titleとtext の2つだけですが、今後渡すべき要素が増える可能性もあります。例えば日付や提出者名など。

そこで、一々要素を選んで渡すのではなく、Reportオブジェクトそのものを渡してしまう方法もあります。

#strategy3.py
class Report(object):
    def __init__(self, title, text, formatter):
        self.title = title
        self.text  = text
        self.formatter = formatter
    
    def output_report(self):
        #self.formatter(self.title, self.text) 
        self.formatter(self) 

def html_formatter(content):
    print("<html>")
    print("<head>")
    print("<title>%s</title>"%(content.title,))
    print("</head>")
    print("<body>")
    for line in content.text:
        print("<p>%s</p>"%(line,))
    print("</body>")
    print("</html>")

if __name__ == "__main__":
    report = Report(u"月次報告", [u"順調!", u"最高です!"], 
                     html_formatter)
    report.output_report()

しかし、こうするとformatterとReportの結合度が増してしまいます。

Strategyパターンを使う際は、ストラテジの範囲と渡すべきデータを見極める必要があり、
それを誤ると、第2第3のStrategyの適用を妨げることにもなります。