Decorator

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Also known as: Wrapper

Participants

RoleIn this exampleResponsibility
Component#speed, #descriptionThe interface both the enemy and its wrappers share.
Concrete ComponentGoblinThe core object being decorated.
DecoratorSimpleDelegatorHolds the wrapped object and forwards every message to it.
Concrete DecoratorSlowed, EnragedOverrides a message, calls super, and changes the result.

Ruby implementation

decorator.rb
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"
end

In 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 (from require "delegate") is the decorator base class in the standard library. It forwards every method you don’t define, and super reaches the wrapped object.
  • Module#prepend decorates a whole class rather than one object. Use it when every instance needs the behaviour.
  • Forwardable with def_delegators is 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, and is_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.