Menu
CoddyTech

使用 Forwardable 实现委托

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

委托是指一个对象将部分工作交给它持有的另一个对象,而不是继承该对象。与本身就是数组的播放列表相比,包含歌曲数组的播放列表更安全,因为外部代码只能使用你选择转发的方法:

class Playlist
  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end

  def size
    @songs.size
  end

  def first
    @songs.first
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts list.size
puts list.first

输出:

2
Intro

手动编写每个转发方法很重复。标准库中的 Forwardable 可以帮你完成这件事:使用 extend Forwardable,然后用 def_delegators :@songs, :size, :first 将这些调用转发给 @songs 中的对象:

require 'forwardable'

class Playlist
  extend Forwardable
  def_delegators :@songs, :size, :first, :last, :empty?, :include?

  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts "#{list.size} #{list.first} #{list.last}"
puts list.include?("Dune")
puts list.empty?

输出:

2 Intro Dune
true
false

def_delegator :@songs, :push, :add 会用新名称转发一个方法。对象仍完全控制所有未转发的操作,例如 delete 或 clear:

require 'forwardable'

class Queue2
  extend Forwardable
  def_delegator :@jobs, :push, :enqueue
  def_delegator :@jobs, :shift, :next_job
  def_delegators :@jobs, :size

  def initialize
    @jobs = []
  end
end

q = Queue2.new
q.enqueue("a")
q.enqueue("b")
puts q.next_job
puts q.size
puts q.respond_to?(:clear)

输出:

a
1
false

如果一个类只需要另一个类的部分行为,优先使用委托(“拥有一个”)。如果继承自 Array,就会暴露几十个方法,例如 clear 和 delete_at,这些方法可能会破坏播放列表的规则:

class Playlist < Array      # Array 的所有方法都是公开的,包括 clear
end

class Playlist              # 只有你转发的方法是公开的
  extend Forwardable
  def_delegators :@songs, :size, :each
end
challenge icon

挑战

简单

使用 Forwardable 完成 Playlist 类。它在 @songs 中保存歌曲,每首歌曲都是一个 [title, seconds] 对。使用 def_delegators 转发 size、empty? 和 first,并使用 def_delegator 将 push 转发为 add。它自己的方法 duration 返回总时长,格式为 m:ss;titles 返回用 , 拼接的歌曲标题。

提供的代码会为每行输入(title,seconds)添加一首歌曲,然后打印歌曲数量、第一首歌曲的标题、所有歌曲标题、总时长,以及播放列表是否响应 clear。

把你的代码写在 playlist.rb 中。main.rb 包含提供的输入/输出代码,不能编辑。

亲手试试

require_relative 'playlist'

# 提供的输入和输出代码:保持原样。
input = []
while (line = gets)
  input << line.chomp
end
list = Playlist.new
input.each do |line|
  title, seconds = line.split(",")
  list.add([title, seconds.to_i])
end
puts "songs: #{list.size}"
puts "first: #{list.empty? ? "none" : list.first[0]}"
puts "titles: #{list.titles}"
puts "duration: #{list.duration}"
puts "can clear: #{list.respond_to?(:clear)}"
quiz icon自我测试

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

面向对象编程中的所有课

自己练习: Ruby 在线编译器