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