LoginSignup
2
2

More than 5 years have passed since last update.

深入理解和学习Ruby的Forwardable

Last updated at Posted at 2015-05-02

Forwardable 这个库我很喜欢, 它能够很大程度上简单代码,让我的库保持简洁. 通常我发现如果想试图去学习理解一个事物的存在就需要明白它的存在性和如果使用.

如何使用

首先,我们来看看如何通过Forwardable 这个模块来简单代码. 我经常发现一些莫须有的东西拖慢自己的速度, 而且很多时候存在重复的想法,当我在阅读一些代码的时候.

我们看下面的代码:

class Person
    def street
        address.street
    end
    def city
        address.city
    end

    def state
        address.state
    end
end

看上面的代码,可能觉得很简单,但是你会发现他们其实在做同一件事情.也就是Person.street 其实就是address.street 如此而已.

ruby 有一个内置的方式来提炼这个代码.那就是Forwardable.

上面代码中的意思是给一个对象添加赋值语句, 这个情况下, 如果你要获取人的地址信息, 它要去到相关的地址对象. 这个过程相对简单, 但是我还是想换个方式来作, 看下面的代码:

require 'forwardable'
class Person
    extend Forwardable
    delegate [:street, :city, :state] => :address

上面代码的意思就是它把一个消息传递给了合作的对象, 看上去很清楚,感觉和读取配置文件似得. 简单明了.

Forwardable 可以工作在更长的代码段上, 你完全可以定义一个很长的方法 list, 然后把他们委托给对应的对象上. 我们可以通过下面的代码感受一下Forwardable 是怎么工作的.

module Forwardable
  def delegate(hash)
    hash.each{ |methods, accessor|
      methods.each{ |method|
        instance_eval %{
          def #{method}(*args, &block)
            #{accessor}.__send__(:#{method}, *args, &block)
          end
        }
      }
    }
  end
end

这个delegate 方法接受一个 hash 对象, 他的 key 是对应的方法,然后它把这个方法传递给了对应的value 值,也就是对应的委托对象. 通过instance_eval 把这个方法绑定到对象上. 但是实际上Forwardable 的代码很复杂,它用了2个循环来处理一个 hash 和数组,之后再instance_eval. 通过遍历和动态方法定义就完成了重复的代码简化.

模块和它的上下文

Forwardable 它是一个模块. 它可以用来给一个对象追加行为. 一般我们看到的实现这个功能的方式是:

class Person
    include SuperSpecial
end

但是Forwardable不一样,它是用extend 来完成的.

require 'forwardable'
class Person
    extend Forwardable
end

通过使用extend 包含了模块到当前对象的singleton_class中. 这里可能有点绕,但是其实区别就在于 include 是追加实例方法, extend是追加类方法的. 总结一句话就是在使用Forwadable 的时候咱们使用extend.

定于传递规则

  • 通常我喜欢使用的方式就是上面提到的: delegate 方式,把一系类的方法名通过symbol 或者string 以 hash 的键传递给对应value 的对象上.
class Person
  extend Forwardable

  delegate [:message_to_forward, :another_method_name] => :object_to_receive_message,
            :single_method => :other_object
end
  • 其他的方式:

Forwardable 提供了很多方法. 但是最常见的就是 delegate, def_delegatordef_delegators. 但是他们也都是方法别名.

alias delegate instance_delegate
alias def_delegators def_instance_delegators
alias def_delegator def_instance_delegator

我们发现也就是delegate 这个方法名最简单一点.

这个def_delegators 方法接受多个参数, 这样就是不容易记住那些参数比较重要, 第一个参数是对应的我们要委托的对象. 第二个是我们要传递的方法.

class SpecialCollection
  extend Forwardable

  def_delegators :@collection, :clear, :first, :push, :shift, :size
  # The above is equivalent to:
  delegate [:clear, :first, :push, :shift, :size] => :@collection
end

通过上面可以发现,delegate 的方式参数最起码把要委托的方法名和委托方给隔离开, 看上去清晰一些.

还有一个区别的地方是:

def instance_delegate(hash) # aliased as delegate
  hash.each{ |methods, accessor|
    methods = [methods] unless methods.respond_to?(:each)
    methods.each{ |method|
      def_instance_delegator(accessor, method)
    }
  }
end

delegate 在接到参数之后,会先把方法名的参数验证是否指数组,如果不是还要转,然后再调用def_instance_delegator方法. 下面是def_instance_delegators 的实现:

