Skip to Content
Course content

194: Class Reopening and Monkey Patching Risks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen dozens of times in professional codebases: a developer discovers they are repeatedly calling a specific set of transformations on a string—maybe converting a title to a URL-friendly slug—and they think, "Why am I passing this string into a helper method? It would be so much more elegant if I could just call string.to_slug."

The "Clean Code" Trap of Extending Core Classes

The misconception here is that adding methods directly to core classes like String, Array, or Integer is the "Ruby way" because it makes the code more readable and object-oriented. On the surface, it looks beautiful. You get to write "My Great Post".to_slug instead of SlugHelper.format("My Great Post"). It feels like you're enhancing the language to fit your domain.

# This looks elegant, right?
class String
  def to_slug
    self.downcase.gsub(/[^a-z0-9]+/, '-')
  end
end

puts "Hello World!".to_slug # => "hello-world"

Here is why that thinking is dangerous. You aren't just adding a method to your version of a string; you are modifying the String class for the entire Ruby process. Every single piece of code running in that memory space—including the Rails framework, your database driver, and every third-party gem you've installed—now sees your version of String.

Why Global Overwrites are a Production Nightmare

The real world isn't a vacuum. Imagine you've implemented String#to_slug as shown above. Six months later, you add a new gem to your project for SEO optimization. Unknown to you, that gem also monkey-patches String to add its own to_slug method, but it handles special characters differently.

# Inside some third-party gem you just installed...
class String
  def to_slug
    # This gem uses a different regex and handles accents
    self.downcase.unicode_normalize(:nfkd).gsub(/[^\x00-\x7F]+/, '').gsub(/[^a-z0-9]+/, '-')
  end
end

Now, your original implementation is gone. Completely wiped out. Because Ruby allows classes to be reopened, the last definition of a method wins. If the gem loads after your code, your logic is overwritten. If your code loads after the gem, you've broken the gem's internal logic. This creates "heisenbugs"—errors that appear or disappear depending on the order in which files are required. I once spent an entire weekend debugging a production crash that was caused by two different gems fighting over how Hash#to_xml should behave.

Using Refinements for Surgical Precision

If you absolutely must add functionality to a core class, don't reopen the class globally. Use Refinements. Refinements allow you to "activate" your changes only in a specific file or module, leaving the rest of the application untouched. It's like a localized monkey patch.

module Slugification
  refine String do
    def to_slug
      self.downcase.gsub(/[^a-z0-9]+/, '-')
    end
  end
end

# The method doesn't exist here
# "Hello".to_slug # This would raise a NoMethodError

class Post
  using Slugification # This activates the refinement ONLY in this class

  def generate_url(title)
    title.to_slug # This works!
  end
end

# Outside the Post class, String remains untouched.

By using refine and using, you get the syntactic sugar you wanted without the risk of breaking a dependency. You're telling Ruby: "I want these extra methods, but only right here, in this specific context." It's a professional compromise that keeps your global namespace clean.




📋 Practical Task

Resolving the Conflicting Currency Formatter

You are working on a legacy codebase where two different developers have monkey-patched the Integer class to add a to_currency method. This is causing inconsistent formatting across the app. Your task is to remove the global monkey patches and implement the formatting using a Refinement so it can be used safely within the Invoice class.

Current Broken State:

class Integer
  def to_currency
    "$#{self}.00"
  end
end

# Some other part of the app accidentally does this:
class Integer
  def to_currency
    "USD #{self}"
  end
end

class Invoice
  def print_total(amount)
    puts "Total: #{amount.to_currency}"
  end
end

# Expected result for amount 100: "Total: $100.00"
# Actual result: "Total: USD 100"

Requirements:

  • Remove the class Integer blocks entirely.
  • Create a module named CurrencyFormatting that uses refine Integer to implement the to_currency method (returning the "$#{self}.00" format).
  • Modify the Invoice class to use this refinement so that print_total works as expected without affecting the global Integer class.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.