如何在Ruby类定义的末尾设置一个钩子来运行代码?

Lik*_*ell 12 ruby plugins loading

我正在构建一个插件,允许开发人员在类定义中使用简单声明向类添加各种功能(遵循正常的acts_as模式).

例如,使用插件的代码可能看起来像

class YourClass
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
Run Code Online (Sandbox Code Playgroud)

我的问题出现了,因为我想错误地检查为: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
Run Code Online (Sandbox Code Playgroud)

这可行:

class YourClass
  def your_method; true; end

  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
Run Code Online (Sandbox Code Playgroud)

但这是大多数人编写代码的方式,它不会:

class YourClass
  consumes_my_plugin option1: :value1, specific_method_to_use: :your_method

  def your_method; true; end
end
Run Code Online (Sandbox Code Playgroud)

如何在YourClass加载时失败?我希望它出错,而不是在运行时使用NoMethodError.我可以推迟执行引发ArgumentError的行,直到加载整个类,或者做一些其他聪明的事情来实现吗?

use*_*951 6

使用TracePoint时,你的该类发送了一个跟踪:end事件.


具体解决您的问题

以下是如何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
Run Code Online (Sandbox Code Playgroud)

以下示例表明它按指定的方式工作:

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
Run Code Online (Sandbox Code Playgroud)

一般解决方案

该模块允许您self.finalize在任何类中创建回调.

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
Run Code Online (Sandbox Code Playgroud)

现在,您可以扩展您的类和定义TracePoint,它将在类定义结束后立即运行:

# `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
Run Code Online (Sandbox Code Playgroud)