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


pythonとデザインパターンの学習のため、記事をまとめようと思いました。
デザインパターンの最初はTemplateパターンです。

環境

python3

参考

@kidach1 さん、まるパクリさせてもらいますm(__)m(ソースのみ修正 ruby→python)
参考「Rubyによるデザインパターン」【Template Method】-テンプレは準備した、あとはお好きに-
https://qiita.com/kidach1/items/7c2a80bfc8a87a05051f
この記事はPythonです

どんなパターンか

ソフトウェアには変化がつきものである。
その変化の度に、広範囲な修正が入っているようではとても対応できない。

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

そのための手段の一つがTemplate Method。

骨子は以下2種のクラスの利用。

骨格となるメソッドを持った抽象基底クラス(テンプレートクラス)
処理の詳細を詰める具象クラス(サブクラス)

ひどいコード

あるレポートを様々なフォーマット(HTMLやPlainText)で出力するReportクラス

template.py
#!/usr/bin/python
# coding: utf-8 

class Report:  
  def __init__(self):
    self.title = '月次報告'
    self.text = ['最高!', '順調', '普通']

  def output_report(self, format):
    if ( format == 'plain' ):
      print("*** "+ self.title + " ***")
    elif (format == 'html'):
      print('<html>')
      print('  <head>')
      print("    <title>"+ self.title +"</title>")
      print('  </head>')
      print('  <body>')
    else:
      raise ValueError("Unknown format")

    for elem in self.text:
      if( format == 'plain'):
        print(elem)
      else:
        print("    <p>"+ elem +"</p>")

    if (format == 'html'):
      print('  </body>')
      print('</html>')

report = Report()
report.output_report("plain")
report.output_report("html")

フォーマットがHTMLなのかPlainTextなのかによって、
都度if分で処理を分けている。
フォーマットがさらに増えたらどうなるのか・・

ということで、解決策を考える。

パターンの適用によるリファクタリング

変わるものと変わらないものを分離する

基本的な処理の流れ
・テンプレート(変わらないもの)をフォーマット(変わるもの)に依存させるべきではない。
・前者は抽象的な基底クラスに、後者は具象サブクラスに定義する。

つまり

「基本的な処理の流れ」である、

1.特定フォーマットに必要なヘッダ情報を出力する。
2.タイトルを出力する。
3.レポート本体(body)を出力する。
4.フォーマットに要求される残りの要素を出力する。

これらを「テンプレ」として抽象基底クラスに定義し、残りはサブクラスへ切り出す。

実装

抽象基底クラス

class Report:
  def __init__(self):
    self.title = '月次報告'
    self.text = ['最高!', '順調', '普通']

  def output_report(self):
    self.output_start()
    self.output_title()
    self.output_body_start()
    for line in self.text:
      self.output_line(line)
    self.output_body_end()
    self.output_end()

  def output_start(self):
    pass

  def output_title(self):
    self.output_line(self.title)

  def output_body_start(self):
    pass

  def output_line(self, line):
    raise 'Called abstract method: output_line'

  def output_body_end(self):
    pass

  def output_end(self):
    pass

output_start, output_title, output_body_start, output_body_end, output_endのように
具象クラスによってオーバーライドできる(不要ならしなくてもよい)メソッドのことを
フックメソッドと呼ぶ。

一方でoutput_lineは、直接呼び出すと例外を発生させる。つまり、
具象クラスによるオーバーライドを強要している。

→output_lineは、「各フォーマットごとに処理が異なる=より変化しやすい処理」
という想定があるため。

具象サブクラス1(フォーマット:HTML)

class HTMLReport(Report): 
  def output_start(self):
    print('<html>')

  def output_title(self):
    print('  <head>')
    print("    <title>"+ self.title + "</title>")
    print('  </head>')

  def output_body_start(self):
    print('  <body>')

  def output_line(self, line):
    print("    <p>"+ line +"</p>")

  def output_body_end(self):
    print('  </body>')

  def output_end(self):
    print('</html>')

html_report = HTMLReport()
html_report.output_report()

#<html>
#  <head>
#    <title>月次報告</title>
#  </head>
#  <body>
#    <p>最高!</p>
#    <p>順調</p>
#    <p>普通</p>
#  </body>
#</html>

具象サブクラス2(フォーマット:PlainText)

class PlainTextReport(Report):
  def output_title(self):
    print("*** "+ self.title + " ***")

  def output_line(self, line):
    print(line)

plain_repot = PlainTextReport()
plain_repot.output_report()

#*** 月次報告 ***
#最高!
#順調
#普通

まとめ

TemplateMethodパターンにより、

変わるもの(具象サブクラス)と変わらないもの(テンプレート)を分離して、変化に強い構造へ。

kidach1さんまるパクリですみませんm(__)m