Ruby / Rails元编程,如何动态定义实例和class方法?

Ruby / Rails meta programing, how to define instance and class methods dynamically?

我正在努力让我在大型制作 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 来源查看我遗漏了什么 - 据我所知,我确实遗漏了一些东西!

你有没有尝试过类似的东西:

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

这里的关键是使用class_eval打开你正在调用denormalizable_collection的class。

一个简化的例子是:

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。不是我不喜欢它。但在这种情况下,它增加了一个额外的元编程级别,这足以让我的头爆炸。