ストラテジーパターン
CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 45/57。
戦略パターンは、アルゴリズムの一部を独自のオブジェクトに切り出し、メインオブジェクトからそれに処理を委譲できるようにします。戦略を入れ替えることで、メインクラスを変更せずに動作を変更できます:
class Standard
def cost(kg)
5 + kg * 1.5
end
end
class Express
def cost(kg)
15 + kg * 3
end
end
class Shipment
attr_writer :strategy
def initialize(kg, strategy)
@kg = kg
@strategy = strategy
end
def cost
@strategy.cost(@kg)
end
end
s = Shipment.new(2, Standard.new)
puts s.cost
s.strategy = Express.new
puts s.cost出力:
8.0
21これらのストラテジーはメソッド名だけを共有しているため、costを持つオブジェクトならどれでも機能します。これもダックタイピングです。新しい配送方法を追加するには、Shipment内に別の分岐を追加するのではなく、クラスを1つ追加します。
class Pickup
def cost(_kg)
0
end
end
shipment.strategy = Pickup.new # Shipment は変更されないRubyでは、ストラテジーはラムダであることがよくあります。callに応答できるものなら、ラムダ、メソッドオブジェクト、独自のクラスのオブジェクトのいずれであっても、保存して呼び出すことができます。
class TextFormatter
def initialize(strategy)
@strategy = strategy
end
def format_all(words)
words.map { |w| @strategy.call(w) }.join(" ")
end
end
class Censor
def call(word)
word.size > 3 ? "*" * word.size : word
end
end
words = %w[ruby is great]
puts TextFormatter.new(->(w) { w.upcase }).format_all(words)
puts TextFormatter.new(:capitalize.to_proc).format_all(words)
puts TextFormatter.new(Censor.new).format_all(words)出力:
RUBY IS GREAT
Ruby Is Great
**** is *****プログラムの実行中に1つのオブジェクトの振る舞いを変更する必要がある場合、または変化が1つのステップである場合は、サブクラスよりもストラテジーを選びます。サブクラスでは、オブジェクトの存続期間全体にわたって振る舞いが固定されます:
# サブクラス化: 作成時に固定
ExpressShipment.new(2)
# ストラテジー: 後から変更可能
shipment = Shipment.new(2, Standard.new)
shipment.strategy = Express.newチャレンジ
簡単配送戦略と ShippingCalculator を完成させてください。Standard#cost(kg) は 5 に 1 kg あたり 1.50 を加えた値、Express#cost(kg) は 15 に 1 kg あたり 3 を加えた値、Pickup#cost(kg) は 0 です。計算機は戦略を指定して作成し、strategy= で戦略を変更できます。また、quote(kg) は戦略のクラス名を使って 2.5 kg by Express: 22.50 を返します。
用意されているコードは Standard から開始し、use express、use pickup、use standard、または quote 2.5 のコマンドを読み取り、すべての見積もりを出力します。
コードは standard.rb、express.rb、pickup.rb、および shipping_calculator.rb に記述します。main.rb には用意された入力・出力コードが含まれており、編集できません。
自分で試してみよう
require_relative 'standard'
require_relative 'express'
require_relative 'pickup'
require_relative 'shipping_calculator'
# 提供された入出力コード: そのままにしておく
input = []
while (line = gets)
input << line.chomp
end
strategies = { "standard" => Standard, "express" => Express, "pickup" => Pickup }
calc = ShippingCalculator.new(Standard.new)
input.each do |line|
cmd, arg = line.split
if cmd == "use"
calc.strategy = strategies[arg].new
else
puts calc.quote(arg.to_f)
end
end
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Rubyオンラインコンパイラ