35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Refinements: Scoped Monkey Patching Zonder Risico

Ruby Refinements: Scoped Monkey Patching Zonder Risico

roger
Ruby refinements laten je klassen aanpassen binnen een gecontroleerde scope in plaats van globaal. Leer wanneer je ze gebruikt, hoe ze onder de motorkap werken, en waar ze tekortschieten in productie Ruby 3.3+ applicaties.

Ruby refinements geven je monkey patching met een noodstop. In plaats van een klasse globaal aan te passen (en maar te hopen dat niemand anders dezelfde methode definieert), beperken refinements je wijzigingen tot het bestand of de module waar je ze activeert. Ze zijn beschikbaar sinds Ruby 2.0, waren jarenlang “experimenteel,” en zijn vanaf Ruby 3.1+ een stabiele, volledig ondersteunde feature.

De korte versie: gebruik refine in een module, activeer die module met using waar je het aangepaste gedrag nodig hebt. Buiten die scope blijft de originele klasse onaangetast.

Waarom Globale Monkey Patching Dingen Kapot Maakt

Als je aan een Rails app hebt gewerkt met meer dan een handvol gems, ben je waarschijnlijk een monkey patching-conflict tegengekomen. Twee gems herdefiniëren dezelfde methode op String of Hash, en plotseling breekt er een op manieren die uren debuggen kosten.

# gem_a/lib/gem_a.rb
class String
  def truncate(length)
    self[0...length]
  end
end

# gem_b/lib/gem_b.rb
class String
  def truncate(length, omission: "...")
    "#{self[0...length]}#{omission}"
  end
end

Welke gem als laatste laadt, wint. De andere breekt stilletjes. Ik heb een hele middag besteed aan het opsporen van een bug veroorzaakt door precies dit patroon in een productie-app — een betaalverwerking-gem en een tekstopmaak-gem die allebei String#to_formatted_s patchten.

Hoe Refinements Werken

module StringTruncation
  refine String do
    def truncate(length, omission: "...")
      return self if self.length <= length
      "#{self[0...(length - omission.length)]}#{omission}"
    end
  end
end

Er gebeurt nog niets. De refinement zit slapend totdat je hem activeert:

class ArticleFormatter
  using StringTruncation

  def format_preview(text)
    text.truncate(100)  # gebruikt onze refined versie
  end
end

# Buiten ArticleFormatter:
"hello".truncate(3)  # NoMethodError — String heeft geen truncate methode

De scoping is strikt. using activeert de refinement alleen binnen die klasse of dat bestand. Andere bestanden, andere klassen, andere gems — die zien het nooit.

Scoping Regels

Refinements volgen lexicale scope, niet dynamische scope. Dit verrast mensen:

module MyRefinement
  refine Integer do
    def positive_or_zero
      self < 0 ? 0 : self
    end
  end
end

class Calculator
  using MyRefinement

  def clamp_value(n)
    n.positive_or_zero  # werkt
  end

  def process(&block)
    block.call  # refinement NIET actief in het block
  end
end

Calculator.new.process { -5.positive_or_zero }  # NoMethodError

Het block werd gedefinieerd buiten de lexicale scope waar using werd aangeroepen. Dit is opzettelijk — het voorkomt dat refinements lekken via indirecte aanroepen. Maar het betekent dat je geen blocks kunt doorgeven vanuit ongeraffineerde code en verwachten dat refinements van toepassing zijn.

Refinements vs. Prepend

Ruby’s Module#prepend is het andere gereedschap om klassen veilig uit te breiden. De keuze is niet altijd voor de hand liggend.

Gebruik prepend wanneer je globale gedragswijziging wilt met een nette override-keten:

module LoggedSave
  def save(**options)
    Rails.logger.info("Saving #{self.class}##{id}")
    super
  end
end

ActiveRecord::Base.prepend(LoggedSave)

Gebruik refinements wanneer je de wijziging beperkt wilt houden tot specifieke code en onzichtbaar wilt houden voor al het andere. Dit is gebruikelijk voor DSL’s, test-helpers, of domeinspecifieke uitbreidingen die de globale namespace niet moeten vervuilen.

In de praktijk vertrouwt Rails zwaar op prepend voor zijn interne architectuur. Refinements zijn beter voor applicatiecode waar je strakkere controle wilt. Als je geïnteresseerd bent in hoe Ruby methoden oplost via de ancestor chain, behandelt het delegatie-patronen artikel verwant terrein.

Praktijkvoorbeeld: Geldbedragen Formatteren

Hier is een patroon dat ik gebruik in productie. Verschillende delen van een e-commerce app hebben verschillende getalopmaak nodig — het adminpaneel wil ruwe getallen, de webshop wil valutaopmaak, de API wil centen als integers.

