Refinements — Scoped Monkey Patching
Monkey patching is a fact of life in Ruby — sometimes you just need String to have a method it doesn’t. The problem is that patching String globally affects every piece of code in your project, including code you don’t control. Refinements solve this by limiting your patches to a specific lexical scope.
Defining a Refinement
Use Module#refine inside a module to define what you’re changing:
module StringExtensions
refine String do
def shout
"#{self.upcase}!"
end
def title_case
split.map(&:capitalize).join(" ")
end
end
end
refine creates an anonymous module holding your modifications. It doesn’t affect anything until you activate it.
Activating with using
using StringExtensions
"hello world".shout # => "HELLO WORLD!"
"hello world".title_case # => "Hello World"
using activates the refinements in the current lexical scope — from that line to the end of the file, or to the end of the current class/module definition.
Outside that scope, the original String behavior applies:
using StringExtensions
"hello".shout # => "HELLO!"
# This file doesn't have `using`, so refinements are not active here
require "./other_file" # other_file sees original String
Scope Rules
Refinements are lexical. That has several practical consequences:
You can’t activate them inside a method:
class MyClass
def activate_shout
using StringExtensions # SyntaxError: refinements cannot be used here
end
end
Refinements are active until the end of the class or file:
module MyHelpers
using StringExtensions
def self.process(text)
text.shout # refinement is active here
end
end
"hello".shout # NoMethodError — outside the module scope
Reopening a class doesn’t reactivate your refinement:
module MyExtensions
refine Integer do
def factorial
self <= 1 ? 1 : self * (self - 1).factorial
end
end
end
using MyExtensions
class Foo
5.factorial # works — inside same file as `using`
end
class Foo
10.factorial # still works — same top-level lexical scope
end
Method Lookup Order
When Ruby looks up a method on an object, it checks in this order:
- Refinements (most recently activated takes priority)
- Prepended modules
- The class itself
- Ancestors
Refinements always win over prepended modules, and prepended modules always win over the class. This means your refinement takes precedence over both a prepend and the original method.
Refinements in Practice
One common use is test helpers or debugging tools that want to add methods without polluting global scope:
module DebugRefinements
refine Hash do
def inspect_keys(*keys)
select { |k, _| keys.include?(k) }.inspect
end
end
end
using DebugRefinements
{ name: "Alice", age: 30, role: "admin" }.inspect_keys(:name, :role)
# => "{:name=>\"Alice\", :role=>\"admin\"}"
Gotchas
Code before using doesn’t get the refinement:
"hello".shout # NoMethodError
using StringExtensions
"hello".shout # "HELLO!"
Refinements don’t affect BasicObject methods. You can’t refine methods like object_id or send that exist on BasicObject.
Performance is slower than direct method calls. Each method lookup in a refined scope adds overhead. Negligible in most application code, measurable in tight loops.
Calling self inside a refinement uses the refinement’s version:
module MyRefinements
refine String do
def shout
# If upcase is also refined, this calls the refined upcase
"#{upcase}!"
end
end
end
When to Use Refinements
Refinements are worth reaching for when:
- You’re writing a test helper that adds debugging methods — keep it in the test file
- You’re building a DSL and want clean syntax without polluting the global namespace
- You need to temporarily patch a gem’s behavior without affecting other parts of your codebase
For permanent application-level patches to core classes, a standard monkey patch in an initializer is simpler and faster.
See Also
- Ruby Open Classes — how monkey patching works and why it matters
- Ruby Pattern Matching — advanced destructuring and control flow in Ruby 3.0