The Decorator Pattern in Ruby
The decorator pattern lets you add behaviour to objects without changing their class. Instead of subclassing, you wrap the original object inside a new object that intercepts calls and adds its own logic before or after delegating to the wrapped object.
This is composition over inheritance — one of the SOLID principles. Instead of building a tall inheritance tree to add features, you stack decorators at runtime.
Simple delegation with Forwardable
The most lightweight way to forward method calls is Ruby’s Forwardable module. You declare which methods should pass through to the wrapped object.
require "forwardable"
class PlainFormatter
def format(data)
data.to_s
end
end
class UppercaseFormatter
extend Forwardable
def initialize(wrapped)
@wrapped = wrapped
end
def_delegator :@wrapped, :format
def format(data)
@wrapped.format(data).upcase
end
end
formatter = UppercaseFormatter.new(PlainFormatter.new)
formatter.format("hello")
# => "HELLO"
def_delegator creates a forwarding method. The first argument is the instance variable holding the wrapped object, and the second is the method name to call on it. You can override format to add behaviour while still delegating to the wrapped object for the base behaviour.
Forwardable works well when you only need to forward a handful of methods and you want to be explicit about it.
The Decorator Pattern (Composition Over Inheritance)
The classic decorator pattern in Ruby looks like this. Every decorator implements the same interface as the object it wraps, so decorators can be stacked in any order.
class Coffee
def cost
3
end
def description
"coffee"
end
end
class MilkDecorator
def initialize(component)
@component = component
end
def cost
@component.cost + 1
end
def description
"#{@component.description} + milk"
end
end
class SugarDecorator
def initialize(component)
@component = component
end
def cost
@component.cost + 0.5
end
def description
"#{@component.description} + sugar"
end
end
coffee = Coffee.new
coffee = MilkDecorator.new(coffee)
coffee = SugarDecorator.new(coffee)
coffee.cost # => 4.5
coffee.description # => "coffee + milk + sugar"
Each decorator:
- Holds a reference to the wrapped component
- Implements the same interface (
cost,description) - Delegates to the wrapped component
- Adds its own behaviour
This is the core idea. Everything else is a variation on how you avoid writing the delegation boilerplate.
Using SimpleDelegator
SimpleDelegator from the standard library eliminates the need to write delegation methods by hand. You subclass it and call super with the object to wrap.
require "delegate"
class TaxDecorator < SimpleDelegator
def cost
super + 2
end
end
class ShippingDecorator < SimpleDelegator
def cost
super + 5
end
end
item = Object.new
def item.cost; 10; end
def item.description; "widget"; end
item = TaxDecorator.new(item)
item = ShippingDecorator.new(item)
item.cost # => 17
item.description # => "widget" — automatically delegated
SimpleDelegator automatically forwards all method calls to the wrapped object unless you override them. This means you only define the methods you want to modify.
You can also pass any object to SimpleDelegator.new without subclassing:
require "delegate"
price = SimpleDelegator.new(100)
price.to_s # => "100" (delegated to Integer)
Using ActiveSupport::Notifications
When you need to observe and log method calls rather than modify their return values, ActiveSupport::Notifications provides an event system. This is a decorator that listens for events and acts on them without changing return values.
require "active_support/notifications"
class AuditDecorator < SimpleDelegator
def initialize(component, logger)
super(component)
@logger = logger
end
def cost
ActiveSupport::Notifications.instrument("cost.called", description: description) do
super
end.tap { |result| @logger.info "cost returned #{result}" }
end
private
def instrument_event(name, payload)
ActiveSupport::Notifications.instrument(name, payload)
end
end
This pattern is especially useful in Rails applications where you want to track performance or audit calls without cluttering the core business logic.
Building Stackable Decorators
The real power of decorators is that you can combine them in any order at runtime. This is called stacking.
class LoggingDecorator < SimpleDelegator
def initialize(component)
super(component)
@calls = []
end
def method_missing(meth, *args, &block)
@calls << [meth, args]
super
end
def call_log
@calls
end
end
class CachingDecorator < SimpleDelegator
def initialize(component)
super(component)
@cache = {}
end
def cost
@cache[:cost] ||= super
end
end
price = CachingDecorator.new(LoggingDecorator.new(Coffee.new))
price.cost
price.cost # served from cache
price.call_log # => [[:cost, []], [:cost, []]]
Notice that call_log only recorded two calls even though we called cost twice — the CachingDecorator intercepted the second call before it reached the LoggingDecorator. Order matters.
Testing Decorators
Decorators are easy to test in isolation because they have no dependencies on external state other than the wrapped object.
require "minitest/autorun"
class CachingDecoratorTest < Minitest::Test
def test_cost_returns_cached_value
component = Object.new
def component.cost; 5; end
decorator = CachingDecorator.new(component)
assert_equal 5, decorator.cost
assert_equal 5, decorator.cost # second call hits cache
end
def test_decorator_preserves_wrapped_interface
component = Object.new
def component.cost; 5; end
def component.description; "thing"; end
decorator = CachingDecorator.new(component)
assert_equal 5, decorator.cost
assert_equal "thing", decorator.description # not overridden
end
end
The key testing principle is that each decorator should be tested independently. Mock the wrapped component if needed, but don’t test the entire stack in a single test.
When to Use Decorators vs Module Mixins
Ruby gives you two ways to add behaviour to objects: decorators and module mixins.
| Decorator | Module Mixin | |
|---|---|---|
| Adds behaviour at | Runtime (per instance) | Class level (all instances) |
| Can be stacked | Yes, in any order | No — single inclusion |
| Requires interface matching | Yes | No — methods merge into class |
| Removes cleanly | Yes — unwrap to get original | Tricky — must use super everywhere |
Use a decorator when:
- You need to add behaviour to a specific instance at runtime
- You want to combine behaviours in different orders
- The base object comes from an external source you cannot modify
- You need to easily remove the decoration later
Use a module mixin when:
- The behaviour belongs to the class itself
- All instances of the class need the same behaviour
- You are building a framework or gem that extends core Ruby objects
# Module mixin — behaviour lives on the class
module Reportable
def generate_report
"Report for #{self.class}"
end
end
class Order
include Reportable
end
Order.new.generate_report # => "Report for Order"
# Decorator — behaviour is attached to a specific instance
class ExportDecorator < SimpleDelegator
def generate_report
"Export: #{super}"
end
end
ExportDecorator.new(Order.new).generate_report # => "Export: Report for Order"
The deciding question is: do you want this behaviour on all instances or just some instances? All instances means mixin. Some instances means decorator.
See Also
- /guides/ruby-service-objects/ — Service objects complement decorators by encapsulating business logic that decorators then wrap and layer
- /guides/ruby-blocks-procs-lambdas/ — Procs and lambdas are the building blocks of flexible decorator behaviour
- /guides/ruby-dependency-injection/ — Decorators are a natural fit inside dependency-injected systems