With Ruby 2.0, a feature called refinements was added to limit the extent of changes that may be made to already-existing classes, making it a safer option than monkey patching.
Concept and Purpose of Refinements in Ruby
Polluting the global scope is the primary problem with traditional Monkey Patching, which involves reopening a class like String or Fixnum to add or modify methods. Your code may encounter conflicts if two distinct modules try to alter the same standard class (stepping on each other’s toes) when you monkey patch globally.
By offering monkey patches with a restricted scope, refinements address this issue. These are methods that are specified in a unique module construct and only work when the using keyword is used to specifically activate them.
Key Characteristics
Limited Scope: Lexical scope applies to refinements. They take effect immediately upon activation (by using the using keyword) and continue to do so until control changes, which often occurs at the conclusion of a module, class, or file definition.
Activation: The using keyword is used to activate them, and it has to be used in a class or module definition.
Definition: The class to be changed is generated after the refine keyword, which is used to build the definitions themselves inside a module. It is only possible to use the refine keyword within a module scope.
You can also read What Are The Ruby Version Management With Code Examples
Implementation and Code Examples
Defining a Refinement
A refinement is defined by creating a module, opening an existing class (such as String or Fixnum), and adding additional methods using the refine keyword.
Code Example 1: Defining a Refinement Module
With the addition of methods like plus_one and concat_one, this module defines a refinement for the Fixnum class:
module Patches
refine Integer do
def plus_one
self + 1
end
def plus(num)
self + num
end
def concat_one
self.to_s + '1'
end
end
end
# To activate the refinement:
using Patches
puts 5.plus_one # => 6
puts 10.plus(5) # => 15
puts 7.concat_one # => "71"
Output
6
15
71
Activating the Refinement (using)
Until the using keyword is used to include the refinement module into a class or module, the methods specified inside it remain inactive.
Code Example 2: Activating Refinement within a Class
The following RefinementTest class makes use of the Patches module, which restricts access to the refined methods to its scope.
module Patches
refine Integer do
def plus_one
self + 1
end
def concat_one
self.to_s + '1'
end
end
end
class RefinementTest
using Patches # Refinements active only inside this class
def initialize
puts 1.plus_one # => 2
puts 3.concat_one # => "31"
end
end
# --- Outside refinement scope ---
begin
puts 1.plus_one
rescue NoMethodError => e
puts e.message # Expected: undefined method `plus_one'
end
# Inside refinement scope
RefinementTest.new
Output
undefined method `plus_one' for an instance of Integer
2
31
The methods are not accessible if the refined module is not used, indicating the narrow scope.
You can also read What Are The XML And RSS Processing In Ruby With Examples
Comparing Refined Scope vs. Unrefined Scope
A basic method such as String#reverse can be modified to provide a more lucid example of scope limitation.
Code Example 3: Modifying String#reverse
The adjustment that follows modifies String#reverse output to “Hell riders“:
module RefiningString
refine String do
def reverse
"Hell riders"
end
end
end
class AClassWithoutMP
def initialize(str)
@str = str
end
def reverse
@str.reverse # Uses original String#reverse
end
end
class AClassWithMP
using RefiningString # Refinement applied only in this class
def initialize(str)
@str = str
end
def reverse
@str.reverse # Uses refined String#reverse → "Hell riders"
end
end
puts AClassWithoutMP.new("hello").reverse # => "olleh"
puts AClassWithMP.new("hello").reverse # => "Hell riders"
Output
olleh
Hell riders
You can also read Date And Time Manipulation In Ruby With Code Examples
Dual-Purpose and Dynamic Refinements
A module that contains refinements can be structured to be used for global monkey patching or limited-scope refinement, depending on how it is loaded. In development or testing situations, when global patching may be required, this is helpful.
Dual-Purpose Modules
Methods wrapped in refine (for scoped inclusion) and regularly declared methods (for global inclusion) can both be included in a module.
Code Example 4: Dual-Purpose Patching
module Patch
# This method can be globally mixed into classes
def patched?
true
end
refine String do
# refinement version of patched?
def patched?
true
end
end
end
# Global patch:
String.include Patch
puts "".patched? # => true
# Refinement scope:
class LoadPatch
using Patch
def test
"".patched? # uses refined version
end
end
puts LoadPatch.new.test # => true
Output
true
true
Dynamic Refinements
Using is static and can only be called in a class or module definition. One intricate method to deal with load order problems (such as when refinement files are not loaded first) is to encapsulate the patched class/module definition in a Proc. You can use send :refine to dynamically invoke the refine method itself.
Code Example 5: Dynamic Refinement Activation
This example demonstrates how using can be called inside a loop and accept a variable referring to a module:
module Patch
def patched?
true
end
refine String do
def patched?
true
end
end
end
PATCH_CLASSES = [Patch] # Use a constant instead of a local variable
class Patched
PATCH_CLASSES.each { |klass| using klass }
def test
"".patched?
end
end
puts Patched.new.test # => true
Output
true
You can also read What Is Mean By Introspection And Singleton Classes In Ruby
