Inheritance is powerful but doesn’t always lead to flexible designs and Decorator Pattern is one of the solutions to balance between inheritance and flexibility
Decorators work like object wrappers that add functionality/behavior to the object during run time instead of compile time.
we said wrapper*S* with an S because you can wrap as many as your object need, adding layers to your Main object.
Structure of the Decorator Pattern
The Decorator Pattern consists of four main parts:
- Component – the main abstraction that can be used directly or wrapped by decorators.
- Concrete Component – the actual object whose behavior we want to extend dynamically.
- Decorator – implements the same interface as the component and has a component object that it wraps.
- Concrete Decorators – extend the decorator and add new behavior before or after delegating to the wrapped object.
At first, this may sound a bit abstract, so let's look at a practical example.
Example: Rack Middleware
If you've worked with Ruby on Rails, you've already used one of the best real-world examples of the Decorator Pattern: Rack middleware.
Each middleware wraps another application object, adds its own behavior, and then delegates the request to the wrapped object.
# Component
class App
def call(env)
raise NotImplementedError
end
end
# Concrete Component
class Application < App
def call(env)
puts "Processing request: #{env[:path]}"
[200, {}, ["OK"]]
end
end
# Decorator
class Middleware < App
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
end
end
# Concrete Decorators
class LoggingMiddleware < Middleware
def call(env)
puts "[LOG] Received request for #{env[:path]}"
response = @app.call(env)
puts "[LOG] Response status: #{response[0]}"
response
end
end
class AuthenticationMiddleware < Middleware
def call(env)
puts "[AUTH] Authenticating user"
if env[:authenticated]
@app.call(env)
else
[401, {}, ["Unauthorized"]]
end
end
end
class TimingMiddleware < Middleware
def call(env)
start = Time.now
response = @app.call(env)
elapsed = ((Time.now - start) * 1000).round(2)
puts "[TIMING] Request took #{elapsed}ms"
response
end
end
# Usage
app = Application.new
app = LoggingMiddleware.new(app)
app = AuthenticationMiddleware.new(app)
app = TimingMiddleware.new(app)
env = {
path: "/articles",
authenticated: true
}
response = app.call(env)
p response
Output:
[AUTH] Authenticating user
[LOG] Received request for /articles
Processing request: /articles
[LOG] Response status: 200
[TIMING] Request took 0.12ms
[200, {}, ["OK"]]
In this example:
-
Applicationis our Concrete Component. -
Middlewareis the Decorator that wraps another component. -
LoggingMiddleware,AuthenticationMiddleware, andTimingMiddlewareare Concrete Decorators. - Each middleware adds behavior before, after, or around the original
callmethod. - We can add, remove, or reorder middleware without modifying the original
Applicationclass.
What's particularly interesting is that this is almost exactly how Rack itself builds a web application stack:
app = Rails.application
app = Rack::Runtime.new(app)
app = Rack::MethodOverride.new(app)
app = Rack::Session::Cookie.new(app)
Each middleware wraps the previous object, creating a chain of decorators that process requests and responses.
In real Rack applications there isn't a common App base class because Ruby relies on duck typing. As long as an object responds to call, it can participate in the middleware stack.
The example above introduces an explicit App class only to make the Decorator Pattern structure easier to see.
Takeaways
- Decorators use inheritance mainly for type compatibility, not for behavior reuse.
- New behavior is acquired through composition rather than deep inheritance hierarchies.
- The Decorator Pattern involves a family of decorator classes that wrap concrete components.
- A component can be wrapped by any number of decorators.
- Decorators share the same interface as the objects they decorate.
- The pattern follows the principle of favoring composition over inheritance.
Why favoring composition ?
inheritance becomes difficult to manage when you're trying to mix and match behaviors.
The number of subclasses grows quickly because every new feature creates more combinations.
With the Decorator Pattern, each behavior becomes its own object that can be composed dynamically.
The next time you find yourself creating subclasses for every possible combination of features, it may be worth asking whether a decorator would provide a more flexible solution.
Top comments (0)