Menu
CoddyTech

策略模式

本课属于 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 中再添加一个分支:

class Pickup
  def cost(_kg)
    0
  end
end

shipment.strategy = Pickup.new     # Shipment 保持不变

在 Ruby 中,策略通常是一个 lambda。任何响应 call 的对象都可以被存储和调用,无论它是 lambda、方法对象,还是你自己定义的类的对象:

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 *****

当一个对象必须在程序运行时改变其行为,或者变化只涉及一个步骤时,优先选择策略而不是子类。子类会在对象的整个生命周期内固定其行为:

# 通过子类实现:创建时就确定了
ExpressShipment.new(2)

# 使用策略:之后还可以更改
shipment = Shipment.new(2, Standard.new)
shipment.strategy = Express.new
challenge icon

挑战

简单

完成运输策略和 ShippingCalculator。Standard#cost(kg) 的费用为 5 加上每公斤 1.50,Express#cost(kg) 的费用为 15 加上每公斤 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
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Ruby 在线编译器