如何在Ruby中为所有公开类方法「魔法般」注入代码?
Your existing code gets the job done, but injecting code during instance initialization feels a bit clunky—plus it runs redundant logic every time a new instance is created. Let's refactor this to use clean, attr_accessor-style syntax, and even support injecting code into all public methods with a single flag.
Solution 1: Custom Class Method with alias_method
We'll create a reusable class method that lets you specify which methods to wrap directly in the class definition. This avoids per-instance setup and keeps your code organized.
module MethodInjection # Class method to define wrapped methods def attr_add_code(*methods) # Handle the :all flag to target all public instance methods defined in this class if methods.include?(:all) methods = public_instance_methods(false) # `false` excludes inherited methods end methods.each do |meth| meth = meth.to_sym # Alias the original method before redefining it alias_method "original_#{meth}".to_sym, meth # Redefine the method to wrap the original with our timing logic define_method(meth) do elapsed do send("original_#{meth}".to_sym) end end end end private # Reusable timing logic (inherited by including classes) def elapsed start = Process.clock_gettime(Process::CLOCK_MONOTONIC) block_value = yield finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) puts "elapsed: #{finish - start} seconds, block_value: #{block_value}." block_value end end class MyClass include MethodInjection # Use our custom syntax to specify methods to wrap attr_add_code :a, :b # Or wrap all public methods: attr_add_code :all def a sleep 1 "returning from a" end def b sleep 1 "returning from b" end end
Why this works better:
- Clean syntax: Just like
attr_accessor, you declare which methods to wrap at the top of your class. - One-time setup: Injection happens once during class definition, not every time you create an instance.
- Safe targeting:
public_instance_methods(false)ensures we only wrap methods defined directly inMyClass, not inherited ones liketo_sorinspect.
Solution 2: Using prepend (Ruby's Preferred Method Injection)
For even more robustness, we can use Ruby's prepend feature, which inserts a module into the method lookup chain before the class itself. This avoids managing aliases and plays nicer with other metaprogramming.
module MethodInjection def attr_add_code(*methods) if methods.include?(:all) methods = public_instance_methods(false) end # Dynamically create an injection module with wrapped methods injection_module = Module.new do methods.each do |meth| define_method(meth) do elapsed { super() } # `super` calls the original class method directly end end private def elapsed start = Process.clock_gettime(Process::CLOCK_MONOTONIC) block_value = yield finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) puts "elapsed: #{finish - start} seconds, block_value: #{block_value}." block_value end end # Prepend the module to the class so our wrapped methods run first prepend injection_module end end class MyClass extend MethodInjection # Use extend since we're adding a class method attr_add_code :a, :b # Or: attr_add_code :all def a sleep 1 "returning from a" end def b sleep 1 "returning from b" end end
Benefits of this approach:
- No alias management:
superdirectly references the original method, so we don't have to trackoriginal_*aliases. - Better compatibility:
prependis the standard Ruby way for method wrapping, and it works seamlessly with other modules or inheritance. - Cleaner separation: The injection logic is encapsulated in a dynamic module, keeping your class code uncluttered.
Both solutions let you achieve your goal of clean, non-repetitive method injection. The prepend approach is generally preferred for modern Ruby code due to its robustness.
内容的提问来源于stack exchange,提问作者tophan

