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
但是现在你必须明确你想要委派哪些方法,这很脆弱。
有没有办法让两个世界都做到最好,我... ...
答案 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_missing
和delegate
个库