Strategyパターン

概要

アルゴリズム等、戦略の変更を容易にする」パターンです。

例題

月報をHTML or マークダウン形式で出力します。

タイトルは「Monthly Report」、中身は「Good」と「Best」の2行だけのシンプルな月報を作ります。

サンプルコード

def main():
    title = "Monthly report"
    contents = ["good", "best"]

    # Ouput by Markdown
    report = Report(title, contents, PlainTextFormatter())
    report.output_report()

    print("-----")

    # Output by HTML
    report = Report(title, contents, HTMLFormatter())
    report.output_report()


# Context
class Report():
    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)


# Strategy
class Formatter:
    def output_report(self, title, text):
        raise NotImplementedError()


# ConcreteStrategy
class HTMLFormatter(Formatter):
    def output_report(self, title, text):
        print("<html><head><title>{}</title></head>".format(title))
        print("<body>")
        for line in text:
            print("<p>{}</p>".format(line))
        print("</body>")
        print("</html>")


# ConcreteStrategy
class PlainTextFormatter(Formatter):
    def output_report(self, title, text):
        print("**{}**".format(title))
        for line in text:
            print(" -", line)


if __name__ == "__main__":
    main()

# 出力

**Monthly report** - good - best ----- <html><head><title>Monthly report</title></head> <body> <p>good</p> <p>best</p> </body> </html>

特徴

重要な部分を以下に挙げます。

def main():
    ...

    # Ouput by Markdown
    report = Report(title, contents, PlainTextFormatter())
    report.output_report()

    ...

    # Output by HTML
    report = Report(title, contents, HTMLFormatter())
    report.output_report()

class HTMLFormatter(Formatter):
    def output_report(self, title, text):
        ...

class PlainTextFormatter(Formatter):
    def output_report(self, title, text):
        ...

PlainTextとHTMLで出力するとき、

共通 → 出力すること
差異 → 出力の仕方

です。

そこで、「変わるものを変わらないものから分離する」という原則に従い、出力の仕方の部分を別のクラス(PlainTextFormatter() または HTMLFormatter())に任せます。

こうすることで、report.output_report()と同じコードであっても、出力を変更することが可能になります。

このように、同じコードであっても、中身の(変数の)違いによって、ふるまいが変わることを委譲といいます。 (この場合、reportの振る舞いをformatterに委譲しています

委譲はデザパタでは頻出なので、ぜひ覚えておきましょう。

Pythonらしく書くためのコツ

Pythonのクラスは第一級オブジェクト」が使えます。
(ここでは関数が第一級オブジェクトであることを利用しています。)

具体的な例をあげると、こんな感じです。

def hoge():
    print("hogehoge")
h = hoge
h() # => hogehoge

サンプルコードでは report.output_report() にオブジェクトが入っていることで、 ふるまいを委譲することが可能でしたが、Pythonだと関数を変数として扱えるので、オブジェクトにしなくてもふるまいを委譲することができます。

def main():
    title = "Monthly report"
    contents = ["good", "best"]

    # Ouput by PlainText
    report = Report(title, contents, plain_formatter)
    report.output_report()

    print("-----")

    # Output by HTML
    report = Report(title, contents, html_formatter)
    report.output_report()


# Context
class Report():
    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)


def html_formatter(title, text):
    print("<html><head><title>{}</title></head>".format(title))
    print("<body>")
    for line in text:
        print("<p>{}</p>".format(line))
    print("</body>")
    print("</html>")


def plain_formatter(title, text):
        print("**{}**".format(title))
        for line in text:
            print(" -", line)


if __name__ == "__main__":
    main()

クラス図

# 一般的なクラス図

一般的なクラス図(Strategyパターン)

# サンプルコードにおけるクラス図

Contextクラスしかないため、省略。

具体的なアルゴリズムはグローバルな関数から直接選択することになります。

参考URL

実践Python3