Open Classes and Monkey Patching in Ruby
One of Ruby’s most distinctive features is that classes are never truly closed—you can add methods to any class at any time, even built-in ones like String or Array. This capability, known as open classes, is incredibly powerful but comes with significant responsibility. This guide explores how open classes work, why they exist, and how to use them safely.
What Are Open Classes?
In languages like Java or C++, once a class is defined, you cannot add new methods to it. Ruby takes a different approach: any class can be reopened and modified at any point in your program. This is called “open classes.”
class String
def shout
upcase + "!"
end
end
"hello".shout # => "HELLO!"
We just added a shout method to Ruby’s built-in String class. This works because in Ruby, classes are objects—specifically, instances of Class. When you reference a class that hasn’t been defined, Ruby creates it. When you reference an existing class, Ruby simply reopens it.
How Open Classes Work
Every class in Ruby has a constant that points to a Class object. When you use the class keyword with an existing class name, Ruby doesn’t create a new class—it reopens the existing one and adds whatever you define inside the block.
# This doesn't create a new Array
# It reopens the existing Array class
class Array
def summarize
"Array with #{length} elements"
end
end
[1, 2, 3].summarize # => "Array with 3 elements"
This mechanism is the foundation for many of Ruby’s dynamic features and is heavily used by frameworks like Rails.
Monkey Patching
When you modify a built-in class like String, Integer, or Hash, you’re engaging in what’s called “monkey patching.” The term carries a somewhat negative connotation because of the risks involved, but it’s a powerful technique when used carefully.
A Practical Example
Rail’s ActiveSupport extends Ruby core classes with useful methods:
# ActiveSupport adds many methods to core classes
"hello".present? # => true
"".present? # => false
nil.present? # => false
[1, 2, 3].present? # => true
[].present? # => false
{}.present? # => false
{a: 1}.present? # => true
These extensions make code more readable and are considered “acceptable” monkey patching because they’re well-documented, widely-used, and namespaced to avoid conflicts.
The Dangers of Monkey Patching
While open classes are powerful, they can cause serious problems if used carelessly.
1. Method Collisions
If two different libraries add the same method to a class, one will override the other, potentially breaking functionality:
# Library A adds this method
class String
def slugify
downcase.gsub(" ", "-")
end
end
# Library B adds a different implementation
class String
def slugify
downcase.tr(" ", "_")
end
end
# Whichever loaded last wins
"Hello World".slugify # => "hello_world" (or "hello-world")
2. Unexpected Behavior
Modifying core classes can lead to subtle bugs that are hard to track down:
class Array
def first
"custom first"
end
end
# Now every array's 'first' method is changed
[1, 2, 3].first # => "custom first"
[].first # => "custom first"
This breaks the expected behavior of Array#first throughout your entire application.
3. Confusing Code
When you modify core classes, anyone reading your code might be confused about where methods come from:
# Is this a built-in method or did someone modify String?
"my string".some_custom_method
Safe Approaches to Open Classes
Here are strategies to harness open classes safely.
1. Use Refinements (Ruby 2.0+)
Refinements let you modify classes in a scoped, temporary way:
module StringExtensions
refine String do
def shout
upcase + "!"
end
end
end
# Only available within the using block
class MyApp
using StringExtensions
def greet
"hello".shout # => "HELLO!"
end
end
# Outside the refinement scope, String is unchanged
"hello".shout # => NoMethodError
Refinements solve many monkey patching concerns by limiting the scope of modifications.
2. Subclass Instead
When possible, create your own class that extends the built-in one:
class CustomArray < Array
def summarize
"Array with #{length} elements"
end
end
# Original Array is unchanged
[1, 2, 3].summarize # => NoMethodError
CustomArray.new([1, 2, 3]).summarize # => "Array with 3 elements"
3. Use Module Prepending (Ruby 2.0+)
Prepending a module lets you override methods while calling the original:
module StringReverser
def reverse
"Reversed: #{super}"
end
end
class String
prepend StringReverser
end
"hello".reverse # => "Reversed: olleh"
This approach maintains access to the original method via super.
4. Namespace Your Extensions
Create a module that wraps the extended functionality:
module MyApp
module StringExtensions
def summarize
"#{self} (#{length} chars)"
end
end
end
# Use explicitly
"hello".extend(MyApp::StringExtensions).summarize
# => "hello (5 chars)"
When to Use Open Classes
Open classes are appropriate when:
- Building a framework that needs to extend core classes (like Rails does)
- Adding domain-specific methods for a specific application
- Creating internal extensions with clear documentation
- Using refinements for temporary, scoped modifications
When to Avoid Open Classes
Avoid open classes when:
- You need to add utility methods—consider module functions instead
- You’re working on code others will maintain
- The modification would change expected core behavior
- You can solve the problem with inheritance or composition
Summary
Open classes are one of Ruby’s most powerful features, enabling the dynamic metaprogramming that makes Rails and other frameworks possible. However, with great power comes great responsibility:
- Open classes let you modify any class at any time
- Monkey patching core classes can cause conflicts and bugs
- Use refinements, subclasses, prepending, or namespacing for safer modifications
- Always consider the maintainability implications before modifying built-in classes
Used judiciously, open classes become an invaluable tool in your Ruby toolkit.