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 method_missing: Wanneer Gebruiken en Wanneer Hard Wegrennen

Ruby method_missing: Wanneer Gebruiken en Wanneer Hard Wegrennen

TTB Software
ruby

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:

  1. respond_to_missing? is geïmplementeerd met bijpassende logica
  2. super wordt aangeroepen voor onafgehandelde methodenamen
  3. Performance is acceptabel voor de aanroepfrequentie
  4. Overweeg define_method-caching voor herhaalde aanroepen
  5. Tests dekken het respond_to?-contract, niet alleen de methode-aanroepen
  6. 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.

#ruby #metaprogramming #method_missing #respond_to_missing #design-patterns
T

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