酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

ストラテジ Ruby 2.0.0 デザインパターン速攻習得 [Strategy][Design Pattern]

GoFのデザインパターン(Design Pattern)のストラテジー(Strategy)のRubyコードを使った紹介記事です。

ストラテジーパターンは、例えば5ステップの中の3ステップが異なったAとBがあり、このAとBをスイッチしたい時に使えるパターンです。


ストラテジの構成

ストラテジは以下の3つのオブジェクトによって構成されます。

* コンテキスト(Context):ストラテジの利用者
* 抽象戦略(Strategy):同じ目的をもった一連のオブジェクトを抽象化したもの
* 具象戦略(ConcreteStrategy):具体的なアルゴリズム

ストラテジのアイデアは、コンテキストが「委譲」によってアルゴリズムを交換できるようにすることです。委譲とは、ある機能を持つオブジェクトを生成してオブジェクトに処理を依頼することです。

ストラテジのメリット

* 使用するアルゴリズムに多様性を持たせることができる
* コンテキストと戦略を分離することでデータも分離できる
* 継承よりもストラテジを切り替えるのが楽

サンプルソース1

レポートをHTML形式とプレーンテキスト形式で作成するプログラムをサンプルとしてストラテジーパターンを解説します。サンプルの概要は次の通りです。

* Report(コンテキスト):レポートを表す
* Formatter(抽象戦略):レポートの出力を抽象化したクラス
* HTMLFormatter(具象戦略1):HTMLフォーマットでレポートを出力
* PlaneTextFormatter(具象戦略2):PlanTextフォーマットでレポートを出力

まずイメージしやすい、HTML形式で出力するHTMLFormatterクラスPlaneTextFormatterクラス、そしてその2つのクラスのインターフェイスを規定するFormatterクラスを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# レポートの出力を抽象化したクラス(抽象戦略)
class Formatter
  def output_report(title, text)
    raise 'Called abstract method !!'
  end
end

# HTML形式に整形して出力(具体戦略)
class HTMLFormatter < Formatter
  def output_report(report)
    puts "<html><head><title>#{report.title}</title></head><body>"
    report.text.each { |line| puts "<p>#{line}</p>" }
    puts '</body></html>'
  end
end

# PlaneText形式(*****で囲う)に整形して出力(具体戦略)
class PlaneTextFormatter < Formatter
  def output_report(report)
    puts "***** #{report.title} *****"
    report.text.each { |line| puts(line) }
  end
end

続いてレポートを表すReportクラスを作成します。このクラスにはformatterがあり、このformatterによって出力フォーマットを設定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# レポートを表す(コンテキスト)
class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(formatter)
    @title = 'report title'
    @text = %w(text1 text2 text3)
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self)
  end
end

コーディングは以上です。では結果を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
report = Report.new(HTMLFormatter.new)
report.output_report
#<html><head><title>report title</title></head><body>
#<p>text1</p>
#<p>text2</p>
#<p>text3</p>
#</body></html>

report.formatter = PlaneTextFormatter.new
report.output_report
#***** report title *****
#text1
#text2
#text3

Reportクラス内のformatterがレポートの出力を委譲されています。 上の結果からformatterをスイッチすれば出力形式(戦略)を変更させることができるのを確認できました。

ちなみに、ここにあるFormatterクラスはインターフェースを規定するだけのクラスなので、Rubyらしく書くなら不要です。(ダック・タイピング哲学)

サンプルソース2

先ほどのソースコードをProcオブジェクト(コードブロック)を使って置き換えると次のようになります。

Procオブジェクトは、コードのかたまりを保持するオブジェクトです。lambdaメソッドが良く使われます

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(&formatter)
    @title = 'report title'
    @text = %w(text1 text2 text3)
    @formatter = formatter
  end

  def output_report
    @formatter.call(self)
  end
end

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

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

コーディングは以上です。では結果を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
report = Report.new(&HTML_FORMATTER)
report.output_report
#<html><head><title>report title</title></head><body>
#<p>text1</p>
#<p>text2</p>
#<p>text3</p>
#</body></html>

report.formatter = PLANE_TEXT_FORMATTER
report.output_report
#***** report title *****
#text1
#text2
#text3

先ほどよりもRubyらしいコードで同様の結果を得ることができました。

このサンプルソースはGitHubにも置いています。

サンプルソース(GitHub)

ストラテジの注意点

* コンテキストとストラテジ間のインターフェイスがストラテジの種類の増加を妨げないようにする
* コンテキストの変更がストラテジに影響を与えないようにする

コンテキストからストラテジへのデータの渡し方は、(1) ストラテジメソッドを呼び出すときに適切なデータを渡す (2) コンテキストへの参照をストラテジに渡すといった方法があります。これを適切に選択してストラテジの種類を増やすことを阻害しないようにしてください。

Special Thanks

RubyでStrategyパターン - 暁 [stfuawsc]

RubyでStrategyパターン - tbpg’s programming memo

Amazon.co.jp: Rubyによるデザインパターン: Russ Olsen, ラス・オルセン, 小林 健一, 菅野 裕, 吉野 雅人, 山岸 夢人, 小島 努: 本

変更来歴

12/10 09:00 GitHubへのサンプルソースの設置。導入文の修正
12/11 00:00 書籍へのリンクをAmazon アフィリエイトに変更
12/16 18:05 ソースコードへの説明を追加
06/20 14:50 Ruby2.0.0対応、読みづらい部分を修正

おすすめの書籍