とんちゃんといっしょ

Cloudに関する技術とか日常とかについて書いたり書かなかったり

Storategyパターン

明日の読書会の担当なのに資料作ってない。
\(^o^)/オワタ


そうも言ってられないので作れるところまで作る。

背景

Template Methodは継承で成り立ってる → 親クラスに依存してしまう
Templateを決定したあとに方法を変更 → 全体を書き換える必要がある

委譲で回避

  • 継承より委譲
    • Templateの種類を変えるたびにサブクラスを作るのをやめる
    • TemplateをMethodからクラスに変えてしまう
class Formatter
  def output report(title, text)
    # RubyにはAbstractクラスは無いがエラーを返す仕組みにして対応
    raise 'Abstract method called' 
  end
end

class HTMLFormatter < Formatter
  def  output report(title, text)
    # HTMLを出力するコードを書く
    puts('<html>')
  end 
end

class PlainTextFormatter < Formatter
  def  output report(title, text)
    # PlainTextを出力するコードを書く
    puts("**** #{title} ****")
  end 
end

出力フォーマットをReportクラスから取り除いたのでP69のReportクラスと比べてシンプルに

class Report
  attar_reader :title, :text
  attr_accesseor :formatter

  def initialize(formatter)
    @title = "タイトル"
    @text = ["Sample1", "Sample2"]
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(@title, @text)
  end
end

Reportクラスを使用する場合は利用したいFormatterを
Reportクラスのコンストラクタ引数に入れる。

# Report.new(利用したいFormatter.new)
report = Report.new(HTMLFormatter.new)

report.output_report

アルゴリズムを別のオブジェクトとして扱う → Strategy(同じ目的を持った一群のオブジェクト)
Strategyは同一のインターフェイスを持つ → 利用者は親となるStrategyを知っていればいい

Strategyのメリット

  • 処理の切り出しが行える → 変更や保守が容易に
  • 委譲と集約に基づく → 継承より実行時の切り替えが楽
  • Template Methodと同様に利用変更の切り替え部分が数箇所に集中
    • Template Method:サブクラスの選択時に決定
    • Strategy:実行時に選択して決定
  • 利用者とStrategyが別クラス → ○データ分離が行える ×データのやり取りが手間

×の解決方法としてStrategyに利用者を渡してしまう

class Report
  attar_reader :title, :text
  attr_accesseor :formatter

  def initialize(formatter)
    @title = "タイトル"
    @text = ["Sample1", "Sample2"]
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self)
  end
end

class HTMLFormatter < Formatter
  def  output report(context)
    # HTMLを出力するコードを書く
    puts('<html>')
    # title => context.title, text => context.textでアクセスするように変更
  end 
end

メリット:データの流れを単純化する
デメリット:利用者とStrategyの結合度が上がる

Rubyっぽく変更

同じインターフェイスを持って同じような動きをする = ダックタイピング
ダックタイピングの考え方からするとFormaterクラスはらないので削除

class HTMLFormatter < Formatter
  def  output report(title, text)
    # HTMLを出力するコードを書く
    puts('<html>')
    # title => context.title, text => context.textでアクセスするように変更
  end 
end

class PlainTextFormatter < Formatter
  def  output report(title, text)
    # PlainTextを出力するコードを書く
    puts("**** #{title} ****")
    # title => context.title, text => context.textでアクセスするように変更
  end 
end

class Report
  attar_reader :title, :text
  attr_accesseor :formatter

  def initialize(formatter)
    @title = "タイトル"
    @text = ["Sample1", "Sample2"]
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(self)
  end
end

Procを使ってもっと簡単に

Procオブジェクト = コードの塊
生成にはlambdaかProc.newを使う
呼び出しにはProc#callか[]を使う。

hoge = lambda{puts "hoge"}
hoge.call # => hoge
hoge[] #=> hoge

huga = Proc.new{puts "huga"}
huga.call # => huga
huga[] # => huga


#引数あり
hoge = lambda{|x,y| x+y}
hoge.call(1, 2) # => 3
hoge[1, 2] #=> 3

huga = Proc.new{|x,y| x+y}
huga.call(1, 2) # => 3
huga[1, 2] # => 3

Procオブジェクトをメソッドの引数に渡す

def sample_method
  puts "ここからブロックを実行"
  yield
  puts "ここまでブロックを実行"
end

sample_method do
  puts "ブロックが実行されています"
end

# => "ここからブロックを実行"
"ブロックが実行されています"
"ここまでブロックを実行"

# => こっちの方がすっきり
sample_method{puts "ブロックが実行されています"}

yieldに引数を設定しておくことも可能

def sample_method
  puts "ここからブロックを実行"
  yield("現在")
  puts "ここまでブロックを実行"
end

sample_method do |str|
  puts "#{str}ブロックが実行されています"
end

# => "ここからブロックを実行"
"現在ブロックが実行されています"
"ここまでブロックを実行"

# => こっちの方がすっきり
sample_method{|str|puts "#{str}ブロックが実行されています"}

ブロックに名前をつけて実行することも可能

# 関数の引数リストの最後に&をつけることで実現可能
def sample_method(&block)
  block.call("hoge")
end

sample{|x|puts x}
# => hoge

def sample_method(&block)
  block["hoge"]
end

sample{|x|puts x}
# => hoge

Procオブジェクトを利用したStrategy

  • Procオブジェクトを使うことでクラスを作る必要が無くなる
  • 何も無いところからStrategyが作れるようになる!
    • ただし、1つのメソッドで足りるときだけ有効
    • やることがいっぱいあるならクラスベースの方がいい
class Report
  attar_reader :title, :text
  attr_accesseor :formatter

  def initialize(&formatter) # &で
    @title = "タイトル"
    @text = ["Sample1", "Sample2"]
    @formatter = formatter
  end

  def output_report
    @formatter.call(self)
  end
end

HTML_FORMATTER = lambda do |context|
  #HTMLフォーマットで出力をする処理を記述
end

# Procオブジェクトを引数に渡すときは&が必要
report = Report.new &HTML_FORMMATER

利用所の注意