skip to content

In Ruby, how do Forwardable's def_delegators and def_delegator expose selected methods of an inner object, and what do they generate?

level: middleimportance: must knowfreq 46%

answer

  1. extend, not include
  2. accessor first, then method names
  3. :@user, a method, or a constant path
  4. third argument renames
  5. real methods, nothing else forwarded

basics

~10 s

After extend Forwardable, def_delegators :@user, :name, :email defines real instance methods that call the same methods on @user, and def_delegator :@user, :name, :display_name forwards one method under a new name. Unlisted methods stay unavailable.

solid answer

~40 s

`Forwardable` is a module you `extend` in a class body, so its methods run at class-definition time. `def_delegators accessor, *methods` defines one ordinary instance method per name; each evaluates the accessor and calls the same method on the result, passing arguments, keywords and block through with `...`. `def_delegator accessor, method, new_name` does the same for one method and can rename it. The accessor can be an instance variable (`:@user`), a method (`:user`) or a constant path String. Because real methods are generated, `respond_to?`, `instance_methods` and `method(:display_name)` all see them, and anything not listed raises `NoMethodError`: the wrapper's interface is exactly what you wrote. `delegate [:name, :email] => :@user` is the Hash form. It suits a presenter that should expose a few user fields, not the whole object.

code

ruby · 18 lines
ruby
require "forwardable"

User = Data.define(:name, :email, :created_at)

class UserPresenter
  extend Forwardable
  def_delegators :@user, :email, :created_at
  def_delegator :@user, :name, :display_name

  def initialize(user)
    @user = user
  end
end

presenter = UserPresenter.new(User.new(name: "Ada", email: "[email protected]", created_at: Time.now))
presenter.display_name        # => "Ada"
presenter.respond_to?(:email) # => true
presenter.respond_to?(:name)  # => false

go deeper

for a junior

Recall that extend Forwardable plus def_delegators :@user, :name forwards named methods to an inner object, and that the accessor comes first.

for a middle

Explain that real methods are generated, the three accessor forms, renaming with def_delegator, and why unlisted methods raise NoMethodError.

for a senior

Use Forwardable to keep a presenter's interface deliberately narrow, and spot the nil-accessor and private-target warnings in logs.

for a principal

Set a convention for when wrappers list their interface explicitly versus forwarding everything, weighing safety of a narrow surface against boilerplate.

## What Forwardable is **Forwarding** means an object answers a call by making the same call on an object it holds. Ruby's standard library ships **`Forwardable`** (`require "forwardable"`, a default gem, 1.4.0 in Ruby 4.0) to generate those one-line methods for you. You add it with **`extend Forwardable`** in a class body. `extend` puts its methods on the class object itself, which is what you need: `def_delegators` is called while the class is being defined, just like `attr_reader`. ## The calls you use | Call | Effect | |---|---| | `def_delegator :@user, :name` | defines `name`, calling `@user.name` | | `def_delegator :@user, :name, :display_name` | defines `display_name`, calling `@user.name` | | `def_delegators :@user, :email, :created_at` | one forwarding method per name, no renaming | | `delegate [:email, :created_at] => :@user` | Hash form of the same; `instance_delegate` is its long name | `def_delegator` and `def_delegators` are aliases of `def_instance_delegator` and `def_instance_delegators`. Note the argument order: **accessor first**, then the method on the target, then the optional new name. ## What the accessor can be - an **instance variable** Symbol such as `:@user`; - a **method** name such as `:user`, for example an `attr_reader` or a private helper; - a **constant** path String such as `"Settings::DEFAULTS"`, written in full. The accessor is evaluated on every call, so reassigning `@user` later changes where calls go. ## What gets generated Forwardable builds Ruby source for each method and evaluates it into the class. For `def_delegator :@user, :name, :display_name` the result behaves like: 1. define `display_name(...)`, so positional arguments, keywords and a block all pass through; 2. evaluate `@user`; 3. if the target has a **public** `name`, call it and return its result; 4. otherwise print a warning that it is "forwarding to private method" and call it with `__send__` anyway. Because these are **real methods**, they are as fast as hand-written ones, show up in `instance_methods(false)`, answer `respond_to?` and can be overridden or wrapped with `super` like any method. Nothing is forwarded implicitly: a method you did not list raises `NoMethodError` on the wrapper. That explicit, narrow interface is Forwardable's main selling point for a **presenter** that wraps a user and should expose `name` and `email` but not `password_digest` or `destroy`. ## Traps - **A nil accessor.** If `@user` is nil, the generated method finds that nil has no public `name`, prints the misleading private-method warning naming `NilClass`, then raises `NoMethodError` for nil. Assign the target in `initialize`. - **Private targets.** Forwarding to a private method works but warns every time; forward only public methods. - **`include` instead of `extend`.** `include Forwardable` adds `def_delegators` as an *instance* method, so calling it in the class body raises `NoMethodError`. - `__send__` and `__id__` are skipped by `def_delegators`, and RDoc does not document generated delegators. ## Return values and visibility `def_delegator` returns the name of the method it defined, as a Symbol, so it composes with visibility modifiers: `private def_delegator(:@user, :password_digest, :digest)` defines a private forwarding method in one line. That keeps sensitive fields available to the presenter's own methods without exposing them to templates. Forwardable also works on a **single object**. When an object that is not a class or module does `extend Forwardable`, `def_delegator` defines the method on that object's singleton class, so only that one object gains it: - `notices = []` then `notices.extend Forwardable`; - `notices.def_delegator "STDOUT", "puts"`; - `notices.puts "saved"` now writes to standard output, while other Arrays are unchanged. ## Forwardable or a delegator class | | `Forwardable` | `SimpleDelegator` | |---|---|---| | What is forwarded | only the names you list | every public method | | Mechanism | generated methods | `method_missing` at call time | | Renaming | yes, third argument | no | | Swapping the target | reassign the accessor | `__setobj__` | For class-level forwarding, `SingleForwardable` provides `def_single_delegator` and `def_single_delegators`, which define singleton methods on the module or object that extends it.

  • What happens when a Forwardable delegator runs while the accessor instance variable is still nil?
    The generated method evaluates `@user`, sees that nil has no public method of that name and takes its private-method branch: it prints a warning about forwarding to a private method of `NilClass`, then `__send__` raises `NoMethodError` for nil. The warning is misleading, so set the target in `initialize`.
  • How does SingleForwardable differ from Forwardable?
    `Forwardable` defines instance methods on the class that extends it. `SingleForwardable` defines singleton methods on the module or object that extends it, through `def_single_delegator` and `def_single_delegators`, so a module such as a facade can forward its own module-level calls to another class.

saying these in an interview costs you the question

  • Forwardable forwards every method the inner object has
  • def_delegator :@user, :name, :display_name defines name, calling display_name
  • You enable def_delegators with include Forwardable
  • Forwardable delegators work through method_missing at call time
  • The accessor must be an instance variable symbol