【问题标题】:Ruby / Rails meta programing, how to define instance and class methods dynamically?Ruby / Rails 元编程,如何动态定义实例和类方法?
【发布时间】:2019-11-14 15:39:15
【问题描述】:

我正在尝试在一个大型生产 Rails 6.0 网站中简化我的生活。我有一堆从 Redis 作为非规范化哈希提供的数据,因为带有所有包含和关联的 Rails 非常慢。

为了保持干燥,我想使用可以包含在ApplicationRecord 中的关注(或模块),它允许我为我想要存储的数据动态定义收集方法。

这是我目前所拥有的:

class ApplicationRecord < ActiveRecord::Base
    include DenormalizableCollection
    # ...
end
# The model
class News < ApplicationRecord
    denormalizable_collection :most_popular
    # ...
end
# The Concern
module DenormalizableCollection
  extend ActiveSupport::Concern

  class_methods do
    def denormalizable_collection(*actions)
      actions.each do |action|

        # define News.most_popular
        define_singleton_method "#{action}" do
          collection = Redis.current.get(send("#{action}_key"))

          return [] unless collection.present?

          JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
        end

        # define News.set_most_popular
        define_singleton_method "set_#{action}" do
          Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
        end

        # define News.most_popular_data, which is a method that returns an array of hashes
        define_singleton_method "#{action}_data" do
          raise NotImplementedError, "#{action}_data is required"
        end

        # define News.most_popular_key, the index key to use inside of redis
        define_singleton_method "#{action}_key" do
          "#{name.underscore}_#{action}".to_sym
        end
      end
    end
  end
end

这可行,但我似乎不正确,因为我也不能定义实例方法或 ActiveRecord after_commit 回调来更新 Redis 内部的集合。

我想添加如下内容:

    after_commit :set_#{action}
    after_destroy :set_#{action}

但显然这些回调需要实例方法,而after_commit :"self.class.set_most_popular" 会导致抛出错误。所以我想添加一个实例方法,如下所示:

class News
   # ...
   def reset_most_popular
       self.class.send("set_most_popular")
   end
end

我一直在阅读尽可能多的文章,并通过 Rails 源查看我遗漏了什么 - 因为我知道我错过了一些东西!

【问题讨论】:

  • define_method 定义实例方法的方式与define_singleton_method 定义类方法的方式相同。
  • 可以使用included块定义回调,defined_method定义实例方法api.rubyonrails.org/classes/ActiveSupport/Concern.html
  • 您的意思是从class_methods 块调用define_method
  • class_methods 在幕后确实做了类似ClassMethods = Module.new do ... end 的事情。因此,当您调用define_singleton_method 时,您定义的是模块的单例方法,而不是模块扩展的类。就像在模块内调用def self.foo
  • @max,我宁愿不使用define_singleton_method,但这是我让它以基本方式运行的唯一方法。我确实尝试过class_eval &lt;&lt;-CODE, __FILE__, __LINE__ + 1 ... CODE 和普通define_method,但在检查News.methods.include?(:most_popular)时都没有注册方法

标签: ruby-on-rails ruby metaprogramming


【解决方案1】:

这里的关键是使用class_eval打开你调用denormalizable_collection的类。

一个简化的例子是:

class Foo
  def self.make_method(name)
    class_eval do |klass|
      klass.define_singleton_method(name) do
        name
      end 
    end
  end

  make_method(:hello)
end

irb(main):043:0> Foo.hello
=> :hello

module DenormalizableCollection
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def denormalizable_collection(*actions)
      actions.each do |action|
        generate_denormalized_methods(action)
        generate_instance_methods(action)
        generate_callbacks(action)
      end
    end
    private
    def generate_denormalized_methods(action)

      self.class_eval do |klass|
        # you should consider if these should be instance methods instead.
        # define News.most_popular
        define_singleton_method "#{action}" do
          collection = Redis.current.get(send("#{action}_key"))
          return [] unless collection.present?
          JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
        end
        # define News.most_popular
        # define News.set_most_popular
        define_singleton_method "set_#{action}" do
          Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
        end
        # define News.most_popular_data, which is a method that returns an array of hashes
        define_singleton_method "#{action}_data" do
          raise NotImplementedError, "#{action}_data is required"
        end
        # define News.most_popular_key, the index key to use inside of redis
        define_singleton_method "#{action}_key" do
          "#{name.underscore}_#{action}".to_sym
        end
      end
    end

    def generate_callbacks(action)
      self.class_eval do
        # Since callbacks call instance methods you have to pass a
        # block if you want to call a class method instead
        after_commit -> { self.class.send("set_#{action}") }
        after_destroy -> { self.class.send("set_#{action}") }
      end
    end

    def generate_instance_methods(action)
      class_eval do
        define_method :a_test_method do
          # ...
        end
      end
    end
  end
end

请注意,我没有使用ActiveSupport::Concern。不是我不喜欢。但在这种情况下,它增加了一层额外的元编程,这足以让我大吃一惊。

【讨论】:

  • 谢谢!这正是我所需要的。我可以问一下,当您使用define_singleton_method 时,这是定义类方法的唯一方法。有人提到这就像在模块上定义方法,而不是在类本身上。
  • 我似乎无法从模块中定义实例方法。在generate_denoramalized_methods 中使用define_method 似乎不起作用,但是当我使用def testing_method 手动定义时,它可以。 :-/
  • define_singleton_method 有一个旧的替代方法,我记不得了。但是,如果您使用类 eval 调用它在这里工作得非常好,因为接收者是您正在调用 generate_denormalized_methods 方法的类,而不是定义它的模块。您是否尝试在块中使用 define_method?
  • 我用如何生成实例方法更新了答案。还有一个我之前没有意识到的问题,那就是当你传递一个符号时,回调调用实例方法而不是类方法。我还设法搞砸了generate_callbacks 方法,因为它不应该嵌套在generate_denormalized_methods 中。
  • 谢谢@max。在玩了一些之后,我实际上也做了同样的事情。感谢您对此的帮助,非常感谢。
【解决方案2】:

您是否尝试过类似的方法:

  class_methods do
    def denormalizable_collection(*actions)
      actions.each do |action|
        public_send(:after_commit, "send_#{action}")
        ...
      end
    end
  end

【讨论】:

  • 不幸的是,这似乎不起作用。我得到NoMethodError: undefined method 'set_most_popular'
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-04
  • 2020-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-08
  • 1970-01-01
相关资源
最近更新 更多