When would you build a Ruby presenter as class UserPresenter < DelegateClass(User) instead of subclassing SimpleDelegator or User itself?
answer
- a class generated for one class
- forwarding methods defined up front
- super(user) in initialize
- Tempfile < DelegateClass(File)
- wrap an existing object, do not rebuild it
basics
~20 sDelegateClass(User) builds a Delegator subclass with real forwarding methods for User's public and protected methods, so calls skip method_missing and reflection lists them. Use it when the wrapped type is fixed; subclass User only when you create the objects yourself.
solid answer
~40 s`DelegateClass(User)` returns a new class that inherits from `Delegator` and, at that moment, defines one method per public instance method of `User` (protected ones too, via `__send__`), each calling the same method on `__getobj__`. Compared with `SimpleDelegator`, those calls do not go through `method_missing`, and `instance_methods` and `instance_method` report the forwarded names; methods added to `User` later still work through `method_missing`. Your `initialize` must call `super(user)`. The standard library's `Tempfile < DelegateClass(File)` is the model. Choose it when the presenter always wraps a `User` and you want explicit, reflectable forwarding; choose `SimpleDelegator` when the wrapped type varies. Inheriting from `User` fits only when you construct the object: a presenter usually receives users loaded elsewhere and adds behaviour per context without changing the `User` class.
code
ruby · 21 linesrequire "delegate"
class User
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
end
class UserPresenter < DelegateClass(User)
def initialize(user, viewer:)
super(user)
@viewer = viewer
end
def full_name = "#{first_name} #{last_name}"
end
presenter = UserPresenter.new(User.new("Ada", "Lovelace"), viewer: :admin)
presenter.full_name # => "Ada Lovelace"
UserPresenter.instance_methods.include?(:first_name) # => truego deeper
Know that DelegateClass(User) builds a wrapper class for users and that its initialize must call super(user).
Explain which methods DelegateClass generates, how it differs from SimpleDelegator, and the Tempfile < DelegateClass(File) example.
Justify the choice between inheritance, SimpleDelegator, DelegateClass and Forwardable for a presenter from how objects are created and how hot the path is.
Standardise one presenter style across the codebase and document when an exception is justified, so reviewers are not re-arguing the choice.
## Three ways to put presentation on a user A **presenter** adds display methods (`full_name`, `joined_on`, `avatar_url`) to a user model. In Ruby you can get there by **inheritance** (`class UserPresenter < User`), by a **generic wrapper** (`class UserPresenter < SimpleDelegator`), or by a **class-specific wrapper** (`class UserPresenter < DelegateClass(User)`). The last one comes from the `delegate` library and is the least known. ## What DelegateClass generates `DelegateClass(superclass)` is a top-level method in `delegate.rb`. When called it: 1. creates an anonymous class that inherits from **`Delegator`**; 2. reads `User.public_instance_methods` and `User.protected_instance_methods`, skipping the names `Delegator` handles itself; 3. defines a real method for each, whose body is `__getobj__.name(...)` (protected ones use `__send__` and stay protected); 4. defines `__getobj__` and `__setobj__`, storing the target in its own instance variable; 5. overrides the class's `instance_methods`, `public_instance_methods` and `instance_method` so reflection includes `User`'s methods. You then subclass that class and call `super(user)` in `initialize` (`Delegator#initialize` stores the target). A block passed to `DelegateClass` is evaluated in the new class, allowing `UserPresenter = DelegateClass(User) do ... end`. Because the forwarding methods are ordinary methods, calls skip `method_missing`, and tools that inspect `instance_methods` see a complete interface. The list is a **snapshot**: a method added to `User` after `DelegateClass(User)` ran is not predefined, but it is still reachable, because the class remains a `Delegator` and falls back to `method_missing`. ## Comparing the options | | `< User` | `< SimpleDelegator` | `< DelegateClass(User)` | `Forwardable` | |---|---|---|---|---| | Works on a user loaded elsewhere | no, you must build a `UserPresenter` | yes | yes | yes | | Forwarding mechanism | inheritance | `method_missing` | generated methods | generated methods | | Wrapped type | fixed | any | fixed, `User` | any, per accessor | | Interface | all of `User` | all public methods | all public and protected | only listed names | | `is_a?(User)` | true | false | false | false | ## Picking one - **Subclass `User`** only when you create the objects and the presenter truly is a kind of user. Records built by persistence code, a cache or an API client already exist as `User` instances; turning them into a subclass means re-instantiating them, and every context would share one subclass. - **`SimpleDelegator`** when the wrapper should accept whatever it is given, or swap targets with `__setobj__`. - **`DelegateClass(User)`** when the presenter always wraps a `User`, the call rate is high enough that `method_missing` shows up in profiles, or reflection matters, for example documentation or tooling that lists `instance_methods`. - **`Forwardable`** when the presenter should expose only a few fields. The standard library uses the third option itself: `Tempfile` is declared as `class Tempfile < DelegateClass(File)`, a file with extra rules about where it lives and when it is deleted. ## Reflection details `DelegateClass` also patches the generated class's reflection methods so tools see the wrapped interface. `UserPresenter.instance_methods` includes `User`'s instance methods, and `UserPresenter.public_instance_method(:first_name)` or `instance_method(:first_name)` succeed; for a name that exists only on `User`, those lookups fall back to `User`'s own method object. A `SimpleDelegator` subclass has none of this: `UserPresenter.instance_methods` lists only what the presenter and `Delegator` define, because forwarding is decided per call. For documentation generators, interface checks in tests or code that builds method lists at boot, that difference is often the deciding factor rather than speed. ## Pitfalls - Forgetting `super(user)`: the generated methods call `__getobj__` with no fallback and raise `ArgumentError` "not delegated". - Assuming the wrapper is a `User`: `is_a?`, `User ===` and pattern matching still see a `Delegator` subclass. - Calling `DelegateClass(User)` before `User`'s methods are defined, for instance with a class reopened later, leaves those methods to the slower `method_missing` path.
- Does a method added to User after DelegateClass(User) was evaluated still work on the presenter?Yes. It was not in the generated list, but the class is a `Delegator` subclass, so the call falls back to `method_missing` and is forwarded. It just does not appear among the predefined methods and takes the slower path.
- Why can a presenter built on a wrapper serve records loaded elsewhere, where class UserPresenter < User cannot?A subclass only helps when you construct a `UserPresenter` yourself. Persistence code, caches and API clients hand you `User` instances; a wrapper takes those objects as they are, and different screens can wrap the same user in different presenters without changing `User`.
saying these in an interview costs you the question
- DelegateClass(User) makes the presenter an instance of User
- DelegateClass forwards through method_missing just like SimpleDelegator
- Methods added to User later are unreachable through a DelegateClass wrapper
- A DelegateClass subclass can skip super(user) in initialize
- Subclassing User works for users loaded by other code