没有覆盖构造函数的对象的Ruby委派

时间:2014-01-02 19:51:19

标签: ruby delegation

Ruby委派的一个例子是使用SimpleDelegator:

class FooDecorator < SimpleDelegator
  include ActiveModel::Model
  # ...
end

这非常方便,因为FooDecorator未响应的任何内容都会传递给底层对象。

但是这会覆盖构造函数的签名,这使得它与期望看到特定签名的ActiveModel::Model等内容不兼容。

Ruby委派的另一个例子是使用Forwardable

class Decorator
  include ActiveModel::Model
  extend Forwardable

  attr_accessor :members
  def_delegator  :@members, :bar, :baz
  def_delegators :@members, :a, :b, :c
end

但是现在你必须明确你想要委派哪些方法,这很脆弱。

有没有办法让两个世界都做到最好,我... ...

  • 不要篡改构造函数
  • 获取委托对象
  • 上任何等效命名方法的传递委派

2 个答案:

答案 0 :(得分:3)

你看过Delegator文档了吗?您基本上可以使用__getobj____setobj__方法重新实现自己的Delegator子类。或者,您可以将SimpleDelegator子类化并指定自己的构造函数来调用super(obj_to_delegate_to),不是吗?

你总是可以在装饰器上实现method_missing,并将任何未找到的方法传递给底层对象。

编辑:我们走了。使用中间继承类会破坏super()链接,允许您根据需要包装类:

require 'active_model'
require 'delegate'

class Foo
  include ActiveModel::Model
  attr_accessor :bar
end

class FooDelegator < Delegator
  def initialize
    # Explicitly don't call super
  end

  def wrap(obj)
    @obj = obj
    self
  end

  def __getobj__
    @obj
  end
end

class FooDecorator < FooDelegator
  include ActiveModel::Model

  def name
    self.class.name
  end
end

decorator = FooDecorator.new.wrap(Foo.new bar: "baz")
puts decorator.name #=> "decorator"
puts decorator.bar  #=> "bar"

答案 1 :(得分:1)

为什么你想要ActiveModel :: Model?你真的需要所有这些功能吗?

你能做同样的事吗?

extend  ActiveModel::Naming
extend  ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion

我知道对ActiveModel :: Model的更改可能会破坏你的装饰器的预期行为,但无论如何你都要紧密地联系它。

允许模块控制构造函数是代码气味。可能没问题,但你应该再次猜测它,并清楚它为什么会这样做。

ActiveModel :: Model定义initialize以期望散列。我不确定你希望得到你想要包装的特定对象。 SimpleDelegator在其构造函数中使用__setobj__,因此您可以在包含覆盖构造函数的模块之后使用它。

如果您想要自动转发,您可以在设置对象时定义装饰器上所需的方法。如果您可以控制对象的创建方式,请创建一个build方法(或类似的方法)调用需要用于ActiveModel :: Model的initialize和使用的__setobj__对于SimpleDelegator:

require 'delegate'
require 'forwardable'
class FooCollection < SimpleDelegator
  extend Forwardable
  include ActiveModel::Model

  def self.build(hash, obj)
    instance = new(hash)
    instance.send(:set_object, obj)
    instance
  end

  private

  def set_object(obj)
    important_methods = obj.methods(false) - self.class.instance_methods
    self.class.delegate [*important_methods] => :__getobj__
    __setobj__(obj)
  end
end

这允许您使用ActiveModel接口,但将SingleForwardable模块添加到装饰器的单例类,它为您提供delegate方法。您需要做的就是传递一组方法名称和一个用于获取转发对象的方法。

如果您需要包含或排除特定方法,只需更改important_methods的创建方式即可。我没有想太多,所以在抓住这段代码之前仔细检查一下实际使用的是什么。例如,一旦调用set_object方法,您可以稍后跳过调用它,但这是为了期望所有包装对象具有相同的接口。

正如您在Twitter上指出的那样,draper gem使用delegate内的method_missing方法(来自ActiveSupport)。通过这种方法,每次错过命中都会产生打开课程和定义转发方法的成本。好处是它很懒,你不需要计算哪些方法需要转发,而且只有在第一次错过时才会发生命中;后续方法调用不会被遗漏,因为您正在定义该方法。 我上面的代码将获得所有这些方法并立即定义它们。

如果你需要更多的灵活性并期望你的装饰器不是同一类型的对象,你可以使用SingleForwardable获得相同的效果,但它将为每个包装的实例定义方法,而不是影响装饰器类:

require 'delegate'
require 'forwardable'
class FooCollection < SimpleDelegator
  include ActiveModel::Model

  def self.build(hash, obj)
    instance = new(hash)
    instance.set_object(obj)
    instance
  end

  def set_object(obj)
    important_methods = obj.methods(false) - self.class.instance_methods
    singleton_class.extend SingleForwardable
    singleton_class.delegate [*important_methods] => :__getobj__
    __setobj__(obj)
  end
end

但所有这些都是使用SimpleDelegator,如果你实际上没有使用method_missing,你可以减少它(假设你已经正确计算了important_methods部分:

require 'forwardable'
class FooCollection
  include ActiveModel::Model

  def self.build(hash, obj)
    instance = new(hash)
    instance.set_object(obj)
    instance
  end

  def set_object(obj)
    important_methods = obj.methods(false)# - self.class.instance_methods
    singleton_class.extend SingleForwardable
    singleton_class.delegate [*important_methods] => :__getobj__
    __setobj__(obj)
  end

  def __getobj__
    @obj
  end

  def __setobj__(obj)
    __raise__ ::ArgumentError, "cannot forward to self" if self.equal?(obj)
    @obj = obj
  end
end

但是,如果你这样做,它就会杀死super的使用,所以你不能覆盖你的包装对象上定义的方法,并调用super来获得原始值,就像你可以{ {1}}在SimpleDelegator中使用。

我写了casting来向对象添加行为而不用担心包装器。你不能用它覆盖方法,但如果你所做的只是添加新的行为和新的方法,那么只需在现有对象中添加一堆方法就可以更简单地使用它。值得一试。我在RubyConf 2013

上介绍了method_missingdelegate个库