In Ruby, how does a UserPresenter < SimpleDelegator wrap a user object, and what do __getobj__ and __setobj__ do?
answer
- require "delegate"
- every public method passes through
- method_missing plus respond_to_missing?
- super reaches the wrapped object
- __setobj__ swaps the target
basics
~10 sSimpleDelegator.new(user) forwards every public method it does not define itself to user. A subclass adds or overrides presentation methods; getobj returns the wrapped user and setobj replaces it with another object.
solid answer
~40 sAfter `require "delegate"`, `class UserPresenter < SimpleDelegator` takes the wrapped object in `new`; `Delegator#initialize` stores it with `__setobj__`. Any public method the presenter does not define is caught by `Delegator#method_missing` and sent to the wrapped object, and `respond_to_missing?` makes `respond_to?` agree. Methods you define win, and inside them `super` reaches the wrapped object's method of the same name, so an `email` override can return `super.downcase`. `__getobj__` returns the wrapped object, which is how you hand the real user to code that needs it, and `__setobj__(other)` swaps the target in place; passing the presenter itself raises `ArgumentError`. Private methods of the wrapped object are not forwarded, and a subclass that overrides `initialize` must call `super(user)`, or forwarded calls raise `NoMethodError`.
code
ruby · 19 linesrequire "delegate"
User = Data.define(:first_name, :last_name, :email)
class UserPresenter < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
def email
super.downcase
end
end
user = User.new(first_name: "Ada", last_name: "Lovelace", email: "[email protected]")
presenter = UserPresenter.new(user)
presenter.full_name # => "Ada Lovelace"
presenter.email # => "[email protected]"
presenter.__getobj__.equal?(user) # => truego deeper
Recall that SimpleDelegator.new(user) passes every public method through to user and that a subclass can add presentation methods.
Explain method_missing and respond_to_missing? forwarding, super in overrides, getobj and setobj, and why initialize must call super.
Decide where a presenter must be unwrapped with getobj, and weigh method_missing cost against DelegateClass or Forwardable in hot paths.
Agree team rules for presenters: where they are created, which layers may see them, and when an explicit Forwardable interface is required instead.
## What SimpleDelegator is The `delegate` library (`require "delegate"`, a default gem, 0.6.1 in Ruby 4.0) provides three tools: the abstract class **`Delegator`**, the ready-made **`SimpleDelegator`**, and the **`DelegateClass`** method. `SimpleDelegator` is the one most code uses: an object that stands in front of another object and passes on every call it does not handle itself. It is the usual base for a **presenter**, an object that adds display logic to a model without changing the model's class. ## Building a presenter 1. `require "delegate"`. 2. Declare `class UserPresenter < SimpleDelegator`. 3. Add display methods such as `full_name` that call the user's own methods (`first_name`, `last_name`) directly, as if they were the presenter's. 4. Wrap each user where you need it: `UserPresenter.new(user)`. If you override `initialize` to take extra arguments, call `super(user)` inside it. Without that call the target is never stored, and every forwarded call ends in `NoMethodError`. ## How forwarding works - **`method_missing`**: `Delegator` inherits from `BasicObject`, so it has very few methods of its own. A call it does not define lands in `Delegator#method_missing`, which fetches the target with `__getobj__` and calls the method there with the same arguments and block. - **`respond_to_missing?`**: answers `respond_to?` for forwarded names, so duck-typing checks see the user's public methods. - **Public only**: the target must respond to the method publicly. A private method of the user is not forwarded; the call raises `NoMethodError`. - **Overrides win**: a method defined in `UserPresenter` is found first, so the presenter can change what `email` returns. - **`super` still reaches the user**: `Delegator` has no `email` of its own, so `super` in an override falls through to `method_missing` and calls the user's `email`. ## __getobj__ and __setobj__ | Method | Returns or does | Error cases | |---|---|---| | `__getobj__` | the wrapped object | `ArgumentError` "not delegated" if nothing was ever set (unless given a block) | | `__setobj__(obj)` | makes `obj` the new target | `ArgumentError` "cannot delegate to self" when `obj` is the delegator | The double underscores keep these names out of the way of the wrapped object's own methods. `__getobj__` is the escape hatch for code that needs the real model, for example a serializer or a persistence call. `__setobj__` lets one wrapper be reused for several objects, but the presenter's own methods do not change, so swap only to objects of the same kind. ## Details that matter in practice - `methods` and `public_methods` on the wrapper report the union of the wrapper's and the target's methods. - `dup` and `clone` copy the wrapped object as well, so the copy wraps a copy. - Rendering code usually calls a few presenter methods and many plain model attributes; the forwarded ones need no declaration, which is SimpleDelegator's convenience compared with listing each name. - Calls go through `method_missing`, which costs more than a direct method call. For a presenter used in a view this rarely matters; in a hot loop, a `DelegateClass` or Forwardable avoids it. ## Reflection and debugging Several reflective calls see through the wrapper, which helps when exploring a presenter in a console: - `presenter.respond_to?(:email)` is true for forwarded methods; - `presenter.method(:email)` returns a `Method` object even though the call is served by `method_missing`, because `respond_to_missing?` vouches for it; - `presenter.public_send(:email)` forwards like a direct call; - `presenter.methods` lists the user's methods along with the wrapper's own. Other questions, such as the object's class, are answered by the wrapper itself; which ones do and which ones forward is a subject of its own. ## Common mistakes - Overriding `initialize` without `super(user)`. - Expecting private model helpers to be reachable through the wrapper. - Forgetting `require "delegate"` in a plain Ruby script. - Passing the wrapper where code checks the model's class; the wrapper is a different object of a different class.
- What happens if UserPresenter#initialize takes (user, viewer) and forgets to call super(user)?No target is stored. Each forwarded call reaches `Delegator#method_missing`, which asks `__getobj__` with a fallback block, finds nothing and falls through to `NoMethodError`. Calling `presenter.__getobj__` directly raises `ArgumentError` "not delegated". The fix is `super(user)` before setting `@viewer`.
- When would you call __setobj__ on an existing SimpleDelegator?When one wrapper should be reused for a series of objects of the same kind, such as a presenter reused across rows while rendering a list. The presenter's own methods stay the same, so swapping to an unrelated class tends to break them.
saying these in an interview costs you the question
- SimpleDelegator forwards private methods of the wrapped object too
- super inside a SimpleDelegator override raises NoMethodError
- __getobj__ returns a copy of the wrapped object
- A subclass can skip super in initialize and still forward
- SimpleDelegator only forwards methods you list explicitly