Ruby Open Classes — Monkey Patching Refinements and Safe Modification Explained
In this tutorial, you will learn about Ruby Open Classes. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby open classes let you reopen and modify any class at any time, including core classes like String and Array, with refinements providing lexically-scoped alternatives for safer code.
What You'll Learn
- How to reopen and modify existing classes
- When monkey patching is appropriate
- Using refinements for scoped modifications
- Best practices for safe class modifications
Why It Matters
Open classes are Ruby's most powerful and dangerous feature. Durga Antivirus Pro uses refinements to add scan methods to String without global pollution. Doda Browser uses monkey patching for cross-cutting logging in development. Understanding when to use each approach separates expert Rubyists from beginners.
Real-World Use
Rails adds dozens of methods to core classes (2.days.ago, "hello".titleize). Testing frameworks use monkey patching to add assertion methods. Gems use refinements to add functionality without conflicts.
flowchart LR
A["Open Classes"] --> B["Monkey Patching"]
B --> C["Refinements"]
C --> D["safe_modify"]
D --> E["Best Practices"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Reopening Classes
You can reopen any class at any time to add or modify methods:
class String
def shout
"#{upcase}!"
end
end
puts "hello".shout # HELLO!
puts "ruby rocks".shout # RUBY ROCKS!
This works because Ruby's class definitions aren't closed — they're executable code that adds to the existing class.
Adding Methods to Existing Classes
class Array
def second
self[1]
end
def middle
self[size / 2]
end
end
arr = [10, 20, 30, 40, 50]
puts arr.second # 20
puts arr.middle # 30
Overriding Existing Methods
class Integer
def +(other)
self - other # Redefine addition as subtraction!
end
end
puts 5 + 3 # 2
This is why monkey patching is dangerous — you can break core language behavior.
Why Monkey Patching is Dangerous
class String
def capitalize
"#{self[0].upcase}#{self[1..-1]}" # Bug: nil when empty
end
end
puts "hello".capitalize # Hello
puts "".capitalize # NoMethodError: undefined method `upcase' for nil
A bug in your patch breaks every capitalize call in your entire application, including gems and Rails internals.
Safe Monkey Patching with Modules
module StringExtensions
def to_slug
downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
end
end
String.include(StringExtensions)
puts "Hello World!".to_slug # hello-world
puts "Ruby 3.3 Features!".to_slug # ruby-33-features
Using include with a module is safer than reopening the class directly.
Refinements (Ruby 2.0+)
Refinements provide scoped, opt-in modifications to classes:
module StringRefinements
refine String do
def shout
"#{upcase}!"
end
def reverse_and_shout
"#{reverse.upcase}!"
end
end
end
class Document
using StringRefinements
def format(text)
text.shout
end
end
puts Document.new.format("hello") # HELLO!
# Outside the scope, String is unchanged
# puts "hello".shout # NoMethodError
Refinements are active only within the lexical scope where using is called. They don't pollute global behavior.
Refinements in a Module
module ArrayStats
refine Array do
def median
sorted = sort
mid = size / 2
size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
end
def mode
group_by(&:itself).max_by { |_, v| v.size }.first
end
end
end
class Analyzer
using ArrayStats
def analyze(data)
puts "Median: #{data.median}"
puts "Mode: #{data.mode}"
end
end
Analyzer.new.analyze([1, 2, 2, 3, 4])
# Median: 2
# Mode: 2
Refinements Can Activate Methods
module IntegerDays
refine Integer do
def days
self * 86400
end
def ago
Time.now - days
end
def from_now
Time.now + days
end
end
end
using IntegerDays
puts 5.days # 432000
puts 2.days.ago # 2026-06-26 12:00:00 (approximately)
This is how Rails' 2.days.ago works, but with refinements instead of global monkey patching.
The super Keyword in Patches
When overriding a method, you can still call the original with super:
class Hash
def [](key)
if key.is_a?(String)
puts "Warning: accessing string key '#{key}'"
end
super
end
end
h = { "name" => "Alice" }
puts h["name"]
# Warning: accessing string key 'name'
# Alice
The prepend Pattern
prepend inserts a module before the class in the method lookup chain, letting you override methods while still calling the original via super:
module LoggingWrapper
def save
puts "[LOG] Saving #{self.class}"
super
puts "[LOG] Saved successfully"
end
end
class User
prepend LoggingWrapper
def save
puts "User saved to database"
end
end
User.new.save
# [LOG] Saving User
# User saved to database
# [LOG] Saved successfully
Open Classes for DSLs
Open classes make Ruby great for DSLs:
class Object
def given(description, &block)
puts "Given: #{description}"
block.call if block
end
def when_action(description, &block)
puts "When: #{description}"
block.call if block
end
def then_expect(description, &block)
result = block.call
status = result ? "PASS" : "FAIL"
puts "Then: #{description} [#{status}]"
end
end
given "a user is logged in" do
user = { name: "Alice", role: "admin" }
when_action "the user views the admin panel" do
result = user[:role] == "admin"
then_expect "they see admin features" do
result
end
end
end
Common Mistakes
1. Patching Core Methods Globally
class String
def length
super * 2 # Breaks everything!
end
end
2. Not Using Refinements for Library Code
# Bad — pollutes globally
class Array
def pluck(method)
map(&method.to_sym)
end
end
# Good — scoped to your code
module MyExtensions
refine Array do
def pluck(method)
map(&method.to_sym)
end
end
end
3. Overwriting Methods Instead of Extending
# Bad — loses original behavior
class String
def upcase
"OVERRIDDEN"
end
end
# Good — extends with super
class String
def upcase
"#{super}!"
end
end
4. Expecting Refinements in called Methods
module MyRefine
refine String do
def dash_case
tr("_", "-")
end
end
end
class Parser
using MyRefine
def parse(text)
text.dash_case # Works here
end
def self.parse(text)
text.dash_case # NoMethodError! Refinements don't apply to class methods
end
end
5. Forgetting Refinements Don't Work in eval
module Test
refine String do
def foo; "bar"; end
end
end
using Test
"hello".foo # Works
"hello".instance_eval { foo } # NoMethodError — refinements don't apply in eval
Practice Questions
1. What is an open class in Ruby?
The ability to reopen and modify any class at runtime, including adding, changing, or removing methods from existing classes like String, Array, or your own classes.
2. What are refinements?
A Ruby 2.0+ feature that allows scoped, opt-in modifications to classes. Refinements are active only within the lexical scope where using is called, preventing global pollution.
3. When should you use refinements vs monkey patching?
Use refinements for library code, production applications, and when you need to add methods to core classes. Use global monkey patching only for debugging, prototyping, or when the change must be application-wide.
4. How does prepend differ from include?
prepend inserts the module before the class in the method lookup chain. This lets the module override class methods while still calling the original via super. include inserts after the class.
Challenge: Create a refinement that adds a to_currency method to Numeric that formats numbers as currency strings (e.g., 1234.5 becomes "$1,234.50").
Solution
module CurrencyFormat
refine Numeric do
def to_currency(unit: "$", precision: 2)
formatted = format("%.#{precision}f", self)
parts = formatted.split(".")
parts[0] = parts[0].reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
"#{unit}#{parts.join(".")}"
end
end
end
using CurrencyFormat
puts 1234.5.to_currency # $1,234.50
puts 1000000.to_currency # $1,000,000.00
puts 0.99.to_currency # $0.99
puts 42.to_currency(unit: "€", precision: 0) # €42
FAQ
{{< faq question="Is monkey patching always bad?" >}} No. Rails famously adds methods to core classes (2.days.ago). The key is using refinements for library code and being careful with global patches. Test thoroughly and document your patches. {{< /faq >}}
{{< faq question="What's the difference between refinements and monkey patching?" >}}
Monkey patching modifies classes globally — every part of your application sees the change. Refinements scope changes to specific lexical contexts using using. Refinements are the safer choice for most cases.
{{< /faq >}}
{{< faq question="Can I use refinements in gems?" >}}
Yes, and you should. Refinements let gems add methods to core classes without affecting the host application. The developer must opt-in with using in their own code.
{{< /faq >}}
{{< faq question="Why does Rails use monkey patching instead of refinements?" >}} Rails was created before refinements existed (Ruby 2.0, 2013). Modern Rails uses refinements in some places but still relies on global patches for backward compatibility and because Rails owns the application layer. {{< /faq >}}
{{< faq question="Can I remove a method I added to an open class?" >}}
You can use remove_method or undef_method. remove_method removes the method from the current class but inherited methods remain. undef_method prevents the method from being called at all.
{{< /faq >}}
Try It Yourself
# open_classes_demo.rb
module SlugExtension
refine String do
def slugify
downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(^-|-$)/, "")
end
end
end
module ArrayExtension
refine Array do
def average
sum.to_f / size
end
def median
sorted = sort
mid = size / 2
size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
end
end
end
using SlugExtension
using ArrayExtension
puts "Hello World!".slugify # hello-world
puts "Ruby 3.3!!".slugify # ruby-33
puts [1, 2, 3, 4, 5].average # 3.0
puts [1, 2, 3, 4, 5].median # 3
puts [1, 2, 3, 4].median # 2.5
puts "hello".respond_to?(:slugify) # true
What's Next
Now that you understand open classes and refinements, apply your knowledge to Enumerable and the powerful collection methods it provides.
| Topic | Description | Link |
|---|---|---|
| Ruby Enumerable | map, select, reduce, group_by | {{< ref "21-enumerable" >}} |
| Ruby Duck Typing | respond_to?, method_missing | {{< ref "15-duck-typing" >}} |
| Python Decorators | Compare with Python's approach | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro