Monkey patching in Ruby done right - refine method with prepend and include
Monkey patching in Ruby is a powerful technique that allows developers to modify the behaviour of classes and modules without directly modifying the original code. In this article, we will explore two methods, prepend/include
, along with the refine
approach to monkey patching in Ruby. We'll discuss their use cases, their behaviours, and the benefits they offer.
One approach to changing the behaviour of a class or module is to change the code directly. Let's look at an example where direct code modification can lead to problems:
class DateTime
def weekend?
sunday? || saturday?
end
end
In the code above, the weekend?
method is added directly to the DateTime
class. While this may seem convenient at first, it can cause problems in the long run. Here are some reasons why directly modifying the code can be problematic:
- Maintainability: Modifying the code directly makes it difficult to track changes and can lead to confusion, especially when multiple developers are involved.
- Compatibility: If the modified class is part of a library or framework, upgrading to newer versions may become challenging due to conflicts with the modified code.
- Codebase Integrity: Changing the code directly can introduce unforeseen bugs or break existing functionality, making it harder to maintain a stable codebase.
The much better approach in this example would be:
# put that in correct folder structure like /lib/core_extensions/datetime/weekend_support.rb
module CoreExtensions
module DateTime
module WeekendSupport
def weekend?
sunday? || saturday?
end
end
end
end
# Actually monkey-patch DateTime
DateTime.include CoreExtensions::DateTime::WeekendSupport
To address these concerns, explore alternative approaches offering better control and maintainability.
Prepend and include
The prepend and include methods in Ruby allow us to add methods to a class or module's method lookup chain, giving the added methods precedence over existing methods. This approach provides a more flexible way to extend or modify existing classes without changing the original code. Here's an example:
module MyNewModule
def say_hello
puts "Hello from MyNewModule!"
end
end
class MySuperClass
prepend MyNewModule
def say_hello
puts "Hello from MySuperClass!"
end
end
MySuperClass.new.say_hello #=> "Hello from MyNewModule!"
In the example above, the say_hello
method of MySuperClass
is overridden by the method defined within MyNewModule
. By using prepend, the added method takes precedence over the original method, resulting in the changed behaviour.
Pros:
- Provides a way to add methods that override existing methods.
- Ensures that the added methods are called before the original methods in the method lookup chain.
- Allows easy extension and modification of existing classes.
Cons:
- The order in which modules are included matters when multiple modules modify the same class, which can lead to complex interactions between methods.
Modifying behavior within limited scope - refine
The refine
method in Ruby allows us to modify the behaviour of a class or module within a limited lexical scope. This approach provides control over the scope of the changes, preventing unintended changes outside the desired scope. Here's an example:
module MyNewModule
refine String do
def to_s
"Hello, #{self}!"
end
def to_i
0
end
end
end
using MyNewModule
puts "World".to_s #=> "Hello, World!"
puts "String".to_i #=> always return 0
In the example above, the refine block is used to modify the behaviour of the String
class in a limited way. The using keyword activates the refinement, causing the to_s
method of String to be overridden by the method defined within MyModule
. The change only affects the code within the block where refine is used.
Pros of using refine
:
- Provides a localized way of modifying the behaviour of classes or modules.
- Allows fine-grained control over the scope of changes.
- Prevents unintentional changes outside the desired scope.
Cons of using refine
:
- Requires the
using
keyword to enable refinement, which can be easily overlooked. - Multiple refine within the same codebase can lead to confusion if not managed carefully.
Summary
Both prepend
and refine
are valuable methods in Ruby for monkey patching. Using prepend
allows for more extensive modifications and flexibility, providing a way to add methods that take precedence over existing methods. On the other hand, refine
offers a more localized and controlled approach, allowing for modifications within limited scopes. By understanding the differences and choosing the appropriate method based on the specific requirements, developers can effectively leverage monkey patching techniques in Ruby.