Menu
CoddyTech

装饰器模式

本课属于 Coddy Ruby 学习旅程的“面向对象编程”单元。第 47 课,共 57 课。

装饰器模式会用另一个具有相同接口的对象包装一个对象,并在转发调用之前或之后添加一些内容。装饰器可以按任意顺序叠加:

class Coffee
  def cost
    2.0
  end

  def description
    "coffee"
  end
end

class Milk
  def initialize(drink)
    @drink = drink
  end

  def cost
    @drink.cost + 0.5
  end

  def description
    "#{@drink.description}, milk"
  end
end

order = Milk.new(Milk.new(Coffee.new))
puts format("%s: %.2f", order.description, order.cost)

输出:

coffee, milk, milk: 3.00

每个装饰器都会存储被包装的对象并将调用转发给它,因此可以用一个共享的基类来保存这部分逻辑。然后,每个具体装饰器只需重写它要修改的部分:

class Coffee
  def cost
    2.0
  end

  def description
    "coffee"
  end
end

class Extra
  def initialize(drink)
    @drink = drink
  end

  def cost
    @drink.cost
  end

  def description
    @drink.description
  end
end

class Sugar < Extra
  def cost
    super + 0.2
  end

  def description
    super + ", sugar"
  end
end

order = Sugar.new(Coffee.new)
puts format("%s: %.2f", order.description, order.cost)

输出:

coffee, sugar: 2.20

标准库中的 SimpleDelegator(require 'delegate')会将它未定义的所有方法转发给被包装的对象。接口较大时,这能让装饰器保持简短:

require 'delegate'

class Loud < SimpleDelegator
  def to_s
    super.upcase + "!"
  end
end

text = Loud.new("hello")
puts text
puts text.size
puts text.start_with?("he")

输出:

HELLO!
5
true

装饰器可以在运行时为单个对象添加行为,并且可以自由组合。使用子类时,像 MilkSugarCoffee 这样的每种组合都需要有自己的类:

Sugar.new(Milk.new(Coffee.new))      # 可以任意组合,顺序不限
Whip.new(Sugar.new(Coffee.new))

# 如果用子类:MilkCoffee、SugarCoffee、MilkSugarCoffee 等
challenge icon

挑战

简单

完成咖啡装饰器。Coffee 的价格是 2.00,描述为 coffee。Milk 会增加 0.50 和 , milk,Sugar 会增加 0.20 和 , sugar,Whip 会增加 0.70 和 , whip。这三个装饰器共用基类 Extra,该类会保存被包装的饮品,并转发 cost 和 description;每个子类都会使用 super。

每行输入都是一份订单,例如 milk sugar。提供的代码会按顺序将新的 Coffee 包装在装饰器中,并输出 coffee, milk, sugar: 2.70。

你的代码应写在 coffee.rb、extra.rb、milk.rb、sugar.rb 和 whip.rb 中。main.rb 包含提供的输入/输出代码,不能编辑。

亲手试试

require_relative 'coffee'
require_relative 'extra'
require_relative 'milk'
require_relative 'sugar'
require_relative 'whip'

# 提供的输入输出代码:保持原样。
input = []
while (line = gets)
  input << line.chomp
end
extras = { "milk" => Milk, "sugar" => Sugar, "whip" => Whip }
input.each do |line|
  drink = line.split.reduce(Coffee.new) { |d, name| extras[name].new(d) }
  puts format("%s: %.2f", drink.description, drink.cost)
end
quiz icon自我测试

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

面向对象编程中的所有课

自己练习: Ruby 在线编译器