Strategyパターンについて(デザインパターン)

【結論】

・Strategyパターンとは、デザインパターンの一つで、状況に応じてアルゴリズムを切り替える処理を記述する時に有効な実装手法

・ConcreteStrategy役に個々の具体的なアルゴリズムが記述されており、どれを使用するかはStrategy役が規定する

アルゴリズムを容易に切り替えられるようになる為、新しいアルゴリズムの拡張や、既存のアルゴリズムの改修がしやすくなる

【目次】

【本題】

Strategyパターンについて

Strategyパターンは、デザインパターンの一種です。

Strategyは「戦略」という意味を持ちますが、プログラミングで言えばアルゴリズムがそれに相当します。

Strategyパターンにおいては、アルゴリズム(戦略)を状況に応じて切り替える実装方法になります。

構成

Strategyパターンは、Strategy(抽象戦略)とConcreteStrategy(具体戦略)とContext(文脈)の3つで構成されています。

ConcreteStrategy(具体戦略)に、具体的なアルゴリズムがプログラミングされています。

Strategy(抽象戦略)で、ConcreteStrategy(具体戦略)のどれを利用するのかを定めます。

Context(文脈)が、実際にStrategy(抽象戦略)を利用して、ConcreteStrategy(具体戦略)のアルゴリズムを実行します。

メリット

IF文などでメソッド内で条件分岐させる方法を採ると、アルゴリズムが増えるほどコード量が多くなり、可読性が落ちてしまいます。

また、アルゴリズムを追加・改良するにしても、他のアルゴリズムに影響を及ぼさない様に細心の注意を払う必要があります。

Strategyパターンを利用すれば、アルゴリズムを他の部分と分離することが出来ます。

これにより、簡単にアルゴリズムの追加・改良を行うことが可能になります。

サンプルコード

以下は、映画の各タイトルに3つの評価点が与えられており、それらをランキング形式で出力するコードです。

ランキングは「合計点」「最高点」「平均点」の3つのアルゴリズムから切り替えて出力できるようになっています。

コード

class Result
  attr_reader :scores
  attr_accessor :sort_type

  def initialize(scores, sort_type)
    @scores = scores
    @sort_type = sort_type
  end

  def output_ranking
    @sort_type.output_ranking(self)
  end
end

class TotalScoreSort
  def output_ranking(result)
    output = {}
    result.scores.each do |s|
      output[s[:title]] = [s[:judge1], s[:judge2], s[:judge3]].sum
    end
    puts '++++++ランキング(合計スコア)++++++'
    Hash[output.sort_by{ |_, v| -v }].each.with_index(1) do |o, i|
      puts "#{i}位:#{o[0]}(スコア:#{o[1]}"
    end
  end
end

class HighestScoreSort
  def output_ranking(result)
    output = {}
    result.scores.each do |s|
      output[s[:title]] = [s[:judge1], s[:judge2], s[:judge3]].max
    end
    puts '++++++ランキング(最高スコア)++++++'
    Hash[output.sort_by{ |_, v| -v }].each.with_index(1) do |o, i|
      puts "#{i}位:#{o[0]}(スコア:#{o[1]}"
    end
  end
end

class AverageScoreSort
  def output_ranking(result)
    output = {}
    result.scores.each do |s|
      output[s[:title]] = ([s[:judge1], s[:judge2], s[:judge3]].sum / 3.0).round(1)
    end
    puts '++++++ランキング(平均スコア)++++++'
    Hash[output.sort_by{ |_, v| -v }].each.with_index(1) do |o, i|
      puts "#{i}位:#{o[0]}(スコア:#{o[1]}"
    end
  end
end

入力

scores  = [
  {title: 'ダークナイト', judge1: 8, judge2: 6, judge3: 7},
  {title: 'トイストーリー4', judge1: 1, judge2: 3, judge3: 0},
  {title: '容疑者Xの献身', judge1: 5, judge2: 4, judge3: 4},
  {title: 'コマンドー', judge1: 2, judge2: 9, judge3: 1},
  {title: 'インセプション', judge1: 7, judge2: 5, judge3: 6},
  {title: 'LIFE!', judge1: 5, judge2: 6, judge3: 3}
]

result = Result.new(scores, TotalScoreSort.new)
result.output_ranking

result = Result.new(scores, HighestScoreSort.new)
result.output_ranking

result = Result.new(scores, AverageScoreSort.new)
result.output_ranking

出力

++++++ランキング(合計スコア)++++++
1位:ダークナイト(スコア:21)
2位:インセプション(スコア:18)
3位:LIFE!(スコア:14)
4位:容疑者Xの献身(スコア:13)
5位:コマンドー(スコア:12)
6位:トイストーリー4(スコア:4)
++++++ランキング(最高スコア)++++++
1位:コマンドー(スコア:9)
2位:ダークナイト(スコア:8)
3位:インセプション(スコア:7)
4位:LIFE!(スコア:6)
5位:容疑者Xの献身(スコア:5)
6位:トイストーリー4(スコア:3)
++++++ランキング(平均スコア)++++++
1位:ダークナイト(スコア:7.0)
2位:インセプション(スコア:6.0)
3位:LIFE!(スコア:4.7)
4位:容疑者Xの献身(スコア:4.3)
5位:コマンドー(スコア:4.0)
6位:トイストーリー4(スコア:1.3)

参考情報

ストラテジ(Strategy) | Ruby デザインパターン | 酒と涙とRubyとRailsと

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

10. Strategy パターン | TECHSCORE(テックスコア)