module CurrencyFormatting
  refine Numeric do
    def to_eur
      format("€%.2f", self / 100.0)
    end

    def to_price_display
      formatted = format("%.2f", self / 100.0)
      parts = formatted.split(".")
      "€#{parts[0].reverse.gsub(/(\d{3})(?=\d)/, '\\1.').reverse},#{parts[1]}"
    end
  end
end

# app/views/helpers/storefront_helper.rb
module StorefrontHelper
  using CurrencyFormatting

  def display_price(cents)
    cents.to_price_display  # "€1.234,56"
  end
end

De API serializers en admin controllers zien nooit to_eur of to_price_display. Geen onbedoelde opmaak in JSON responses. Geen conflicten met andere gems.

Performance

Refinements voegen vrijwel geen runtime overhead toe aan method dispatch. Ruby’s method cache behandelt refined methoden hetzelfde als reguliere methoden binnen de geactiveerde scope.

Benchmark op Ruby 3.3.0:

Reguliere method call:    48.2M iteraties/sec
Refined method call:      47.8M iteraties/sec
Monkey-patched method:    48.1M iteraties/sec

Het verschil is ruis. De enige kost zit in method cache invalidatie bij het laden — in een typische Rails app met een handvol refinements is dit onmeetbaar.

Waar Refinements Tekortschieten

Geen send of method ondersteuning: obj.send(:refined_method) en obj.method(:refined_method) werken niet met refinements in Ruby 3.3. Dit breekt code die dynamische dispatch gebruikt.

using StringTruncation

"hello".truncate(3)          # werkt
"hello".send(:truncate, 3)   # NoMethodError
"hello".method(:truncate)    # NameError

Dit is de belangrijkste reden dat refinements niet breder worden toegepast.

Geen overerving-activering: Als klasse B erft van klasse A, en A gebruikt een refinement, krijgt B de refinement niet automatisch.

Debuggen kan verwarrend zijn: Wanneer een methode in de ene context bestaat maar niet in een andere, kunnen stack traces verwarrend zijn voor ontwikkelaars die niet bekend zijn met refinements.

Wanneer Refinements Gebruiken

Na het gebruik van refinements in productie in meerdere Ruby 3.x apps, zijn dit de gevallen waar ze hun waarde bewijzen:

  • Test helpers die core klassen uitbreiden met assertion-vriendelijke methoden
  • View-laag opmaak waar je convenience methoden wilt op String, Numeric, of Date zonder models of controllers te vervuilen
  • DSL constructie waar je tijdelijke syntax sugar nodig hebt
  • Gem ontwikkeling waar je absoluut geen conflicten met gebruikerscode kunt riskeren

Voor alles met dynamische dispatch, metaprogramming, of cross-cutting concerns (logging, caching, memoization patronen), blijf bij prepend of gewone modules.

FAQ

Zijn Ruby refinements hetzelfde als monkey patching?

Nee. Monkey patching wijzigt een klasse globaal voor alle code in het proces. Refinements wijzigen een klasse alleen binnen de lexicale scope waar using wordt aangeroepen. Code buiten die scope ziet de originele, ongewijzigde klasse.

Kan ik refinements gebruiken in Rails applicaties?

Ja. Refinements werken in elke Ruby 2.0+ applicatie, inclusief Rails. Ze zijn nuttig in helpers, service objects, en formatters. De belangrijkste kanttekening is dat Rails’ eigen internals send en dynamische dispatch veel gebruiken, dus wees voorzichtig met het verfijnen van klassen waar Rails op reflecteert.

Beïnvloeden refinements de performance?

Niet meetbaar. Method dispatch voor refined methoden draait op dezelfde snelheid als reguliere method calls. De enige overhead is een iets grotere method cache footprint tijdens het opstarten van de applicatie.

Waarom gebruiken niet meer Ruby developers refinements?

De send en method beperkingen zijn de voornaamste reden. Veel idiomatische Ruby code gebruikt dynamische dispatch, en refinements werken daar stilletjes niet mee. De scoping regels verrassen ook ontwikkelaars die verwachten dat block-gebaseerde activering door closures propageert.

Hoe werken refinements samen met Ruby’s method lookup chain?

Refinements worden gecontroleerd vóór de normale method lookup chain, maar alleen binnen hun actieve scope. Wanneer using actief is, controleert Ruby eerst refined methoden, dan de reguliere ancestor chain (prepended modules, de klasse zelf, included modules, superklassen). Buiten de using scope slaat Ruby de refinement-controle over en gaat direct naar de normale chain.

#ruby #metaprogramming #best-practices
r

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation