如何在 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