Murayama blog.

プログラミング教育なブログ

Strategy

Rubyによるデザインパターン生活2日目。
今日はStrategyパターンを学びます。

Rubyによるデザインパターン

Rubyによるデザインパターン



アルゴリズムの一部を変化したい場合の対処法としてTemplete methodを習いました。
Templete methodではベースとなるアルゴリズムを実装したスーパークラスを継承しなくてはなりません。
Rubyは単一継承しかできないので、継承を頻繁に利用することは好ましくありません。
可能であれば、何事も継承より委譲で実装すべきです。
そこで、Strategyパターンの出番ですよ、と。
Strategyパターンは委譲によりアルゴリズムを切り替えることができます。


それでは、コードを見ていきます。


登場するクラス

  • Reportクラス
    • レポート出力するクラス。出力処理の詳細アルゴリズムを委譲する。
  • HTMLReportFormatクラス
    • HTMLレポート出力アルゴリズムを実装したクラス。
  • PlainReportFormatクラス
    • プレーンレポート出力アルゴリズムを実装したクラス。


まずはReportクラス。

class Report
  def initialize(format)
    @title = "日記"
    @text = ["今日は大阪に行ってきました。", "天気は晴れでした。"]
    @format = format
  end
  
  def output_report
    @format.print(@title, @text)
  end
end


コンストラクタに出力フォーマット用のオブジェクトを受け取ります。
output_reportメソッドではフォーマット用のオブジェクトに処理を委譲しています。


次にHTMLReportFormatクラスとPlainReportFormatクラスです。

class HtmlReportFormat < Format
  def initialize()
  end
 
  def print(title, text)
    puts "<html>"
    puts "<head><title>#{@title}</title></head>"
    puts "<body>"
    text.each { |line| puts "<p>" + line + "</p>"}
    puts "</body>"
    puts "</html>"
  end
end
class PlainReportFormat < Format
  def initialize
  end
  
  def print(title, text)
    puts "***** #{title} *****"
    text.each { |line| puts line }
  end
end


実行するためのプログラムです。

puts "HTML REPORT--------"
report = Report.new(HtmlReportFormat.new)
report.output_report

puts "PLAIN REPORT--------"
report = Report.new(PlainReportFormat.new)
report.output_report


そんで実行結果です。

HTML REPORT--------
<html>
<head><title></title></head>
<body>
<p>今日は大阪に行ってきました。</p>
<p>天気は晴れでした。</p>
</body>
</html>
PLAIN REPORT--------
***** 日記 *****
今日は大阪に行ってきました。
天気は晴れでした。


とまぁ実行結果は前回と同じです。ちょっとソースを解説してみます。


RubyJavaと違って厳密な型定義は必要としません。
そのため、HTMLReportFormatクラス、PlainReportFormatクラスにスーパークラスを定義する必要がありません。*1


Strategyパターンでは、アルゴリズムを委譲する側のクラス(Reportクラス)をContextと呼び、
アルゴリズムを実装する側のクラス(HTMLReportFormatクラス、PlainReportFormatクラス)をStrategyと呼ぶようです。


また、今回はCotextからStrategyへパラメータを受け渡す際に、

  def output_report
    @format.print(@title, @text)
  end

のように2つの引数を渡しています。
これは次のように置き換えても構いません。*2

  def output_report
    @format.print(self)
  end

これはStrategyの引数に自身を示すオブジェクトを渡すことで、
引数をまとめちゃうテクニックです。
この場合のトレードオフとして、ContextとStrategy間の結合度が強くなってしまいます。


で、RubyのStrategyパターンのお話はここまでで前半戦終了です。
JavaのエンジニアならここまででOK!な気持ちになりますが、
RubyだとこんなふうにStrategyをコードブロックで渡すこともできます。


Reportクラスを次のように変更します。

class Report
  attr_reader :title, :text

  def initialize(&format)
    @title = "日記"
    @text = ["今日は大阪に行ってきました。", "天気は晴れでした。"]
    @format = format
  end
  
  def output_report
    @format.call(self)
  end
end

コンストラクタにコードブロックを引数にとるように修正しました。
すると、呼び出しもとでStrategyを作成することができます。


呼び出しもとプログラム

require "report"

#format
HTML_FORMATTER = lambda do |context|
  puts "<html>"
  puts "<head><title>#{context.title}</title></head>"
  puts "<body>"
  context.text.each { |line| puts "<p>" + line + "</p>"}
  puts "</body>"
  puts "</html>"  
end

PLAIN_FORMATTER = lambda do |context|
  puts "***** #{context.title} *****"
  context.text.each { |line| puts line }
end

puts "HTML REPORT--------"
report = Report.new &HTML_FORMATTER
report.output_report

puts "PLAIN REPORT--------"
report = Report.new &PLAIN_FORMATTER
report.output_report

なんとなくRubyっぽいプログラムになりました。
実行結果はさきほどと同じになります。
Rubyの場合は実行時にStrategyアルゴリズムを動的に作ることもできそうです。


勉強になりました。

*1:ダックタイピングというやつです。

*2:ぼくはこっちの方をよく使います。