Search code examples
rubyoopmonkeypatching

Organizing monkey patches


I read a blog post that recommends namespacing your monkey patches so they can be easily viewed and included.

For example:

module CoreExtensions
  module DateTime
    module BusinessDays
      def weekday?
        !sunday? && !saturday?
      end
    end
  end
end

Would go in the: lib/core_extensions/class_name/group.rb file.

It can be included in the DateTime class with the Module#include instance method (which a class inherits because a Class is a Module)

# Actually monkey-patch DateTime
DateTime.include CoreExtensions::DateTime::BusinessDays

My question is where do the include statements go? Is there a convention?

For example:

I have the following monkey patches:

# http://www.justinweiss.com/articles/3-ways-to-monkey-patch-without-making-a-mess/
module CoreExtensions
  module String
    module Cases
      def snakecase
        return self if self !~ /[A-Z]+.*/
        # http://rubular.com/r/afGWPWLRBB
        underscored = gsub(/(.)([A-Z])/, '\1_\2')
        underscored.downcase
      end

      def camelcase
        return self if self !~ /_/ && self =~ /[A-Z]+.*/
        split('_').map{ |e| e.capitalize }.join
      end
    end
  end
end

That live inside the lib/core_extensions/string/cases.rb file.

Where should I put my String.include CoreExtensions::String::Cases statement?

Also to be clear this is just a ruby project, does that make a difference?

I've tried putting it inside lib/devify.rb

require 'devify/version'
require 'devify/some_dir'
require 'devify/scaffold'
require 'devify/tree_cloner'
require 'devify/renderer'
require 'devify/project'

require 'devify/tasks/some_task'
require 'devify/tasks/bootstrap'

require 'core_extensions/string/cases'

module Devify
  String.include CoreExtensions::String::Cases
end

This works and it makes sense why it works. It's because my entire app lives inside the Devify module or namespace.

This way is also good because I'm not polluting the global namespace correct? Because I'm only monkey patching Strings that live inside Devify?

Just not sure not if this is the right way to go about it.


Solution

  • It doesn't matter where you put the include call.

    Calling String.include will always monkey patch the one String class that is used by all the strings in the entire object space. So best put the instruction at the top level as to not mislead readers of the code.

    Monkey patching is always global.

    It is a powerful feature and can be used for good.

    If you are authoring a gem be aware that you're sharing a global namespace with others. The same is also true for top-level modules and even the gem name though. Shared namespaces are just a reality of shared code.

    If you are looking for lexically scoped monkey patches look into the new refinement feature that was introduce with Ruby 2.

    Refinements are an idea taken from Smalltalk's class boxes. Refinements are not without their own issues though, for example they lack support for introspection and reflection. Thus essentially making them stealth and unfit for production use.

    If you are looking to limit the monkey patches to some string object only, consider either subclassing String or calling extend on an instance.