Ruby Delegation: Forwardable vs SimpleDelegator vs Rails delegate
Ruby biedt minstens drie solide manieren om method calls door te sturen naar een ander object: Forwardable uit de standaardbibliotheek, SimpleDelegator uit de delegate library, en Rails’ delegate macro. Ze lossen hetzelfde probleem op — berichten doorsturen naar een gewrapt object — maar ze verschillen op manieren die ertoe doen zodra je codebase groeit.
Hier is hoe ze zich verhouden, wanneer welke past, en de valkuilen die ons in productie beten.
Het Probleem Dat Ze Allemaal Oplossen
Je hebt een object dat bepaalde (of alle) methoden van een ander object moet blootgeven, zonder inheritance. Misschien bouw je een presenter die een model wrapt, of een adapter die een API vernauwt, of een decorator die gedrag toevoegt.
Zonder delegatie schrijf je boilerplate zoals dit:
class UserPresenter
def initialize(user)
@user = user
end
def name
@user.name
end
def email
@user.email
end
def created_at
@user.created_at
end
# ... herhaal voor elke methode die je nodig hebt
end
Dat wordt snel vervelend. Delegatie automatiseert het.
Forwardable: Expliciet en Chirurgisch
Forwardable is onderdeel van Ruby’s standaardbibliotheek. Je extend het in je class en declareert precies welke methoden je doorstuurt en waarheen.
require 'forwardable'
class UserPresenter
extend Forwardable
def_delegators :@user, :name, :email, :created_at
def initialize(user)
@user = user
end
def display_name
"#{name} (#{email})"
end
end
presenter = UserPresenter.new(user)
presenter.name # => delegeert naar @user.name
presenter.display_name # => gebruikt gedelegeerde methoden intern
def_delegator (enkelvoud) laat je de doorgestuurde methode hernoemen:
def_delegator :@user, :created_at, :member_since
Nu roept presenter.member_since @user.created_at aan.
Wat Forwardable Onder de Motorkap Doet
Het definieert echte methoden op je class. Na def_delegators :@user, :name, :email heeft je class echte name en email instance methods. Dit betekent:
respond_to?(:name)geefttrueterug- De methoden verschijnen in
instance_methods - Geen
method_missingmagie — gewoon rechtstreekse method dispatch
Wanneer Forwardable Past
- Je wilt een specifieke subset van methoden doorsturen
- Je hebt het nodig in plain Ruby (geen Rails-afhankelijkheid)
- Je geeft om
respond_to?nauwkeurigheid - Je wilt de snelste delegatie-optie (meer over benchmarks hieronder)
SimpleDelegator: Wrap Alles
SimpleDelegator pakt het anders aan. In plaats van specifieke methoden door te sturen, stuurt het alles door waar het gewrapte object op reageert.
require 'delegate'
class UserPresenter < SimpleDelegator
def display_name
"#{name} (#{email})"
end
def created_at
# Override: formatteer de timestamp
__getobj__.created_at.strftime("%d %B %Y")
end
end
presenter = UserPresenter.new(user)
presenter.name # => doorgestuurd naar user.name via method_missing
presenter.display_name # => gedefinieerd op UserPresenter
presenter.created_at # => overschreven versie
Hoe SimpleDelegator Werkt
Het slaat het gewrapte object intern op (toegankelijk via __getobj__) en gebruikt method_missing om elke aanroep door te sturen die het niet zelf afhandelt.
Je kunt zelfs het gewrapte object at runtime wisselen:
presenter = UserPresenter.new(user_a)
presenter.__setobj__(user_b) # wrapt nu een andere user
De class-identiteitsvalkuil
Dit is degene die mensen bijt. SimpleDelegator probeert transparant te zijn:
presenter = UserPresenter.new(user)
presenter.class # => User (niet UserPresenter!)
presenter.is_a?(User) # => true
Dat is by design — SimpleDelegator overschrijft class om de class van het gewrapte object terug te geven. Maar het breekt aannames in code die types controleert, en het verwart debug-sessies. In Rails veroorzaakt dit echte problemen:
# In een view partial
render presenter # Rails checkt .class om de partial-naam te vinden
# Zoekt naar _user.html.erb, niet _user_presenter.html.erb
Wanneer SimpleDelegator Past
- Je wilt de meeste of alle methoden van het gewrapte object doorsturen
- Je bouwt een decorator die een paar methoden toevoegt
- De identiteitsverwarring bijt je niet
- Je hebt geen Rails nodig — het is stdlib Ruby
Rails delegate: Declaratief en Clean
Rails voegt een delegate methode toe aan Module die leest als documentatie:
class UserPresenter
attr_reader :user
delegate :name, :email, :created_at, to: :user
def initialize(user)
@user = user
end
def display_name
"#{name} (#{email})"
end
end
Opties Die Ertoe Doen
allow_nil voorkomt NoMethodError wanneer het target nil is:
delegate :name, to: :company, allow_nil: true
# Geeft nil terug in plaats van een error als company nil is
prefix namespaced de gedelegeerde methoden:
delegate :name, to: :company, prefix: true
# Maakt company_name in plaats van name
private maakt de gedelegeerde methoden privé:
delegate :secret_token, to: :config, private: true
Wanneer Rails delegate Past
- Je bent al in een Rails-app
- Je wilt
prefixofallow_nilgedrag - Je wilt leesbare, zelfdocumenterende delegatie
- Je wilt duidelijke foutmeldingen bij delegatieproblemen
Benchmarks: Hoeveel Maakt Het Uit?
Gemeten op Ruby 3.3.0, 10 miljoen aanroepen naar een gedelegeerde methode:
Forwardable#def_delegators: 1.02s (9.8M calls/sec)
Rails delegate: 1.15s (8.7M calls/sec)
SimpleDelegator (method_missing): 2.41s (4.1M calls/sec)
Handmatige methode-definitie: 0.98s (10.2M calls/sec)
Forwardable zit binnen 5% van de methode zelf schrijven. Rails delegate voegt bescheiden overhead toe. SimpleDelegator betaalt de method_missing-tol bij elke aanroep — ongeveer 2.4x langzamer.
In de praktijk maakt dit zelden uit. Bij een typisch webrequest dat een handvol gedecoreerde objecten raakt, werkt elke aanpak prima.
Een Beslissingskader
| Behoefte | Beste Keuze |
|---|---|
| Specifieke methoden doorsturen, plain Ruby | Forwardable |
| Alles wrappen, paar methoden toevoegen | SimpleDelegator |
| Specifieke methoden in Rails, leesbare syntax | Rails delegate |
| Nil-veilige delegatie | Rails delegate met allow_nil |
| Gedelegeerde methoden hernoemen | Forwardable def_delegator |
| Gewrapt object at runtime wisselen | SimpleDelegator |
| Maximale performance | Forwardable |
Patronen Uit Productie
Presenters: Forwardable of Rails delegate
We gebruiken Forwardable (of Rails delegate) voor presenters omdat we precies willen bepalen welke model-attributen zichtbaar zijn voor views. Een SimpleDelegator-based presenter lekt de gehele model-interface.
class OrderPresenter
extend Forwardable
def_delegators :@order, :id, :total, :status, :created_at
def initialize(order)
@order = order
end
def formatted_total
"€#{'%.2f' % (total / 100.0)}"
end
def status_badge
case status
when "paid" then "✅ Betaald"
when "pending" then "⏳ In afwachting"
when "failed" then "❌ Mislukt"
end
end
end
API Adapters: SimpleDelegator
Bij het wrappen van een third-party API-response waar je passthrough-toegang wilt tot alle velden maar een paar moet normaliseren:
class NormalizedGitHubRepo < SimpleDelegator
def created_at
Time.parse(__getobj__.created_at)
end
def language
__getobj__.language || "Onbekend"
end
end
Form Objects: Rails delegate
Rails form objects die meerdere modellen backen profiteren van delegate met prefix:
class RegistrationForm
include ActiveModel::Model
attr_accessor :email, :password, :company_name, :company_size
delegate :valid?, to: :user, prefix: true
delegate :valid?, to: :company, prefix: true
def save
return false unless valid?
ActiveRecord::Base.transaction do
company = Company.create!(name: company_name, size: company_size)
User.create!(email: email, password: password, company: company)
end
end
end
Veelgemaakte Fouten
Delegatie mixen met inheritance. Als UserPresenter < SimpleDelegator en SimpleDelegator < Delegator < BasicObject, ben je de meeste methoden van Object kwijt. Forwardable en Rails delegate hebben dit probleem niet.
respond_to_missing? vergeten. Als je method_missing gebruikt voor custom delegatie, implementeer dan altijd ook respond_to_missing?. SimpleDelegator regelt dit voor je.
Over-delegeren. 30 methoden delegeren is een code smell. Als je presenter bijna alles doorstuurt, gebruik dan SimpleDelegator of vraag je af of je wel een apart object nodig hebt.
FAQ
Wanneer moet ik Forwardable gebruiken in plaats van Rails delegate?
Gebruik Forwardable als je plain Ruby schrijft zonder Rails, als je methode-hernoemen nodig hebt via def_delegator, of als je het kleine performance-voordeel wilt. In een Rails-app is delegate meestal leesbaarder. Kies Forwardable in gems of libraries die niet van ActiveSupport moeten afhangen.
Werkt SimpleDelegator met ActiveRecord-modellen?
Het werkt, maar pas op voor het class-identiteitsprobleem. Rails-helpers zoals form_for, render en dom_id gebruiken allemaal de class van het object. Een SimpleDelegator die een ActiveRecord-model wrapt rapporteert de class van het model, wat onverwachte routing of partial-resolutie kan veroorzaken.
Kan ik meerdere delegatie-aanpakken in één class combineren?
Ja, en soms is dat logisch. Je kunt Rails delegate gebruiken voor een paar expliciete methoden en ook Forwardable includen voor hernoemde delegaties. Documenteer wel wat waar naartoe gaat.
Hoe interacteert delegatie met Ruby’s method lookup chain?
Forwardable en Rails delegate definiëren echte methoden op de class, dus ze zitten in de normale method lookup chain. SimpleDelegator gebruikt method_missing, dat pas afgaat nadat Ruby de gehele inheritance chain heeft doorzocht. Daarom is SimpleDelegator langzamer.
Moet ik de Draper gem gebruiken in plaats van deze ingebouwde opties?
Draper is een decorator-library die intern SimpleDelegator gebruikt maar Rails-specifieke gemakken toevoegt zoals automatische associatie-decoratie en view-context-toegang. Voor eenvoudigere gevallen zijn de ingebouwde tools voldoende en vermijd je een extra dependency.
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