Decorator
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Also known as: Wrapper
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Component | #speed, #description | The interface both the enemy and its wrappers share. |
| Concrete Component | Goblin | The core object being decorated. |
| Decorator | SimpleDelegator | Holds the wrapped object and forwards every message to it. |
| Concrete Decorator | Slowed, Enraged | Overrides a message, calls super, and changes the result. |
Ruby implementation
require "delegate"
# The Component: an enemy, before any status effects.
class Goblin
def speed = 2.4
def description = "Goblin"
end
# Decorators wrap any enemy-like object and change one thing about it.
# SimpleDelegator forwards everything they don't override.
class Slowed < SimpleDelegator
def speed = super * 0.5
def description = "#{super}, slowed"
end
class Enraged < SimpleDelegator
def speed = super * 2
def description = "#{super}, enraged"
endIn use
goblin = Goblin.new
goblin.speed # => 2.4
Slowed.new(goblin).speed # => 1.2
enemy = Enraged.new(Slowed.new(goblin))
enemy.speed # => 2.4
enemy.description # => "Goblin, slowed, enraged"
Each status effect is one small wrapper. Without them, Goblin would need a flag and a timer for every effect, plus code for each combination.
Notes
Ruby idioms
SimpleDelegator(fromrequire "delegate") is the decorator base class in the standard library. It forwards every method you don’t define, andsuperreaches the wrapped object.Module#prependdecorates a whole class rather than one object. Use it when every instance needs the behaviour.Forwardablewithdef_delegatorsis the explicit alternative when you only want to forward a few named methods.- Rack middleware is a stack of decorators around your app.
Reach for it when
- Extras can be combined freely. With subclasses you’d need one for every combination.
- Behaviour has to be added to one object at runtime, such as a goblin hit by a frost tower, not to every instance of a class.
Watch for
- Identity. A decorated object is not
equal?to the original, andis_a?(Goblin)is false. Code that checks classes will be surprised. - Removing an effect means unwrapping, which decorators don’t do on their own. Keep a reference to the plain object if effects can wear off.
Each wrapper adds one thing and leaves the original object alone.