The Art of Monkey Patching in Ruby

chaitalikhangar

Chaitali Khangar

Posted on March 26, 2024

The Art of Monkey Patching in Ruby

Image description

Ruby, celebrated for its dynamic nature and Metaprogramming capabilities, offers a wealth of powerful features that are often underutilized or misunderstood.

In this blog, we'll shed light on a fascinating topic within Ruby known as Monkey Patching.

We are going to cover:

  • What is Monkey Patching?
  • How can we apply Monkey Patching?
  • What happens when Monkey Patching goes wrong?
  • Is there an alternative approach?

Let's wear our Ruby hats and start diving into the concept of Monkey Patching.

What is Monkey Patching?

Ruby is a dynamic programming language and its interpreted language which gives you the ability to write or modify the code at Runtime.

Imagine you could add new abilities to existing things in Ruby.

That's what open classes or Monkey Patching.

Monkey Patch, lets you change or add things to existing stuff, like teaching a phone new tricks.

Why do we use it?

When you want to make something better without changing the original code.

How can we apply Monkey Patching?

Now we know what it is, let's dive into coding.

require 'date'

# TO check if date class has leap_year? method or not
p Date.methods.include?(:leap_year?)
# Output: false

class Date
  def leap_year?
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
  end
end

date = Date.new(2023, 10, 29)
p date.leap_year?
# Output: false

date = Date.new(2024, 2, 29)
p date.leap_year?
# Output: true
Enter fullscreen mode Exit fullscreen mode

In this example, we open the Date class, that's why it is called an Open Class.

You are adding a new leap_year? method here that is called Monkey Patching.

What happens when Monkey Patching goes wrong?

While open classes are powerful, using them too much or in the wrong way can make things confusing or unpredictable.

Don't change the main things without thinking, as it might make others confused or stop things from working together.

Now let's understand this by example.

# Check if a method exists
String.methods.include?(:to_alphanumeric)

# Redefine class
class String
  def to_alphanumeric
    gsub(/[^\w\s]/, '')
  end
end

p "Hello, @World!".to_alphanumeric
# Output: Hello World

p "Ruby is #1 for coding!".to_alphanumeric
# Output: Ruby is 1 for coding
Enter fullscreen mode Exit fullscreen mode

Now whenever you are calling the to_alphanumeric method on the String class you will get the same result.

This changes the global definition of the to_alphanumeric method of the String class.

Now consider third-party applications/gems if they have the same method to_alphanumeric for the String class, then it will behave differently, it will use your code instead of executing third-party applications or gem code.

We don't want this, to impact globally.

How can we solve this?

Is there an alternative approach?

Ruby 2.0 came up with a great feature to solve this issue. It's called Refinement.

What exactly is 'Refinement' all about?

It allows you to modify the behavior of a class or module temporarily while ensuring it doesn't impact the global definition.

Provides a way to extend a class locally.

Refinements can modify both classes and modules.

How can we implement the Refinement feature?

module ToJSON
  refine Hash do
    def to_json
      '{' + map { |k, v| k.to_s.dump + ':' + v.to_s.dump }.join(',') + '}'
    end
   end
end

hash = {1=>2}
p hash.to_json

# Output: undefined method `to_json' for {1=>2}:Hash (NoMethodError)
Enter fullscreen mode Exit fullscreen mode

Oops, an error is thrown.

What's the reason behind the error? The reason is it does not modify the global definition for the Hash class.

But hold on, we need these changes for the 'Hash' class, to fix this we must utilize the 'using' keyword.

module ToJSON
  refine Hash do
    def to_json
      '{' + map { |k, v| k.to_s.dump + ':' + v.to_s.dump }.join(',') + '}'
    end
   end
end

module ConvertToJson
  using ToJSON
  def self.convert(hash)
    p hash.to_json
  end
end

hash = {1=>2}
ConvertToJson.convert(hash)
# Output: "{\"1\":\"2\"}"
Enter fullscreen mode Exit fullscreen mode

Yay, we've accomplished this without affecting the global definition of Hash.

Let's discuss the Scope of Refinement:

  • It's in the refine block itself.
  • Code starts from the place where you call using until the end of the module.

Now you understand how Monkey Patching works, why Refinement is introduced, and how you can use it.

Refactor your Monkey Patches or Refinements, and review your code to ensure they still serve your project's goals.

If you want to use it, go ahead as now you know how you can use Refinement to prevent global scope.

Till we meet next time, Happy Coding!!

💖 💪 🙅 🚩
chaitalikhangar
Chaitali Khangar

Posted on March 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related