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.
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…
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…
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.