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 Delegation: Forwardable vs SimpleDelegator vs Rails delegate

TTB Software
ruby, rails
Een praktische vergelijking van Ruby's drie delegatie-aanpakken. Wanneer gebruik je Forwardable, SimpleDelegator of Rails delegate — met benchmarks, valkuilen en productiepatronen.

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) geeft true terug
  • De methoden verschijnen in instance_methods
  • Geen method_missing magie — 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 prefix of allow_nil gedrag
  • 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.

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