Safer Monkey Patching

Avoid monkey patching and its side effects by extending an object.

The need to override behavior on a one time basis

A few weeks ago we found an issue in one of our tools which was inadvertently printing out some credentials to a log. As we dug in to fix the problem we soon realized that we would need to override behavior in one of the gems we depend on, and not our own code. We didn’t want to issue a pull request however, as we didn’t want to affect other uses of the gem. One obvious go-to solution was to monkey patch the class method that was logging our creds. Monkey patching, however, has the side effect that it overrides behavior all the time, but we only wanted this behavior during initial login.

After some thought, we opted for a solution that allowed us to override behavior locally, that is during login, and avoided having any unexpected behavior in any other calls. We decided to copy and extend an instance of the class. This allowed us, in a way, to have our cake and eat it too.

Why monkey patching is a bad idea

Here’s an example of a standard monkey patch.

# Suppose this was a class definition in an external gem...
class ExternalLogger
  def logit( str )
    puts str
  end
end

# whose logit method has the effect of passing all text through
logger = ExternalLogger.new
logger.logit("password=secret")
=> password=secret

# In a clasic monkey patch, you redefine the method, in this case mask any text including 'password'
class ExternalLogger
  def logit( str )
    if str =~ /password/i
      str = "********"
    end
  end
end

# Unfortunately this affects any new instances in your application, even when you don't want the behavior.
new_logger = ExternalLogger.new
# good...
new_logger.logit("password=secret")
=> "********"
# this was not intended
new_logger.logit("please enter a password")
=> "********"

As you can see, the behavior is completely changed for any new instances of the class. The problem here is that it made sense to make a change to avoid exposing a password at login, but it inadvertedly would change any other text passing through later. Bad monkey patch!

A better way to monkey patch, override the instance.

What if there was a way to keep the original behavior of the included class everywhere else it was intantiated, but still be able to override behavior on demand?

Take for example this alternative…

# Class definition in an external gem...
class ExternalLogger
  def logit(str)
    puts str
  end
end

# with the passthrough behavior...
logger = ExternalLogger.new
logger.logit("password=secret")
=> password=secret

# create a module with the behavior we'd like...
module MySafeLogger
  def logit(str)
    if str =~ /password/i
      puts "*****"
    else
      puts str
    end
  end
end

# dup the object and extend it...
temp_logger = logger.dup
temp_logger.extend(MySafeLogger)

# now, during login the pasword is masked
temp_logger.logit("password=secret")
=> "********"

# Meanwhile new instance has no side effects
new_logger = ExternalLogger.new
# A new instance happily reveals the password, no worries :) live on the edge.
new_logger.logit("password=secret")
=> "password=secret"

Why does this work?

This works because Ruby allows you to specify a reciever when assigning a method, even an object. We can for example, do this…

str = "some string"
def str.redrum
  self.reverse
end

puts str.redrum
=> gnirts emos

Suddenly the object str has a new method that wasn’t part of the String class. Since you didn’t monkey patch the String class, the method is not in the String class either! When you add a method to an instance, Ruby creates an anonymous class, (a.k.a. Singleton class), to be the containier for the newly added method.

This is better visualized in the following diagram.

This Singleton class has some other special properties which make it ideal for a good alternative to monkey patching. Most importantly, it has no name, and can’t be used to instantiate another object, when the object goes away, it goes away too. Perfect for doing something you don’t want someone else to repeat!

Use extend() to add the method to the instance

Now, we don’t want to have to add the method to a copy of the instance every time we need the behavior, since you’d have to re-declare the method every time. But we don’t have to, since extending an instance has the same effect. So we can declare our method in a module and simply extend a copy when we need to override behavior.