如何为一个尚不存在的方法添加别名? (Ruby Decorator)

时间:2013-11-20 00:57:25

标签: ruby-on-rails ruby decorator

这听起来很疯狂,但我正在尝试构建一个基于通用装饰的系统,它允许装饰类用属性做各种疯狂的东西。目标是能够在高级别定义属性,装饰ORM类(例如ActiveRecord,虽然我们的主要情况实际上有点不同),并在应用程序的不同位置使用这些装饰来自动化一些动态“魔术”我们的应用程序需要。例如,我们将使用属性自动生成表单和视图,将复杂的表单哈希转换为更平坦的结构等。

为了适应我们到目前为止已经确定的两个用例,我有一个可混合的模块和一个装饰器(使用Draper,所以Rails形式魔术仍然可以工作,虽然我不是必须与Draper结婚)看起来或多或少像这样(显然省略了很多细节):

class DecoratorThing < Draper::Decorator
  include CoreMixinStuff
  delegate_all
end

module CoreMixinStuff
  extend ActiveSupport::Concern

  module ClassMethods
    def attribute(stuff, blah)
      attribute = AttributeDefinition.new(...)
      add_translation_methods(attribute)
      ...
    end

    def add_translation_methods(attribute)
      name = attribute.name
      reader = name
      writer = "#{name}="

      # In the case of field wrappers, we have to alias the original reader and writer so we
      # don't overwrite them completely
      if attribute.translation_type == :wrapper
        alias_method :"_orig_#{reader}", reader
        alias_method :"_orig_#{writer}", writer

      # Otherwise, we need to error if the reader or writer would collide
      elsif instance_methods.include?(reader) || instance_methods.include?(writer)
        raise RuntimeError.new("Cannot define an attribute which overrides existing methods (#{name.inspect})")
      end
    end
  end
end

然后,特定实例的实际装饰器会执行以下操作:

class FooDecorator < DecoratorThing
  decorates Foo
  attribute :field, multiple: true, serialize: true
  attribute :field2, field: :delegation_field
  attribute :field3 do |field|
    field.subtype ...
    field.subtype ...
  end
end

意图是允许Foo#字段获取数组并将其内部序列化为字符串,然后将其发送到装饰对象获取它的任何位置。 Foo#field2只会按原样将数据传递给delegation_field。 Foo#field3将采用复杂的数据哈希并将其委托给子类型字段。

后两种情况很痛苦,但我让他们在原型中工作。第一个问题是因为上面的alias_method - 因为属性方法是在装饰器上运行的,我试图别名的方法实际上还不存在。直到FooDecorator.new(some_foo_instance)被调用时,其他实例方法才可用。

我认为我的选择仅限于以下内容,但我希望有更好的选择:

  • 放弃装饰,只是接受这整件事必须是混合而不是
  • 放弃mixin,改为需要装饰,然后浏览装饰对象而不是别名方法
  • 放弃字段包装器并要求属性始终具有唯一名称并委托给字段(在上面的示例中,attribute :field...将成为attribute :field_wrapper, multiple: true, serialize: true, field: "field"
  • 放弃序列化数据,使用户有责任正确定义/覆盖处理他们定义的数据的方法

第四个选项可能是最安静的,但我已经做了很多关于能够序列化的假设,所以如果有人知道一个很好的方法来实现这一点,那就会超级膨胀。

1 个答案:

答案 0 :(得分:0)

我想我的答案符合这个问题。

基本上,我的解决方案在与活动记录模型关联的现有列的​​顶部添加了一个代理列。

此代理列会覆盖原始列的访问者方法。除了set之外,基本代理功能并没有做太多事情并且获取原始列的值。

实际代码很长,所以我为它创建了一个要点。 Check it out here

可以对代理列对象进行子类化/重写,以提供所需的任何类型的功能。例如,在您的代码中AttributeDefinition将子类ProxyColumnAttributeDefinition将充当不同属性和值之间的代理。它可以根据您认为合适的条件在DecoratorThing中包装/解包属性值。

在这个阶段,它没有做的事情是提供一种覆盖如何/添加方法的方法。但是,所有这些都可以根据您的需求轻松更改。无论如何,我实际上是根据你的add_translation_methods方法建立的那部分。

这是定义列的基本用法:

class MyModel

  # Add the concern to get the functionality
  include DataCollectionElements # concern

  # create proxy column and set various options
  proxy_column :my_col_1
  proxy_column :my_col_2, :alias => [:col1, col2, col3]
  proxy_column :my_col_3, :column => :actual_col_name

  # define the proxy object using class/string/symbol
  proxy_column :my_col_4, :proxy => MyProxy  # or 'MyProxy' or :my_proxy

  # define the proxy object using a proc/lambda
  proxy_column :my_col_5, :proxy => ->(model, code, options) { MyProxy.new(model, code, options) }
  proxy_column :my_col_6, :proxy => proc {|model, code, options| MyProxy.new(model, code, options) }

  # define the proxy object using a block
  proxy_column :my_col_7 do |model, code, options|
    MyProxy.new(model, code, options)
  end
end