def def_instance_delegators(accessor, *methods) # aliased as def_delegators
  methods.delete("__send__")
  methods.delete("__id__")
  for method in methods
    def_instance_delegator(accessor, method)
  end
end

上面的代码中移除了比较危险的__ send____ id__方法,然后就直接调用def_instance_delegator来完成了.

这里,如果你像一股脑儿的把所有的方法都委托了,直接可以这样:

class SpecialCollection
  extend Forwardable

  def_delegators :@collection, *Array.instance_methods
  # The above is equivalent to:
  delegate [*Array.instance_methods] => :@collection
end

这样的话,当然就包括了__ send__方法进去了, ruby 会警告这个有危险.

def_delegatorsdef_delegator的复数形式, 它提供了更多的选项.

class SpecialCollection
  extend Forwardable

  def_delegator :@collection, :clear, :remove
  def_delegator :@collection, :first
end

这里, def_delegator接受3个参数,第一个是接收方,第二个是委托方法,第三个是可选的就是要定义在当前对象上的方法. 如果不提供,就直接取第二个.

class SpecialCollection
  extend Forwardable

  # def_delegator :@collection, :clear, :remove
  def remove
    @collection.clear
  end

  # def_delegator :@collection, :first
  def first
    @collection.first
  end
end

上面的代码就是如果不适用Forwardable 特点的代码. 可以发现, 第三个可选参数的区别.

这些方法到底是怎么创造的

让我们来看看Forwardable 到底是怎么添加放到你的当前对象的.

def def_instance_delegator(accessor, method, ali = method)
  line_no = __LINE__; str = %{
    def #{ali}(*args, &block)
      begin
        #{accessor}.__send__(:#{method}, *args, &block)
      rescue Exception
        $@.delete_if{|s| Forwardable::FILE_REGEXP =~ s} unless Forwardable::debug
        ::Kernel::raise
      end
    end
  }
  # If it's not a class or module, it's an instance
  begin
    module_eval(str, __FILE__, line_no)
  rescue
    instance_eval(str, __FILE__, line_no)
  end
end

上面代码看上去很长,很复杂. 是的.. 让我们来简化它,

def def_instance_delegator(accessor, method, ali = method)
  str = %{
    def #{ali}(*args, &block)
      #{accessor}.__send__(:#{method}, *args, &block)
    end
  }
  module_eval(str, __FILE__, __LINE__)
end

我们看到,这个地方用 string 定义了一个方法,然后赋值给str. 然后把它传给了module_eval, 虽然module_evalclass_eval 是类似的.但是我往往更多看家难道是class_eval 啊.别管他, 把class_eval 看成是module_eval 的别名好了.

让我们看看之前的一段代码的实现:

def_delegator :@collection, :clear, :remove

它其实对应于:

%{
  def remove(*args, &block)
    @collection.__send__(:clear, *args, &block)
  end
}

错误管理

应用生成的方法

不要就是通过module_eval 来把定义方法.

# If it's not a class or module, it's an instance
begin
  module_eval(str, __FILE__, line_no)
rescue
  instance_eval(str, __FILE__, line_no)
end

如果module_eval 发生错误了, 它会执行instance_eval. 这个地方我算是找到了instance_eval 的用处了, 它也就是说不仅仅作用于类和模块,而且可以用于一个单独的对象.

object = Object.new
object.extend(Forwardable)
object.def_delegator ...

类或者模块级别的委托

上面实现的方法他们都是给类实例的, 虽然有趣,但是就范围有限. 不过别担心forwardable.rb 同样提供了一个SingleForwardable, 他们是为模块专门设计的.(类也是模块!)

class Person
    extend Forwardable
    extend SingleForwardable
end

上面的代码中你可以看到它同时扩展了ForwardableSingleForwardable 这也就意味着它可以同时把委托的方法供给类实例和类本身.
这个用法也就类似def_instance_delegator 替代def_delegator. 如果使用def_delegator,这些方法不会给别名. 你应该如下面的方法来使用:

class Person
  extend Forwardable
  extend SingleForwardable

  single_delegate [:store_exception] => :ExceptionTracker
  instance_delegate [:street, :city, :state] => :address
end

如果你仔细想, 你会发现他们的区别在于:

alias delegate single_delegate
alias def_delegators def_single_delegators
alias def_delegator def_single_delegator

也就是如果你想用细了这个instance_delegatesingle_delegate 就不要省略他们了要写明了这个区别.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2