如何在 Ruby class 定义的末尾设置一个钩子到 运行 代码?
How can I set a hook to run code at the end of a Ruby class definition?
我正在构建一个插件,允许开发人员通过 class 定义中的简单声明(遵循正常的 acts_as 模式)向 class 添加各种功能.
例如,使用插件的代码可能如下所示
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
我的问题出现是因为我想错误检查为 :specific_method_to_use 参数提供的值是否作为方法存在,但代码通常组织和加载的方式尚不存在该方法。
我的插件中的代码暂定如下:
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(options = {})
raise ArgumentError.new("#{options[:specific_method_to_use]} is not defined") if options[:specific_method_to_use].present? && !self.respond_to?(options[:specific_method_to_use])
end
end
end
这可行:
class YourClass
def your_method; true; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
但这是大多数人编写代码的方式,它不会:
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; true; end
end
如何在 YourClass 加载时失败?然后我希望它出错,而不是在 运行 时间出现 NoMethodError。我可以推迟执行引发 ArgumentError 的行,直到加载整个 class,或者做一些其他聪明的事情来实现吗?
使用 TracePoint
跟踪您的 class 何时发送 :end
事件。
一般解
此模块可让您在任何 class.
中创建 self.finalize
回调
module Finalize
def self.extended(obj)
TracePoint.trace(:end) do |t|
if obj == t.self
obj.finalize
t.disable
end
end
end
end
现在您可以扩展 class 并定义 self.finalize
,这将在 class 定义结束后立即 运行:
class Foo
puts "Top of class"
extend Finalize
def self.finalize
puts "Finalizing #{self}"
end
puts "Bottom of class"
end
puts "Outside class"
# output:
# Top of class
# Bottom of class
# Finalizing Foo
# Outside class
OP问题的具体解决方案
以下是如何将 TracePoint
直接安装到您已有的模块中。
require 'active_support/all'
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(**options)
m = options[:specific_method_to_use]
TracePoint.trace(:end) do |t|
break unless self == t.self
raise ArgumentError.new("#{m} is not defined") unless instance_methods.include?(m)
t.disable
end
end
end
end
下面的例子证明它按规定工作:
# `def` before `consumes`: evaluates without errors
class MethodBeforePlugin
include MyPlugin
def your_method; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
# `consumes` before `def`: evaluates without errors
class PluginBeforeMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; end
end
# `consumes` with no `def`: throws ArgumentError at load time
class PluginWithoutMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
我正在构建一个插件,允许开发人员通过 class 定义中的简单声明(遵循正常的 acts_as 模式)向 class 添加各种功能.
例如,使用插件的代码可能如下所示
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
我的问题出现是因为我想错误检查为 :specific_method_to_use 参数提供的值是否作为方法存在,但代码通常组织和加载的方式尚不存在该方法。
我的插件中的代码暂定如下:
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(options = {})
raise ArgumentError.new("#{options[:specific_method_to_use]} is not defined") if options[:specific_method_to_use].present? && !self.respond_to?(options[:specific_method_to_use])
end
end
end
这可行:
class YourClass
def your_method; true; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
但这是大多数人编写代码的方式,它不会:
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; true; end
end
如何在 YourClass 加载时失败?然后我希望它出错,而不是在 运行 时间出现 NoMethodError。我可以推迟执行引发 ArgumentError 的行,直到加载整个 class,或者做一些其他聪明的事情来实现吗?
使用 TracePoint
跟踪您的 class 何时发送 :end
事件。
一般解
此模块可让您在任何 class.
中创建self.finalize
回调
module Finalize
def self.extended(obj)
TracePoint.trace(:end) do |t|
if obj == t.self
obj.finalize
t.disable
end
end
end
end
现在您可以扩展 class 并定义 self.finalize
,这将在 class 定义结束后立即 运行:
class Foo
puts "Top of class"
extend Finalize
def self.finalize
puts "Finalizing #{self}"
end
puts "Bottom of class"
end
puts "Outside class"
# output:
# Top of class
# Bottom of class
# Finalizing Foo
# Outside class
OP问题的具体解决方案
以下是如何将 TracePoint
直接安装到您已有的模块中。
require 'active_support/all'
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(**options)
m = options[:specific_method_to_use]
TracePoint.trace(:end) do |t|
break unless self == t.self
raise ArgumentError.new("#{m} is not defined") unless instance_methods.include?(m)
t.disable
end
end
end
end
下面的例子证明它按规定工作:
# `def` before `consumes`: evaluates without errors
class MethodBeforePlugin
include MyPlugin
def your_method; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
# `consumes` before `def`: evaluates without errors
class PluginBeforeMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; end
end
# `consumes` with no `def`: throws ArgumentError at load time
class PluginWithoutMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end