Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
30
Help us understand the problem. What is going on with this article?

More than 5 years have passed since last update.

@kidach1

Rubyによるデザインパターン【Strategy】-取り替え可能パーツ群を戦略的に利用せよ-

概要

Rubyによるデザインパターン第4章。
Strategy Pattern。

Rubyによるデザインパターン5原則に則って理解する。

どんなパターンか

抽象的な処理と具体的な処理を分離することで、
変化に強い構造を実現する。

→その目的はTemplate Methodとほぼ同等

Template Methodの課題

しかし、Template Methodには欠点がある。それは、
継承をベースにしていること。

継承の欠点
http://qiita.com/kidachi_/items/4b63de9ad5a97726c50c#2-3

  • スーパークラスの振る舞いの変更は、サブクラスの振る舞いを変える
  • サブクラスはスーパークラスの中身を覗くことが出来る

そこでStrategyでは、
変化しやすいコードの塊を抽出し、
全く別のクラスに閉じ込める。

つまり、集約を使う。

異なるオブジェクトからアルゴリズムを引き出す

同じ目的を持った一群のオブジェクトをストラテジ(群)と捉える。
ストラテジ群は、そのすべてが全く同じインターフェイスを提供する。

コンテキスト(ストラテジを利用する側)は、各ストラテジを
取り替え可能パーツとして扱うことが出来るようになる。

→関心の分離 を実現する。

実装

コンテキスト(ストラテジを利用する側)

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

ストラテジ1(フォーマット:HTML)

class HTMLFormatter
  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
    puts('  </body>')
    puts('</html>')
  end
end

ストラテジ2(フォーマット:PlainText)

class PlainTextFormatter
  def output_report(context)
    puts("*** #{context.title} ***")
    context.text.each do |line|
      puts(line)
    end
  end
end

ストラテジの活用

[9] pry(main)> report = Report.new(HTMLFormatter.new)
[10] pry(main)> report.output_report
<html>
  <head>
    <title>月次報告</title>
  </head>
  <body>
    <p>最高!</p>
    <p>順調</p>
    <p>普通</p>
  </body>
</html>

[12] pry(main)> report.formatter = PlainTextFormatter.new
[13] pry(main)> report.output_report
*** 月次報告 ***
最高!
順調
普通

Template MethodにしてもStrategyにしても、
アルゴリズムの切り替えを1箇所(から多くても数箇所)に集中できる。

コンテキストとストラテジ間でのデータ共有

2者のデータ共有の方法としては2パターン存在する。

  1. ストラテジへ、コンテキスト自体の参照(self)を渡す
  2. ストラテジ側で使う必要のあるコンテキスト側のデータは、全て引数として渡す

上の例では、1を用いているが、2のパターンも見てみる。

ストラテジ側で使う必要のあるコンテキスト側のデータは、全て引数として渡す

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_report(title, text)  # (オブジェクトではなく)値を受け取る
    puts("*** #{title} ***")
    text.each do |line|
      puts(line)
    end
  end
end
[3] pry(main)> report = Report.new(PlainTextFormatter.new)
[4] pry(main)> report.output_report
*** 月次報告 ***
最高!
順調
普通

メリット・デメリット

1. ストラテジへ、コンテキスト自体の参照(self)を渡す

メリット:データの流れがシンプルになる。
デメリット:コンテキストとストラテジの結合度が上がってしまう。

2. ストラテジ側で使う必要のあるコンテキスト側のデータは、全て引数として渡す

メリット:コンテキストとストラテジの分離は保てる
デメリット:渡すデータの数によっては、複雑で大量になる可能性がある。

→状況に応じて適切なものを利用すること。

Procやブロックでストラテジを作る

ここまで、コンテキストとストラテジの関係を、
「オブジェクトとオブジェクト(の分離)」として捉えてきたが、
オブジェクトだけでなく、「コード」レベルでも、
分離し、持ち回すことが出来ないだろうか?

→ Proc/ブロックで実現する。

※ Procやブロックの基礎は以下を参照。
[Ruby基礎] ブロックとProcをちゃんと理解する
http://qiita.com/kidachi_/items/15cfee9ec66804c3afd2

コンテキスト2(ストラテジを利用する側)

コンテキスト初期化時に渡されてくるストラテジは、(クラスではなく)ブロックを期待する。

class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(&formatter)  # 渡されてくるブロックをproc化
    @title = '月次報告'
    @text = ['最高!', '順調', '普通']
    @formatter = formatter
  end

  def output_report
    @formatter.call(self)  # Procをcall
  end
end

ストラテジ3(フォーマット:PlainText、タイプ:ブロック)

report = Report.new do |context|
  puts("*** #{context.title} ***")
  context.text.each do |line|
    puts(line)
  end
end
[11] pry(main)> report.output_report
*** 月次報告 ***
最高!
順調
普通

ストラテジ4(フォーマット:PlainText、タイプ:Proc)

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

report = Report.new(&PLAIN_TEXT_FORMATTER)  # procをブロック化して渡す

[15] pry(main)> report.output_report
*** 月次報告 ***
最高!
順調
普通

procやブロックを活用することで、手軽にストラテジを作ることが出来た。

まとめ

Strategyパターンにより、

  • 変わるもの(ストラテジ)と変わらないもの(コンテキスト)を分離して、変化に強い構造へ
  • 継承より集約を実現
  • ブロックによるストラテジも可能

以下へ続く

【Observer】-本日のニュースをお届けします-
http://qiita.com/kidachi_/items/ce18d2a926c558159689

30
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
30
Help us understand the problem. What is going on with this article?