概要
Template Method Pattern
詳細
Template Method Patternは、アルゴリズムの大部分は同じだが
一部だけが異なる部分があるような場合に
・変わらない部分を基底クラスに
・変わる部分を複数の具象クラスに
する設計手法です。
(変わる部分を個別のクラスに実装するために基底クラスにフックメソッドを用意します。
基底クラスでは該当メソッドに NotImplementedError を上げるコードのみ実装します。
継承側で実装を忘れると、NotImplementedError が発生することになります。)
これにより、同じようなロジックをいくつものクラスに記述したり、
一つのクラスないが分岐だらけになってしまうような事態を避けることができます。
結果として、共通部の修正は基底クラスに閉じ
固有の修正は具象クラスに閉じるため修正しやすく、新たな具象クラスも追加しやすくなっています。
静的言語との違い
Javaを例にとると、Template Methodパターンを実装する場合は抽象クラスを利用します。
基底クラスでは共通部を具体的なメソッドとして実装し、固有部は抽象メソッドをフックメソッドとしてインターフェースだけ決めておきます。
具象クラスでは、抽象クラスを継承することでフックメソッドの実装を強要されます。
具象クラスですべてのフックメソッドを実装すると、Template Methodパターンが完成します。
Rubyは動的言語で特別なインターフェースの定義などは不要でメソッド名さえ合っていれば動作します。(ダックタイピングの特性)
ただし、フックメソッドが正しく実装されているかチェックするために NotImplementedError を実装しておくことが好ましいです。
サンプル仕様
株式会社HogeHogeの社内レポートを作成する。
レポートはHTMLとMarkdownの2種類です。
(HTMLはMarkdownから出せばいいような気もしますが気にしない。)
サンプル修正前その1
全て1メソッドに記述している場合
require 'test_toolbox' module HogeHogeInc module REPORT_TYPE HTML = "HTML" MARKDOWN = "Markdown" end class Report attr_reader :title, :contents, :type def initialize(title, contents, type = REPORT_TYPE::HTML) @title, @contents, @type = title, contents, type end def output_report if type == REPORT_TYPE::HTML title = <<-EOS <html> <head> <title>#{@title}</title> </head> EOS body = <<-EOS <body> #{contents} </body> </html> EOS else title = "# #{@title}\n" body = <<-EOS ## Report #{@contents} EOS end title + body end end end html_report = HogeHogeInc::Report.new("html title", "html contents" , HogeHogeInc::REPORT_TYPE::HTML) puts html_report.output_report dp_line __LINE__ markdown_report = HogeHogeInc::Report.new("html title", "html contents" , HogeHogeInc::REPORT_TYPE::MARKDOWN) puts markdown_report.output_report __END__ dp_line は tbpgr_utils gem の機能 詳しくは https://github.com/tbpgr/tbpgr_utils を参照。
出力
<html> <head> <title>html title</title> </head> <body> html contents </body> </html> --------------------|filename=|line=45|-------------------- # html title ## Report html contents
サンプル修正前その2
各メソッドを分割した場合。
サンプル1をリファクタリングしてメソッドを分割し、フックメソットの元となるメソッドを用意します。
これは次のTemplate Method適用への準備段階になります。
require 'test_toolbox' module HogeHogeInc module REPORT_TYPE HTML = "HTML" MARKDOWN = "Markdown" end class Report attr_reader :title, :contents, :type def initialize(title, contents, type = REPORT_TYPE::HTML) @title, @contents, @type = title, contents, type end def output_report report_title + report_contents end private def report_title if type == REPORT_TYPE::HTML <<-EOS <html> <head> <title>#{@title}</title> </head> EOS else "# #{@title}\n" end end def report_contents if type == REPORT_TYPE::HTML <<-EOS <body> #{contents} </body> </html> EOS else <<-EOS ## Report #{@contents} EOS end end end end html_report = HogeHogeInc::Report.new("html title", "html contents" , HogeHogeInc::REPORT_TYPE::HTML) puts html_report.output_report dp_line __LINE__ markdown_report = HogeHogeInc::Report.new("html title", "html contents" , HogeHogeInc::REPORT_TYPE::MARKDOWN) puts markdown_report.output_report __END__ dp_line は tbpgr_utils gem の機能 詳しくは https://github.com/tbpgr/tbpgr_utils を参照。
出力
<html> <head> <title>html title</title> </head> <body> html contents </body> </html> --------------------|filename=|line=55|-------------------- # html title ## Report html contents
サンプルTemplate Methodパターン適用1
共通部を HogeHogeInc::Report クラスに抽出。
個別部を HogeHogeInc::HtmlReport, HogeHogeInc::MarkdownReport クラスに抽出。
また、実装ミスの確認用にわざとフックメソッドを実装しない HogeHogeInc::NotImplementedReport も作成します。
require 'test_toolbox' module HogeHogeInc module REPORT_TYPE HTML = "HTML" MARKDOWN = "Markdown" end class Report attr_reader :title, :contents, :type def initialize(title, contents) @title, @contents = title, contents end def output_report report_title + report_contents end private def report_title fail NotImplementedError.new end def report_contents fail NotImplementedError.new end end class HtmlReport < Report def initialize(title, contents) @type = REPORT_TYPE::HTML super(title, contents) end private def report_title <<-EOS <html> <head> <title>#{@title}</title> </head> EOS end def report_contents <<-EOS <body> #{contents} </body> </html> EOS end end class MarkdownReport < Report def initialize(title, contents) @type = REPORT_TYPE::MARKDOWN super(title, contents) end private def report_title "# #{@title}\n" end def report_contents <<-EOS ## Report #{@contents} EOS end end class NotImplementedReport < Report def initialize(title, contents) @type = REPORT_TYPE::HTML super(title, contents) end end end html_report = HogeHogeInc::HtmlReport.new("html title", "html contents") puts html_report.output_report dp_line __LINE__ markdown_report = HogeHogeInc::MarkdownReport.new("html title", "html contents") puts markdown_report.output_report dp_line __LINE__ begin not_implemented_report = HogeHogeInc::NotImplementedReport.new("not_implemented_ title", "not_implemented_ contents") puts not_implemented_report.output_report rescue NotImplementedError => e puts e end __END__ dp_line は tbpgr_utils gem の機能 詳しくは https://github.com/tbpgr/tbpgr_utils を参照。
出力
<html> <head> <title>html title</title> </head> <body> html contents </body> </html> --------------------|filename=|line=85|-------------------- # html title ## Report html contents --------------------|filename=|line=90|-------------------- NotImplementedError
サンプルTemplate Methodパターン適用2
HogeHogeInc::Report クラスにtbpgr_utils gemのTemplateMethodableをincludeし、must_implに実装が必要なフックメソッドを列挙することで
def report_title(*args) fail NotImplementedError.new end def report_contents(*args) fail NotImplementedError.new end
と同等の動作をします。
ここではサンプルのため2メソッドしかフックメソッドがありませんが、実際にコーディングする際は大量のメソッドを定義することもよくあります。
そのような際に簡単にフックメソッドを定義できます。
フックメソッド + NotImplementedErrorの実装は退屈なボイラーテンプレートです。
メソッド名の宣言とNotImplementedErrorを投げるコードを延々定義するだけです。
この作業を簡単にするために TemplateMethodable は must_impl を利用して宣言的にフックメソッドを定義できるようになっています。
※ただし、汎用的な引数に対応できるように *args 限定の機能にしています。厳密に任意の数の引数に適用したい場合は利用できません。
また、同様にコンストラクタの定義 + メンバ変数への設定もボイラーテンプレートです。
これも、tbpgr_utils gemのAttributesInitializableをincludeし、attr_reader_initに実装がメンバ変数を列挙することで
attr_reader :title, :contents def initialize(options) @title = options[:title] @contents = options[:contents] end
と同等の動作をします。
以上を踏まえて実装内容は以下になります。
require 'test_toolbox' require "template_methodable" require 'attributes_initializable' module HogeHogeInc module REPORT_TYPE HTML = "HTML" MARKDOWN = "Markdown" end class Report include TemplateMethodable include AttributesInitializable must_impl :report_title, :report_contents attr_reader_init :title, :contents def output_report report_title + report_contents end end class HtmlReport < Report def initialize(title, contents) @type = REPORT_TYPE::HTML super({title: title, contents: contents}) end private def report_title <<-EOS <html> <head> <title>#{@title}</title> </head> EOS end def report_contents <<-EOS <body> #{contents} </body> </html> EOS end end class MarkdownReport < Report def initialize(title, contents) @type = REPORT_TYPE::MARKDOWN super({title: title, contents: contents}) end private def report_title "# #{@title}\n" end def report_contents <<-EOS ## Report #{@contents} EOS end end class NotImplementedReport < Report def initialize(title, contents) @type = REPORT_TYPE::HTML super({title: title, contents: contents}) end end end html_report = HogeHogeInc::HtmlReport.new("html title", "html contents") puts html_report.output_report dp_line __LINE__ markdown_report = HogeHogeInc::MarkdownReport.new("html title", "html contents") puts markdown_report.output_report dp_line __LINE__ begin not_implemented_report = HogeHogeInc::NotImplementedReport.new("not_implemented_ title", "not_implemented_ contents") puts not_implemented_report.output_report rescue NotImplementedError => e puts e end __END__ dp_line は tbpgr_utils gem の機能 詳しくは https://github.com/tbpgr/tbpgr_utils を参照。
出力
<html> <head> <title>html title</title> </head> <body> html contents </body> </html> --------------------|filename=|line=80|-------------------- # html title ## Report html contents --------------------|filename=|line=85|-------------------- NotImplementedError