如何使用Ruby元编程将回调添加到Rails模型?

bar*_*own 9 ruby metaprogramming ruby-on-rails

我编写了一个简单的Cacheable模块,可以很容易地在父模型中缓存聚合字段.该模块要求父对象为需要在父级别进行缓存的每个字段实现cacheable方法和calc_方法.

module Cacheable
  def cache!(fields, *objects)
    objects.each do |object|
      if object.cacheable?
        calc(fields, objects)
        save!(objects)
      end
    end
  end

  def calc(fields, objects)
    fields.each { |field| objects.each(&:"calc_#{field}") }
  end

  def save!(objects)
    objects.each(&:save!)
  end
end
Run Code Online (Sandbox Code Playgroud)

我想将回调添加到包含此模块的ActiveRecord模型中.此方法将要求模型实现父模型的哈希值和需要缓存的字段名称.

def cachebacks(klass, parents)
  [:after_save, :after_destroy].each do |callback|
    self.send(callback, proc { cache!(CACHEABLE[klass], self.send(parents)) })
  end
end
Run Code Online (Sandbox Code Playgroud)

如果我使用如下手动添加两个回调,这种方法很有效:

after_save proc { cache!(CACHEABLE[Quote], *quotes.all) }
after_destroy proc { cache!(CACHEABLE[Quote], *quotes.all) }
Run Code Online (Sandbox Code Playgroud)

但是,当我尝试使用该cachebacks方法将这些添加到回调时,我收到以下错误.

cachebacks(Quote, "*quotes.all")

NoMethodError: undefined method `cachebacks' for #<Class:0x007fe7be3f2ae8>
Run Code Online (Sandbox Code Playgroud)

如何动态地将这些回调添加到类中?

Bra*_*dan 6

这看起来很好ActiveSupport::Concern.您可以cachebacks稍微调整您的方法,将其作为类方法添加到包含类:

module Cacheable
  extend ActiveSupport::Concern

  module ClassMethods
    def cachebacks(&block)
      klass = self
      [:after_save, :after_destroy].each do |callback|
        self.send(callback, proc { cache!(CACHEABLE[klass], *klass.instance_eval(&block)) })
      end
    end
  end

  def cache!(fields, *objects)
    # ...
  end

  # ...
end
Run Code Online (Sandbox Code Playgroud)

要使用它:

class Example < ActiveRecord::Base
  include Cacheable
  cachebacks { all }
end
Run Code Online (Sandbox Code Playgroud)

传递给的块cachebacks将在调用它的类的上下文中执行.在此示例中,{ all }等效于调用Example.all并将结果传递给您的cache!方法.


要在注释中回答您的问题,请Concern封装一个通用模式并在Rails中建立约定.语法略显优雅:

included do
  # behaviors
end

# instead of

def self.included(base)
  base.class_eval do
    # behaviors
  end
end
Run Code Online (Sandbox Code Playgroud)

它还利用另一个约定来自动且正确地包含类和实例方法.如果您的命名空间,命名模块的方法ClassMethodsInstanceMethods(尽管你所看到的,InstanceMethods是可选的),那么你就大功告成了.

最后,它处理模块依赖性.文档提供了一个很好的例子,但实质上,它阻止了包含类除了它实际感兴趣的模块之外还必须显式包含依赖模块.