Template Method パターン

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

変化しない部分は基底クラスに書き、
変化する部分は抽象メソッドにしてサブクラスに任せようというもの。

例:レポートを出力するクラス。

HTMLを出力するクラスを作ったら、後からプレーンテキストでも出力してくれと言われてしまった。
とりあえず、フォーマットを引数で指定するようにしたら・・・

#template_method1.py
class Report:
   def __init__(self):
       self.title = u"月次報告"
       self.text = [u"順調", u"最高"]
  
   def output_report(self, format):
       if format == "plain":
           print("*** %s ***"%(self.title))
       elif format == "html":
           print("<html>")
           print("<head>")
           print("<title>%s</title>"%(self.title,))
           print("</head>")
           print("<body>")
      
       for line in self.text:
           if format == "plain":
               print(line)
           else:
               print("<p>%s</p>"%(line,))
      
       if format == "html":
           print("</body>")
           print("</html>")

if __name__ == "__main__":
   report = Report()
   report.output_report("plain")
   report.output_report("html")


このプログラムの悪い点は、output_reportの中でフォーマット固有の処理が絡み合っていること。
もし、PostScript、CSV等とフォーマットが増えていったら・・・やってられません!
デザインパターンの原則「変わるものを変わらないものから分離する」に反しています。


上のソースでは、HTMLもプレーンテキストも、

ヘッダ情報を出力
   ↓
タイトルを出力
   ↓
本文を出力
   ↓
末尾の部分を出力


と言う処理は変わりません。そこで、オブジェクト指向の基本を使います。

抽象基底クラスを定義して、不変な処理は基底クラスに
フォーマット毎に異なる処理の詳細はサブクラスに任せるのです。

#template_method2.py
class Report(object):
   def __init__(self):
       self.title = u"月次報告"
       self.text = [u"順調", u"最高"]
  
   def output_report(self):
       self.output_start()
       self.output_head()
       self.output_body_start()
       self.output_body()
       self.output_body_end()
       self.output_end()
  
   def output_body(self):
       for line in self.text:
           self.output_line(line)
  
   def output_start(self):
       assert False, "called abstruct method output_start"

   def output_head(self):
       print(self.title)

   def output_body_start(self):
       assert False, "called abstruct method output_body_start"

   def output_line(self, line):
       assert False, "called abstruct method output_body"

   def output_body_end(self):
       assert False, "called abstruct method output_body_end"

   def output_end(self):
       assert False, "called abstruct method output_end"

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

   def output_head(self):
       print("<head>")

       print("<title>%s</title>"%(self.title,))

       print("</head>")
      
   def output_body_start(self):
       print("<body>")

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

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

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

class PlainTextReport(Report):
   def output_start(self):
       pass

   def output_head(self):
       print("*** %s ***"%(self.title,))

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

   def output_body_end(self):
       pass
      
   def output_end(self):
       pass
  
if __name__ == "__main__":
   report = PlainTextReport()
   report.output_report()
  
   report = HTMLReport()
   report.output_report()

これでコードがすっきりした上、新しいフォーマットへの対応も容易になったはずです。

ところで、output_body_start等はHTMLReportではオーバーライドしたメソッドで何らかの出力をしていますが、PlainTextReportでは、何もしていません。むしろ、文書の開始を表すoutput_startなど、何もしないメソッドでオーバーライドすることの方が多いはずです。

一々passとだけ書くのは面倒くさいので、基底クラスに初めからそう書いたほうがいいでしょう。

#template_method3.py
class Report(object):
   def __init__(self):
       self.title = u"月次報告"
       self.text = [u"順調", u"最高"]
  
   def output_report(self):
       self.output_start()
       self.output_head()
       self.output_body_start()
       self.output_body()
       self.output_body_end()
       self.output_end()
  
   def output_body(self):
       for line in self.text:
           self.output_line(line)
  
   def output_start(self):
       pass

   def output_head(self):
       print(self.title)

   def output_body_start(self):
       pass

   def output_line(self, line):
       assert False, "called abstruct method output_body"

   def output_body_end(self):
       pass

   def output_end(self):
       pass

class PlainTextReport(Report):
   def output_head(self):
       print("*** %s ***"%(self.title,))

      
   def output_line(self, line):
       print(line)
  
if __name__ == "__main__":
   report = PlainTextReport()
   report.output_report()
  
   report = Report()
   report.output_report()

output_startのように、派生クラスでオーバーライドできる非抽象メソッドを、
フックメソッドと呼びます。

ちなみに、Pythonはその性質上、インターフェイスとか抽象メソッドを
ズバリ定義する構文というものはありませんでした。
上の例では assert 文があるので、Reportクラスをインスタンス化して呼び出すと例外が投げられますが。たぶんあまり必要になることがなかったのでしょう。

しかし、大規模開発では必要な事もあるため、Python 2.6/3.0 からは、
abcモジュールが追加されました。ちなみにabc は Abstract Base Classes の略です。ジョークではありません。この程度の例では必要ない(むしろ、邪魔)のですが、一応、abcも使ってみました。

#template_method4.py
from abc import abstractmethod, ABCMeta #2.6以上限定
class Report(object):
   __metaclass__ = ABCMeta
   def __init__(self):
       self.title = u"月次報告"
       self.text = [u"順調", u"最高"]
  
   def output_report(self):
       self.output_start()
       self.output_head()
       self.output_body_start()
       self.output_body()
       self.output_body_end()
       self.output_end()
  
   def output_body(self):
       for line in self.text:
           self.output_line(line)
  
   @abstractmethod
   def output_start(self):pass
   @abstractmethod
   def output_head(self):pass
   @abstractmethod
   def output_body_start(self):pass
   @abstractmethod
   def output_line(self, line):pass
   @abstractmethod
   def output_body_end(self):pass
   @abstractmethod
   def output_end(self):pass
  
  
if __name__ == "__main__":
   report = Report()
   report.output_report()


これで、Report をインスタンス化しようとすると、

Traceback (most recent call last):
 File "d:\Owner\My Documents\Python\Rubyによるデザインパターン\template_method4.py",
line 37, in 
   report = Report()
TypeError: Can't instantiate abstract class Report with abstract methods output_body_end,
output_body_start, output_end, output_head, output_line, output_start

バッチリ怒ってくれます。