👻

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

2023/03/17に公開約4,200字

概要

以下で書いたTemplate Methodの続き。
https://zenn.dev/nabe3/articles/636ad2d5a2a7c3

ラスオルセン著「Rubyによるデザインパターン」の第四章Strategy
を読んで整理していく。

過去に書いた関連記事は以下を参照

例題

Template Methodはいくつか欠点がある。

それは、継承をベースにしていること。

  • サブクラスはスーパークラスに依存すること。
  • サブクラスはスーパークラスを参照可能であること。

ストラテジパターンでは委譲を使って、同じ目的を持ったグループの
オブジェクト(ストラテジ)を定義する。
それぞれのストラテジは同じ処理を行うだけでなく、同じインターフェースを提供する。

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

以下の例ではoutput_reportがストラテジのインターフェースに該当する。

ストラテジ

class Fomatter
  def output_report(title, text)
    raise 'Abstract method called'
  end
end
 
class HTMLFormatter < Formatter
  def output_report(title, text)
    puts('<html>')
    puts('<head>')
    puts("<title>#{@title}</title>")
    puts('</head>')
    puts('<body>')
    text.each do |line|
      puts("<p>#{line}</p>")
    end
  end
  puts('</body>')
  puts('</html>')
end
	
class PlainTextFormatter < Formatter
  def output_report(title, text)
    puts("*** #{title} ***")
    text.each do |line|
      puts(line)
    end
  end
 end	

コンテキスト

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

ストラテジの使い方

Reportクラスのインスタンス生成時に引数にフォーマットオブジェクトを指定する。

report = Report.new(HTMLFormatter.new)
report.output_report
    
report.formatter = PlainTextFormatter.new
report.output_report

クラスの外部からはストラテジオブジェクトが同じに見えるため、取り替え可能。

上記のストラテジパターンのメリット、デメリットは以下がある。

メリット

  • 関心事を分離することができる。
  • 出力形式についての一切の責務と知識をReportクラスから取り除くことが可能。
  • 委譲と集約に基づいているので、継承に基づく方法よりもストラテジの切り替えが容易。

デメリット

  • コンテキストが持っている情報をストラテジが取得する手段を提供する必要がある。

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

  1. コンテクストがストラテジオブジェクトのメソッドを呼び出すときにストラテジが必要とするすべてを引数に渡す。
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(title, text)
    puts('*** #{title} ***')
    text.each do |line|
      puts(line)
    end
  end
end

引き渡すデータが複雑で大量な場合は使われる保証もなく複雑なデータを大量に渡すことになる

  1. ストラテジにコンテキスト自身(self)を渡して、コンテキストからプロパティを取得する。
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
class Fomatter
  def output_report(context)
    raise 'Abstract method called'
  end
end
     
class HTMLFormatter < Formatter
  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
  end
  puts('</body>')
  puts('</html>')
end

データの流れはシンプルになるが、コンテキストとストラテジ間の結合度が上がる。

Procを使って書き直す

なぜProcベースで書き直すのか?

  • ストラテジの数分クラスを作成する必要がなくなる。
  • なにもないところからメソッドに対してコードブロックを渡すだけで、ストラテジを作れるようになる。

コンテキスト

class Report
  attr_reder : title, :text
  attr_accessor : formatter
  	
  def initialize(&formatter)
    @title = '月次報告'
    @text = ['順調', '最高の調子']
    @formatter = formatter
  end
  	
  def output_report
    @formatter.call(self)
  end

ストラテジ

HTML

HTML_FORMATTER = lambda do |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
	
report = Report.new &HTML_FORMATTER
report.output_report

TEXT

report = Report.new do |context|
  puts('***** #{context.title} *****'
  context.text.each do |line|
    puts(line)
  end
end
  
report.output_report

まとめ

  • Procに対して呼べるメソッドはcallのみのため、シンプルなストラテジであれば有効に働く
  • コンテキストとストラテジオブジェクト間に誤ったインターフェースを設計しないようにする。

Discussion

ログインするとコメントできます