Module: Hoodoo::Monkey
- Defined in:
- lib/hoodoo/monkey/monkey.rb,
lib/hoodoo/monkey/patch/datadog_traced_amqp.rb,
lib/hoodoo/monkey/patch/newrelic_traced_amqp.rb,
lib/hoodoo/monkey/patch/newrelic_middleware_analytics.rb,
lib/hoodoo/monkey/patch/active_record_dated_finder_additions.rb,
lib/hoodoo/monkey/patch/active_record_manually_dated_finder_additions.rb
Overview
Hoodoo provides monkey patching hook points as first class citizens and includes a registration, enabling and disabling mechanism through the Hoodoo::Monkey class.
You encapsulate monkey patch code inside a module. This module will be used to patch one or more target other classes or modules. Usually, one module will only be used to patch one other kind of class or module; re-use of a patch module usually only makes sense when patching one or more subclasses from a common ancestor where some, but not all of the subclass types are to be patched (if you wanted to patch all of them you’d just patch the base class).
Inside your module, you write one or two sub-modules. One of these patches instance methods in the target, the other patches class methods. The mechanism used to patch instance or class methods is different in Ruby, thus the distinct module use; it also helps keep your code clear of distracting syntax and make it very obvious what kind of “thing” is being replaced.
Monkey patch methods are sent to the patch target using ‘prepend`, the Ruby 2 mechanism which means the original overriden implementation can be called via super
, just as if you were writing a subclass.
For examples, see method Hoodoo::Monkey::register.
Any public method in the API can be patched, since the public API is by definition public and stable. Sometimes, normally-private methods are exposed for monkey patching as public methods with the name prefix of “monkey_
” - such methods are NOT intended to be called by client code in general, but can be patched. It is only completely safe to to patch a method in a wrapper fashion, e.g. to filter inputs or outputs; thus whenever possible, always call super
at some point within your replacement implementation. If you completely replace an implementation with a custom version, you risk your code breaking even with patch level changes to Hoodoo, since only the public interface is guaranteed; the way in which it is implemented is not.
You tell the monkey patching system about the outer container module, the instance and/or class patch modules and the target entity via a call to Hoodoo::Monkey::register. See this for more details. Use Hoodoo::Monkey::enable to actually ‘switch on’ the patch and Hoodoo::Monkey::disable to ‘switch off’ the patch again.
The patch engine is “require’d” by Hoodoo as the very last thing in all of its other inclusion steps when ‘hoodoo.rb’ (“everything”) is included by code. If individual sub-modules of Hoodoo are included by client code, it will be up to them when (and if) the monkey patch engine is brought in.
Hoodoo authors should note namespaces Hoodoo::Monkey::Patch and Hoodoo::Monkey::Chaos inside which out-of-the-box Hoodoo patch code should be defined. Third party patches must use their own namespaces to avoid any potential for collision with future new Hoodoo patch modules.
Defined Under Namespace
Constant Summary collapse
- @@modules =
{}
Class Method Summary collapse
-
.disable(extension_module:, target_unit: nil) ⇒ Object
Disable a patch previously enabled with ::enable (see there for more information).
-
.enable(extension_module:) ⇒ Object
Enable a given monkey patch, using the extension module parameter value given to a prior call to ::register (see there for more information).
-
.register(target_unit:, extension_module:) ⇒ Object
Register a set of monkey patch modules with Hoodoo::Monkey - see the top-level Hoodoo::Monkey documentation for an introduction and some high level guidelines for monkey patch code.
Class Method Details
.disable(extension_module:, target_unit: nil) ⇒ Object
Disable a patch previously enabled with ::enable (see there for more information).
A disabled patch will still be present in a target unit’s ancestors
list, but has no performance impact. Repeated enable/disable cycles incur no additional runtime performance penalties.
Named parameters are:
extension_module
-
A module previously passed in the same-named parameter to ::register. The instance and/or class methods defined therein will be removed from the previously registered target.
Disabling the same extension multiple times has no side effects.
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/hoodoo/monkey/monkey.rb', line 264 def self.disable( extension_module:, target_unit: nil ) if ( target_units_hash = @@modules[ extension_module ] ).nil? raise "Hoodoo::Monkey::disable: Extension module '#{ extension_module.inspect }' is not registered" end target_units_hash.each_value do | target_and_module_array | target_and_module_array.each do | target_and_module_array_entry | patch_module = target_and_module_array_entry[ :patch_module ] patch_target = target_and_module_array_entry[ :patch_target ] next if patch_module.nil? || target_and_module_array_entry.has_key?( :unbound_methods ) target_and_module_array_entry[ :unbound_methods ] = {} # We take unbound method references to every patch module method, # then remove the originals. In the re-enable code, the methods # are redefined in the module. This approach means that any # target unit with the module in its ancestors chain will see the # change immediately. We don't need to iterate over them. # patch_module.instance_methods( false ).each do | method_name | unbound_method = patch_module.instance_method( method_name ) target_and_module_array_entry[ :unbound_methods ][ method_name ] = unbound_method patch_module.send( :remove_method, method_name ) end end end end |
.enable(extension_module:) ⇒ Object
Enable a given monkey patch, using the extension module parameter value given to a prior call to ::register (see there for more information).
The initial patch installation is done via Module#prepend
, so you are able to call super
to invoke the original implementation from the overriding implementation, as if you were writing a subclass.
Instance and class method monkey patches should try very hard to always call “super” so that an overridden/patched public API method will still call back to its original implementation; the wrapper just filters inputs and outputs or adds additional behaviour. This way, changes to the Hoodoo implementation will not break the patch.
Patching is global; it is not lexically scoped. Use Ruby refinements manually if you want lexically scoped patches.
Named parameters are:
extension_module
-
A module previously passed in the same-named parameter to ::register. The instance and/or class methods defined therein will be applied to the previously registered target.
Enabling the same extension multiple times has no side effects.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/hoodoo/monkey/monkey.rb', line 205 def self.enable( extension_module: ) if ( target_units_hash = @@modules[ extension_module ] ).nil? raise "Hoodoo::Monkey::enable: Extension module '#{ extension_module.inspect }' is not registered" end target_units_hash.each_value do | target_and_module_array | target_and_module_array.each do | target_and_module_array_entry | patch_module = target_and_module_array_entry[ :patch_module ] patch_target = target_and_module_array_entry[ :patch_target ] next if patch_module.nil? # If the patch contains a target-based collection of unbound # methods, it was disabled previously (see the 'disable' code). # Re-enable by re-building the module's methods. # if target_and_module_array_entry.has_key?( :unbound_methods ) target_and_module_array_entry[ :unbound_methods ].each do | method_name, unbound_method | patch_module.send( :define_method, method_name, unbound_method ) end # Discard the references to the now-unneeded unbound methods. # target_and_module_array_entry.delete( :unbound_methods ) end # *Always* call "prepend". If the same patch modules are being used # against multiple targets, the fact that the code above saw that a # module had been disabled for one particular target doesn't mean # that the module had previously been inserted into the ancestors # for "this" target. It might have been registered later. # # This is safe as repeat calls do nothing; they don't even reorder # the ancestor chain. # patch_target.prepend( patch_module ) end end end |
.register(target_unit:, extension_module:) ⇒ Object
Register a set of monkey patch modules with Hoodoo::Monkey - see the top-level Hoodoo::Monkey documentation for an introduction and some high level guidelines for monkey patch code.
Named parameters are:
target_unit
-
The Class or Module to be patched.
extension_module
-
The module that identifies the collection of instance and/or class methods to overwrite inside the targeted unit. This MUST define a nested module called “InstanceExtensions” containing method definitions that will override same-name instance methods in the targeted unit, or a nested module called “ClassExtensions” to override class methods, or both.
For example, suppose we have this class:
class Foo
def
2 * 2
end
def self.
3 * 3
end
end
Foo.new.
# => 4
Foo.
# => 9
Next define modules which extend/override methods in the above class:
module ExtendedFoo
module InstanceExtensions
def
5 * 5
end
end
module ClassExtensions
# Even though this module will be used to override class methods
# in the target, we define the module methods with "def bar", not
# "def self.bar".
#
def
7 * 7
end
end
end
At this point, the extension is defined, but not registered with Hoodoo and not yet enabled. Register it with:
Hoodoo::Monkey.register(
target_unit: Foo,
extension_module: ExtendedFoo
)
The code is now registered so that it can be easily enabled or disabled via the given extension_module
value:
Hoodoo::Monkey.enable( ExtendedFoo )
Foo.new.
# => 25
Foo.
# => 49
Hoodoo::Monkey.disable( ExtendedFoo )
Foo.new.
# => 4
Foo.
# => 9
You can register the same extension modules for multiple target units, but it can only be enabled or disabled all in one go for all targets.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/hoodoo/monkey/monkey.rb', line 151 def self.register( target_unit:, extension_module: ) if extension_module.const_defined?( 'InstanceExtensions', false ) instance_methods_module = extension_module.const_get( 'InstanceExtensions' ) end if extension_module.const_defined?( 'ClassExtensions', false ) class_methods_module = extension_module.const_get( 'ClassExtensions' ) end if instance_methods_module.nil? && class_methods_module.nil? raise "Hoodoo::Monkey::register: You must define either an InstanceExtensions module ClassExtensions module or both inside '#{ extension_module.inspect }'" end @@modules[ extension_module ] ||= {} @@modules[ extension_module ][ target_unit ] = [ { :patch_module => instance_methods_module, :patch_target => target_unit }, { :patch_module => class_methods_module, :patch_target => target_unit.singleton_class } ] end |