👻
Rubyによるデザインパターン~Strategy~
概要
以下で書いたTemplate Method
の続き。
ラスオルセン著「Rubyによるデザインパターン」の第四章Strategy
を読んで整理していく。
過去に書いた関連記事は以下を参照
例題
Template Methodはいくつか欠点がある。
それは、継承をベースにしていること。
- サブクラスはスーパークラスに依存すること。
- サブクラスはスーパークラスを参照可能であること。
ストラテジパターン
では委譲を使って、同じ目的を持ったグループの
オブジェクト(ストラテジ)を定義する。
それぞれのストラテジは同じ処理を行うだけでなく、同じインターフェースを提供する。
コンテキスト(ストラテジを利用する側)は各ストラテジを取り替え可能なパーツとして
扱うことができる。
以下の例ではoutput_report
がストラテジのインターフェースに該当する。
ストラテジ
class Fomatter
def output_report(title, text)
raise 'Abstract method called'
end
end
class HTMLFormatter < Formatter
def output_report(title, text)
puts('<html>')
puts('<head>')
puts("<title>#{@title}</title>")
puts('</head>')
puts('<body>')
text.each do |line|
puts("<p>#{line}</p>")
end
end
puts('</body>')
puts('</html>')
end
class PlainTextFormatter < Formatter
def output_report(title, text)
puts("*** #{title} ***")
text.each do |line|
puts(line)
end
end
end
コンテキスト
class Report
attr reader :title, :text
attr_accessor :formatter
def initialize(formatter)
@title = '月次報告'
@text = ['順調', '最高の調子']
@formatter = formatter
end
def output_report
@formatter.output_report(@title, @text)
end
end
ストラテジの使い方
Reportクラスのインスタンス生成時に引数にフォーマットオブジェクトを指定する。
report = Report.new(HTMLFormatter.new)
report.output_report
report.formatter = PlainTextFormatter.new
report.output_report
クラスの外部からはストラテジオブジェクト
が同じに見えるため、取り替え可能。
上記のストラテジパターン
のメリット、デメリットは以下がある。
メリット
- 関心事を分離することができる。
- 出力形式についての一切の責務と知識をReportクラスから取り除くことが可能。
- 委譲と集約に基づいているので、継承に基づく方法よりもストラテジの切り替えが容易。
デメリット
- コンテキストが持っている情報をストラテジが取得する手段を提供する必要がある。
コンテキストとストラテジ間のデータの共有方法
- コンテクストがストラテジオブジェクトのメソッドを呼び出すときにストラテジが必要とするすべてを引数に渡す。
class Report
attr reader :title, :text
attr_accessor :formatter
def initialize(formatter)
@title = '月次報告'
@text = ['順調', '最高の調子']
@formatter = formatter
end
def output_report
@formatter.output_report(@title, @text)
end
end
class PlainTextFormatter
def output(title, text)
puts('*** #{title} ***')
text.each do |line|
puts(line)
end
end
end
引き渡すデータが複雑で大量な場合は使われる保証もなく複雑なデータを大量に渡すことになる
- ストラテジにコンテキスト自身(self)を渡して、コンテキストからプロパティを取得する。
class Report
attr reader :title, :text
attr_accessor :formatter
def initialize(formatter)
@title = '月次報告'
@text = ['順調', '最高の調子']
@formatter = formatter
end
def output_report
@formatter.output_report(self)
end
end
class Fomatter
def output_report(context)
raise 'Abstract method called'
end
end
class HTMLFormatter < Formatter
def output_report(context)
puts('<html>')
puts('<head>')
puts("<title>#{context.title}</title>")
puts('</head>')
puts('<body>')
context.text.each do |line|
puts("<p>#{line}</p>")
end
end
puts('</body>')
puts('</html>')
end
データの流れはシンプルになるが、コンテキストとストラテジ間の結合度が上がる。
Procを使って書き直す
なぜProcベースで書き直すのか?
- ストラテジの数分クラスを作成する必要がなくなる。
- なにもないところからメソッドに対してコードブロックを渡すだけで、ストラテジを作れるようになる。
コンテキスト
class Report
attr_reder : title, :text
attr_accessor : formatter
def initialize(&formatter)
@title = '月次報告'
@text = ['順調', '最高の調子']
@formatter = formatter
end
def output_report
@formatter.call(self)
end
ストラテジ
HTML
HTML_FORMATTER = lambda do |context|
puts('<html>')
puts('<head>')
puts("<title>#{context.title}</title>")
puts('</head>')
puts('<body>')
context.text.each do |line|
puts("<p>#{line}</p>")
end
puts('</body>')
puts('</html>')
end
report = Report.new &HTML_FORMATTER
report.output_report
TEXT
report = Report.new do |context|
puts('***** #{context.title} *****'
context.text.each do |line|
puts(line)
end
end
report.output_report
まとめ
- Procに対して呼べるメソッドはcallのみのため、シンプルなストラテジであれば有効に働く
- コンテキストとストラテジオブジェクト間に誤ったインターフェースを設計しないようにする。
Discussion