Pythonでデザインパターン Strategyパターン


概要

取り替え可能パーツ群を戦略的に利用せよ!!

参考

@kidach1 さん、
Rubyによるデザインパターン【Strategy】-取り替え可能パーツ群を戦略的に利用せよ-
https://qiita.com/kidach1/items/02f8f6df955ba1d0e93b

ストラテジ(Strategy) | Ruby デザインパターン
http://morizyun.github.io/ruby/design-pattern-storategy.html

どんなパターンか

抽象的な処理と具体的な処理を分離することで、
変化に強い構造を実現する。

→その目的はTemplate Methodとほぼ同等

Template Methodの課題

しかし、Template Methodには欠点がある。それは、
継承をベースにしていること。

継承の欠点
http://qiita.com/kidachi_/items/4b63de9ad5a97726c50c#2-3

スーパークラスの振る舞いの変更は、サブクラスの振る舞いを変える
サブクラスはスーパークラスの中身を覗くことが出来る
そこでStrategyでは、
変化しやすいコードの塊を抽出し、
全く別のクラスに閉じ込める。

つまり、集約を使う。

異なるオブジェクトからアルゴリズムを引き出す

同じ目的を持った一群のオブジェクトをストラテジ(群)と捉える。
ストラテジ群は、そのすべてが全く同じインターフェイスを提供する。

コンテキスト(ストラテジを利用する側)は、各ストラテジを
取り替え可能パーツとして扱うことが出来るようになる。

関心の分離 を実現する。

ストラテジの構成

ストラテジは次の3つのオブジェクトによって構成されます。

コンテキスト(Context):ストラテジの利用者
抽象戦略(Strategy):同じ目的をもった一連のオブジェクトを抽象化したもの
具象戦略(ConcreteStrategy):具体的なアルゴリズム

ストラテジのアイデアは、コンテキストが「委譲」によってアルゴリズムを交換できるようにすることです。委譲とは、ある機能をもつオブジェクトを生成してオブジェクトに処理を依頼することです。

ストラテジのメリット

  • 使用するアルゴリズムに多様性を持たせることができる
  • コンテキストと戦略を分離することでデータも分離できる
  • 継承よりもストラテジを切り替えるのが楽

実装

レポートをHTML形式とプレーンテキスト形式で作成するプログラムをサンプルとしてストラテジーパターンを解説します。サンプルの概要は次のとおりです。

  • Report(コンテキスト):レポートを表す
  • Formatter(抽象戦略):レポートの出力を抽象化したクラス
  • HTMLFormatter(具象戦略1):HTMLフォーマットでレポートを出力
  • PlaneTextFormatter(具象戦略2):PlanTextフォーマットでレポートを出力

まずイメージしやすい、HTML形式で出力するHTMLFormatterクラスとPlaneTextFormatterクラス、そしてその2つのクラスのインタフェースを規定するFormatterクラスを作成します。

# レポートの出力を抽象化したクラス(抽象戦略)
class Formatter:
  def output_report(self, title, text):
    raise 'Called abstract method !!'

# HTML形式に整形して出力(具体戦略)
class HTMLFormatter(Formatter):
  def output_report(self, report):
    print ("<html><head><title>"+ report.title + "</title></head><body>")
    for line in report.text:
      print("<p>"+ line +"</p>")
    print ('</body></html>')

# PlaneText形式(*****で囲う)に整形して出力(具体戦略)
class PlaneTextFormatter(Formatter):
  def output_report(self, report):
    print ("***** " +report.title + " *****")
    for line in report.text:
      print( line )

続いてレポートを表すReportクラスを作成します。このクラスにはformatterがあり、このformatterによって出力フォーマットを設定します。

# レポートを表す(コンテキスト)
class Report:
  def __init__(self, formatter):
    self.title = 'report title'
    self.text = ['最高', '順調', '普通']
    self.formatter = formatter
  def output_report(self):
    self.formatter.output_report(self)

コーディングは以上です。では結果を確認します。

html_report = Report( HTMLFormatter() )
html_report.output_report()
#<html><head><title>report title</title></head><body>
#<p>最高</p>
#<p>順調</p>
#<p>普通</p>
#</body></html>
plaintext_report = Report( PlaneTextFormatter() ) 
plaintext_report.output_report()
#***** report title *****
#最高
#順調
#普通

Reportクラス内のformatterがレポートの出力を委譲されています。
上の結果からformatterをスイッチすれば出力形式(戦略)を変更させることができるのを確認できました。

ちなみに、ここにあるFormatterクラスはインタフェースを規定するだけのクラスですので、Pythonらしく書くなら不要です。(ダック・タイピング哲学)

ストラテジの注意点

  • コンテキストとストラテジ間のインターフェイスがストラテジの種類の増加を妨げないようにする
  • コンテキストの変更がストラテジに影響を与えないようにする

コンテキストからストラテジへのデータの渡し方は、

(1) ストラテジメソッドを呼び出すときに適切なデータを渡す
(2) コンテキストへの参照をストラテジに渡すといった方法があります。

これを適切に選択してストラテジの種類を増やすことを阻害しないようにしてください。