Ruby method_missing: Wanneer Gebruiken en Wanneer Hard Wegrennen
Wanneer je een methode aanroept die niet bestaat op een Ruby-object, crasht Ruby niet zomaar. Het roept method_missing aan op dat object, met de methodenaam en argumenten. Deze hook laat je ongedefinieerde methode-aanroepen onderscheppen en zelf afhandelen.
class FlexibleConfig
def initialize(data)
@data = data
end
def method_missing(name, *args)
key = name.to_s
if @data.key?(key)
@data[key]
else
super
end
end
def respond_to_missing?(name, include_private = false)
@data.key?(name.to_s) || super
end
end
config = FlexibleConfig.new("host" => "localhost", "port" => 5432)
config.host # => "localhost"
config.port # => 5432
config.nope # => NoMethodError
Die super-aanroep op regel 10 is cruciaal. Zonder die aanroep slik je elke ontbrekende methode stilzwijgend in, en wordt debuggen een nachtmerrie.
Waarom respond_to_missing? Verplicht Is
Elke method_missing-implementatie heeft een bijpassende respond_to_missing? nodig. Dit is geen suggestie — het is een contract. Zonder liegt je object over zijn mogelijkheden:
config = FlexibleConfig.new("host" => "localhost")
# Zonder respond_to_missing?:
config.host # => "localhost" (werkt)
config.respond_to?(:host) # => false (kapot!)
method(:host) # => NameError (kapot!)
# Met respond_to_missing?:
config.respond_to?(:host) # => true (correct)
config.method(:host) # => #<Method: ...> (correct)
Rails zelf had dit fout in vroege versies. De respond_to_missing?-methode werd toegevoegd in Ruby 1.9.2 specifiek omdat respond_to? en method_missing die niet synchroon liepen echte bugs veroorzaakten in grote libraries. Zie Ruby core issue #3008 voor de oorspronkelijke discussie.
De regel: als je method_missing override, override je respond_to_missing? met dezelfde logica. Altijd.
Praktijkvoorbeelden Die Zinvol Zijn
Proxy-objecten
Het sterkste gebruik van method_missing is het bouwen van proxy-objecten die doorverwijzen naar iets anders. Ruby’s Delegator- en BasicObject-klassen bestaan deels voor dit patroon:
class TimedProxy
def initialize(target, logger:)
@target = target
@logger = logger
end
def method_missing(name, *args, **kwargs, &block)
if @target.respond_to?(name)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = @target.public_send(name, *args, **kwargs, &block)
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
@logger.debug("#{@target.class}##{name} duurde #{(elapsed * 1000).round(1)}ms")
result
else
super
end
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name, include_private) || super
end
end
ActiveRecord’s association proxies gebruiken dit patroon intern. Wanneer je user.posts.published aanroept, onderschept de association proxy published via method_missing en stuurt het door naar de scope.
Dynamische Finders (Het Klassieke Voorbeeld)
Vóór Rails 4 de meeste dynamische finders afschreef, werkten find_by_email, find_by_name_and_age en vergelijkbare methodes allemaal via method_missing:
# Vereenvoudigde versie van hoe Rails 3 het deed
def method_missing(name, *args)
if name.to_s =~ /^find_by_(.+)$/
columns = $1.split("_and_")
where(columns.zip(args).to_h).first
else
super
end
end
Rails schakelde over naar expliciete find_by(email: ...)-aanroepen. Die verschuiving is leerzaam — zelfs het Rails-team besloot dat de afweging het niet waard was voor de meeste gevallen.
Builder/DSL-patronen
method_missing werkt goed voor XML/HTML-builders en DSL-constructie:
class HtmlBuilder
def initialize
@html = +""
end
def method_missing(tag, content = nil, **attrs, &block)
attr_str = attrs.map { |k, v| %( #{k}="#{v}") }.join
@html << "<#{tag}#{attr_str}>"
if block
nested = self.class.new
nested.instance_eval(&block)
@html << nested.to_s
elsif content
@html << content.to_s
end
@html << "</#{tag}>"
self
end
def respond_to_missing?(*, **)
true # Elke tagnaam is geldig
end
def to_s
@html
end
end
Nokogiri’s Builder en Markaby gebruiken beide variaties van deze aanpak.
De Performance-kosten
method_missing is trager dan reguliere method dispatch. Hier een benchmark op Ruby 3.3.6 met YJIT ingeschakeld:
require "benchmark/ips"
class Direct
def greet = "hello"
end
class Dynamic
def method_missing(name, *)
"hello" if name == :greet
end
def respond_to_missing?(name, *) = name == :greet
end
Benchmark.ips do |x|
direct = Direct.new
dynamic = Dynamic.new
x.report("direct") { direct.greet }
x.report("method_missing") { dynamic.greet }
x.compare!
end
Resultaten op een M2 MacBook Pro:
direct: 42.3M i/s
method_missing: 8.7M i/s - 4.86x langzamer
Dat is ruwweg 5x trager per aanroep. Voor een configuratie-object dat eenmaal bij opstarten wordt benaderd: irrelevant. Voor iets dat in een strakke loop duizenden records verwerkt, telt het snel op.
YJIT kan method_missing niet optimaliseren zoals het reguliere methode-aanroepen optimaliseert, omdat de doelmethode niet bekend is bij compilatie. Dit verschil zal waarschijnlijk blijven bestaan over Ruby-versies heen.
De define_method-ontsnapping
Als je method_missing nodig hebt voor discovery maar snelle herhaalde toegang wilt, definieer dan de methode bij de eerste aanroep:
class CachedConfig
def initialize(data)
@data = data
end
def method_missing(name, *args)
key = name.to_s
if @data.key?(key)
# Definieer de methode zodat de volgende aanroep snel is
self.class.define_method(name) { @data[key] }
@data[key]
else
super
end
end
def respond_to_missing?(name, include_private = false)
@data.key?(name.to_s) || super
end
end
Eerste aanroep gaat via method_missing. Elke volgende aanroep raakt de gedefinieerde methode op volle snelheid. ActiveRecord gebruikt deze techniek voor attribute-accessors — de eerste toegang tot user.name definieert een echte methode, zodat de tweede toegang een normaal methode-aanroep is.
Wanneer Iets Anders Gebruiken
In de meeste gevallen dat je naar method_missing grijpt, bestaat er een simpeler alternatief:
| Situatie | Gebruik in plaats van method_missing |
|---|---|
| Doorverwijzen naar ander object | Forwardable of delegate in Rails |
| Dynamische attribute-toegang | define_method bij class load |
| Config/settings-object | Struct, Data of OpenStruct |
| Een paar bekende dynamische methodes | Definieer ze expliciet met define_method |
OpenStruct zelf gebruikt method_missing onder de motorkap, maar het handelt alle randgevallen voor je af. In Ruby 3.0+ geeft OpenStruct ook een deprecation-waarschuwing in bepaalde contexten vanwege de performance-overhead — dat zegt iets over het huidige denken van het Ruby core team over dit patroon.
Als je alle mogelijke methodenamen kent bij class-definitie, gebruik dan define_method in een loop:
class Config
FIELDS = %w[host port database username password].freeze
FIELDS.each do |field|
define_method(field) { @data[field] }
end
def initialize(data)
@data = data
end
end
Geen method_missing nodig. Volledige YJIT-optimalisatie. Correct respond_to?-gedrag gratis. Methodes verschijnen in instance_methods. Debuggen is overzichtelijk.
Debuggen van method_missing Die Fout Gaat
Het ergste bug-patroon met method_missing is de oneindige loop. Het gebeurt wanneer method_missing per ongeluk een andere ontbrekende methode aanroept:
# DOE DIT NIET
def method_missing(name, *args)
if name.to_s.start_with?("find_")
# Oeps: 'log' is ook ongedefinieerd, triggert method_missing opnieuw
log("Looking up #{name}")
# ...
end
end
Ruby’s standaard stack-dieptelimiet (meestal rond de 10.000 frames) vangt dit uiteindelijk op met een SystemStackError, maar de backtrace is onleesbaar.
Debugtip: Wanneer je een SystemStackError of een NoMethodError krijgt die nergens op slaat, voeg dan een tijdelijke debugregel toe bovenaan method_missing:
def method_missing(name, *args)
$stderr.puts "method_missing aangeroepen: #{name} op #{self.class}"
# ... rest van implementatie
end
Dit onthult onverwachte aanroepen direct.
Productie-checklist
Voordat je code shipt die method_missing gebruikt:
respond_to_missing?is geïmplementeerd met bijpassende logicasuperwordt aangeroepen voor onafgehandelde methodenamen- Performance is acceptabel voor de aanroepfrequentie
- Overweeg
define_method-caching voor herhaalde aanroepen - Tests dekken het
respond_to?-contract, niet alleen de methode-aanroepen - Je hebt het echt nodig — als de set methodes bekend is, gebruik dan
define_method
FAQ
Werkt method_missing met keyword arguments in Ruby 3.3?
Ja, maar je moet ze expliciet afhandelen. Sinds Ruby 3.0 positionele en keyword arguments scheidde, moet je method_missing-signatuur **kwargs bevatten:
def method_missing(name, *args, **kwargs, &block)
Als je **kwargs weglaat, gooit elke keyword argument die aan een ontbrekende methode wordt meegegeven een ArgumentError voordat method_missing überhaupt draait.
Kan method_missing private methode-aanroepen onderscheppen?
Nee. method_missing vuurt standaard alleen voor publieke methode-aanroepen. Als je een private methode van buiten het object aanroept, gooit Ruby een NoMethodError met een “private method called”-bericht — method_missing wordt niet aangeroepen. Binnen het object worden private methodes normaal opgelost en bereiken ze nooit method_missing.
Hoe gebruiken gems zoals ActiveRecord method_missing veilig?
ActiveRecord combineert method_missing met het define_method-caching patroon dat hierboven beschreven is. De eerste attribute-toegang gaat via method_missing, die vervolgens een echte methode definieert op de klasse. Volgende toegangen raken de gedefinieerde methode direct. Dit geeft dynamisch gedrag bij load time met volledige performance daarna. Je kunt dit zien in ActiveModel::AttributeMethods#method_missing in de Rails-broncode.
Is method_missing trager met YJIT ingeschakeld?
YJIT verbetert reguliere method dispatch aanzienlijk (20-30% in typische Rails-apps), maar het kan method_missing-aanroepen niet optimaliseren omdat het doel onbekend is bij compilatie. Het relatieve performance-verschil tussen reguliere methodes en method_missing wordt zelfs groter met YJIT, omdat reguliere methodes sneller worden terwijl method_missing ongeveer gelijk blijft. In onze benchmarks ging het verschil van ~3x (zonder YJIT) naar ~5x (met YJIT).
Moet ik BasicObject gebruiken in plaats van Object voor proxy-klassen?
BasicObject stript bijna alle methodes (geen to_s, geen ==, geen inspect), waardoor meer aanroepen doorvallen naar method_missing. Dit is handig voor pure proxy-objecten waar je maximale transparantie wilt. Maar het betekent ook dat basisbewerkingen zoals string-interpolatie en gelijkheidschecks breken tenzij je ze afhandelt. Gebruik BasicObject alleen wanneer je een echt transparante proxy nodig hebt en je bereid bent de randgevallen af te handelen.
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 TouchRelated Articles
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