Skip to content

Instantly share code, notes, and snippets.

@afred
Created November 6, 2015 20:31
Show Gist options
  • Save afred/fec9b979d35f1424494b to your computer and use it in GitHub Desktop.
Save afred/fec9b979d35f1424494b to your computer and use it in GitHub Desktop.
A more passive way to monkey patch in ruby
# Suppose DogBehavior and Dog are defined in a gem.
module DogBehavior
def go_pee
puts "i am now peeing"
end
end
class Dog
include DogBehavior
end
# Here is the result of running the interface as-is.
Dog.new.go_pee
# => 'i am now peeing'
# Now say we want to alter the behavior of Dog#go_pee. We could monkey patch
# DogBehavior by copying the method definition, but we set ourselves up for
# getting out-of-sync with the original. If such an upstream change were to
# result in bugs, then tracking those down can be tough if you forget that
# it's been monkey patched.
# Here is a different way that doesn't alter the original method definition.
module DogBehaviorPatch
def go_pee
puts 'first go outside'
super
puts 'then i go back inside'
end
end
# Now monkey patch the class to include the altered behavior.
class Dog
include DogBehaviorPatch
end
Dog.new.go_pee
# => first go outside
# => i am now peeing
# => then i go back inside
# This makes a couple of assumptions:
# 1. inclusion of DogBehavior happens *before* DogBehaviorPatch
# 2. there are no other classes or modules re-defining #go_pee in the
# inheritance hierarchy between DogBehavior and DogBehaviorPatch.
#
# If those two assumptions hold, then patching behavior this way is more
# stable than a direct monkey patch.
@mccalluc
Copy link

mccalluc commented Nov 6, 2015

@afred: hmm.

  • Looking through the patches we have, none (except the one I just put in) could be obviously done just with super and pre- or post-tweaks, but I could be missing something.
  • I worry that if we define the patches in our own modules we might miss all the places they need to be required. For instance, imagine that the code base above also included an obscure class we over-looked:
class RobotDog
  include DogBehavior
  include RobotBrain
end

We really should patch DogBehavior directly, so everything that depends on it gets the patch.

@afred
Copy link
Author

afred commented Nov 9, 2015

@mccallum..

Note that each monkey-patched method takes some args, and returns a value. No side effects, afaik. So all the monkey-patch is doing is taking the same args, and returning a slightly different value. But you're right in that it can't be "obviously done" because there is a lot of logic within the original methods that needs to be repeated in the monkey-patch in order to get the desired result.

In general, the more code that your monkey patch has to copy in order to work correctly, the more likely you are setting yourself up for errors when the original implementation changes.

I've been burned by this before, and so I was just thinking about ways to avoid it in the future.

I also tossed around the idea of creating a MonkeyPathWarning module that, when included, simply prints out a warning message that a particular class is being patched. But I rejected this idea because I, for one, get "warning message blindness" after seeing the same ones for a while.

Re: your 2nd point, it would be annoying to have to sniff out everywhere the patch was required, and explicitly apply it. If you missed one, and if you missed one, that would be confusing. However, it would force you to be more explicit about monkey patching. Maybe making it a little painful isn't a bad thing?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment