Ruby 语言中有许多非常有趣的实现,比如我们在 Ruby 中的 class_eval 和 module_eval 方法 章节中讲解的 class_eval
和 module_eval
两个方法用于在运行时为类和模块动态的添加方法和属性。
本章节,我们就来讲讲另一个模块 forwardable
,它用于给给定类的所有实例添加一个行为。
本章节,我们将涉及到以下几个知识点
Forwardable
模块def_delegator
方法def_delegators
方法delegate
方法
Forwardable 模块
Forwardable 是一个模块,包含了一些方法可用于向给定类的所有实例添加行为 ( behavior )
在 Ruby 内部实现时,使用了 extend
关键字将此模块包含进 singleton 类,为此可以在类级别向类添加方法 ( 保持简单 )
接下来,让我们来阐述下 Forwardable
模块的 API 吧。
def_delegator 方法
Forwardable#def_delegator
方法允许对象将消息转发到定义的接收器
如果您不熟悉 Ruby 中的消息和接收器的概念,可以阅读我们的文章 Ruby 中 private 和 protected 的消息 ( Message ) 机制
揭开先前论调的神秘面纱,没有什么比举例更好的了
# in forwardable.rb class Hero attr :skills def initialize @skills = [:strong, :keen, :brave] end end jack = Hero.new puts "Jack's main skill: #{jack.skills.first}"
运行结果如下
?> ruby forwardable.rb Jack's main skill: strong
这能正常工作,但, 在 Hero 类定义之外调用 jack.skills.first
有点..像个怪人
因此,让我们将此代码封装到 Hero 类定义中
# in forwardable.rb class Hero attr :skills def initialize @skills = [:strong, :keen, :brave] end def main_skill @skills.first end end jack = Hero.new puts "Jack's main skill: #{jack.main_skill}"
运行结果如下
?> ruby forwardable.rb Jack's main skill: strong
棒极了!代码中的 Hero#main_skill
方法包含访问 Hero 主技能的逻辑
这个解决方案是可以接受的。
但 Ruby 提供了一种更好的机制,使用 Forwardable#def_delegator
方法可以将消息( #first )从实例( jack ) 转发到显式接收器(技能)
我们就按照这种机制修改下代码呗
# in forwardable.rb require 'forwardable' class Hero attr :skills extend Forwardable def_delegator :@skills, :first, :main_skill def initialize @skills = [:strong, :keen, :brave] end end jack = Hero.new puts "Jack's main skill: #{jack.main_skill}"
输出结果如下
?> ruby forwardable.rb Jack's main skill: strong
太酷了!这里,我们通过使用 Ruby 提供的消息转发系统避免了创建一个 getter 方法来访问技能
上面的代码中
- 首先,我们加载了
forwardable
模块 - 其次,我们使用
extend Forwardable
关键字将该模块的方法添加到Hero
类级别中 - 最后,我们使用新添加的类级方法
def_delegator
- 第一个参数
:@skills
对应于消息转发的接收者。 - 第二个参数
:first
是要转发的消息 - 最后是第三个参数
:main_skill
是:first
消息的别名 - 当我们调用jack.main_skill
时, 它比jack.first
更可读 - 然后在内部自动调用skills.first
- 第一个参数
def_delegators 方法
def_delegators 方法与 def_delegator 方法类似,
两个方法的主要区别是,def_delegators 方法需要一组方法来转发,并且方法不能别名
# in forwardable.rb require 'forwardable' class Todolist attr :tasks extend Forwardable def_delegators :@tasks, :first, :last def initialize @tasks = %w[conception implementation refactoring] end end todolist = Todolist.new puts "first tasks: #{todolist.first}" puts "last tasks: #{todolist.last}"
输出结果为
?> ruby forwardable.rb first tasks: conception last tasks: refactoring
在这个示例中,tasks
数组的 firt
和 last
方法可用于任何 Todolis
实例
当调用这两个方法其中之一时,消息将被转发到 tasks 数组。
delegate 方法
delegate 方法接受一个散列 ( hash ) 作为参数,其中
- 键 ( key ) 是一条或多条消息
- 值 ( value ) 是 键对应的消息的接收器
# in forwardable.rb require 'forwardable' class Computer attr :cores, :screens extend Forwardable delegate %I[size] => :@cores, %I[length] => :@screens def initialize @cores = (1..8).to_a @screens = [1, 2] end end macrosoft = Computer.new puts "Cores: #{macrosoft.size}" puts "Screens: #{macrosoft.length}"
输出结果为
$> ruby forwardable.rb Cores: 8 Screens: 2
上面的示例中,macrosoft.size
消息对应于 macrosoft.cores.size
。
并且,macrosoft.length
消息对应于 macrosoft.screens.length