4 Ways To Avoid Monkey Patching

??? words · ??? min read

In Ruby land, monkey patching is the act of modifying the methods on someone else’s class. Ruby makes it easy to add, remove, and replace methods on any class – even core classes like Array – but it is generally frowned upon. It can be very difficult to debug problems caused by monkey patching, and it can easily cause bugs when the patched class changes.

If you’re considering monkey patching but you know it’s a bad idea, here are four alternatives in order from most preferable to least preferable.

The Monkey Patch

Here is the (somewhat contrived) example of monkey patching that we will be looking at.

ActionController::Base.class_eval do
  def current_user
    uid = session[:user_id]
    uid ? User.find_by(id: uid)) : nil
  end
end

class EmailController < ApplicationController
  def email
    render text: current_user.email
  end
end

The idea is to make a current_user method that is available for use in controllers. The class_eval method is being used to define current_user on ActionController::Base, which is a class inside of Rails. The EmailController class is just there to show how the patched method is used.

On to the alternatives!

1. Inline It (Don’t Do It)

class EmailController < ApplicationController
  def email
    render User.find(session[:user_id]).email
  end
end

Out of all the options in this article, including monkey patching, this requires the least code. It requires no new classes, modules, or methods. You never have to ask where current_user is defined because it doesn’t exist. The implementation of current_user has been reduced to just a single line: User.find(session[:user_id]).

If the functionality is only used in a couple of places, then this is usually the best choice. Remember that no code is better than no code. If you suspect that the functionality will only be used in two or three places, then strongly consider inlining the code instead of extracting it into a new method.

However, regarding this particular example, we would likely want to access the current user in a lot of different places. If it’s going to be used everywhere, then inlining the code is probably a bad choice.

2. Make It A Standalone Function

module CurrentUser
  def self.get(session)
    uid = session[:user_id]
    uid ? User.find_by(id: uid)) : nil
  end
end

class EmailController < ApplicationController
  def email
    render text: CurrentUser.get(session).email
  end
end

It’s preferable to implement new functionality in new code, instead of modifying existing code. It’s easier to test, and less likely to introduce bugs into existing code. Creating new classes/methods also helps to maintain separation of concerns, and it’s obvious where the functionality lives.

It doesn’t look very “rubyish,” but it should at least be easy to follow. There is nothing clever or magical about it, which is exactly the point. Looking at the usage code, you could correctly assume that there is a module/class called CurrentUser, probably in a file named current_user.rb, which contains the get method. It’s easy to trace your way through the code because it’s explicit.

This is a good solution if current_user is used in just a few different controllers. If it will be used in the majority of controllers, then it might make sense to choose convenience over simplicity with one of the next two alternatives.

3. Make It A Mixin Module

module CurrentUserMethods
  def current_user
    uid = session[:user_id]
    uid ? User.find_by(id: uid) : nil
  end
end

class EmailController < ApplicationController
  include CurrentUserMethods

  def email
    render text: current_user.email
  end
end

If convenience is important, then consider implementing the functionality in a mixin module. This way the new method is limited to just the controllers that explicitly include it, instead of every controller in the whole app. This solution keeps some of the explicitness and separation from previous alternatives, without polluting every controller class.

This is a good solution if the functionality will be used in a lot of different places, but not everywhere. If you are writing include CurrentUserMethods on literally every controller, then you’re not getting any benefit from using a separate mixin, so you should consider the next option: putting it in a superclass.

4. Put It In A Superclass

class ApplicationController < ActionController::Base
  def current_user
    uid = session[:user_id]
    uid ? User.find_by(id: uid) : nil
  end
end

class EmailController < ApplicationController
  def email
    render text: current_user.email
  end
end

If every single controller uses the functionality, then it’s probably best to put it in a class that all controllers inherit from. That way it’s available everywhere by default.

This is the least preferable alternative for a few reasons:

  • It affects literally every controller in the application. That’s a huge surface area for introducing bugs.

  • If you didn’t have an inheritance hierarchy before, you do now! Rails provides the controller superclass ApplicationController by default, but you’re not going to have a preexisting superclass in all situations. If you’re using inheritance sparingly – and you should be – then most of the time you will need to create a new inheritance hierarchy. That’s not something you want to do lightly. If language features were a food pyramid, inheritance would be a “sometimes” food.

  • It’s harder to trace. The controller doesn’t explicitly include or define the method, the method just exists already. If you want to look up the definition, you have to go digging through all the superclasses to find it, plus any overriding methods. That’s more complicated than a normal, stand-alone method.

For our particular current_user method, this might be the best solution. The superclass already exists in Rails, so it’s easy to slap another method in there.

However, just because it’s easy doesn’t mean it’s a good idea. Be aware of the trade-off you’re making between convenience and simplicity. Do you already have a huge ApplicationController class? Does everything tend to break whenever you change it? Is it difficult to test the new functionality? If so, then consider one of the other alternatives.

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: How To Implement Simple Authentication Without Devise

Next up: Talking Ruby And TDD With Viking Code School →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.