DEV Community

Cover image for Design Patterns in Ruby #3: Decorator Pattern
Shrouk Abozeid
Shrouk Abozeid

Posted on

Design Patterns in Ruby #3: Decorator Pattern

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:

  1. Component – the main abstraction that can be used directly or wrapped by decorators.
  2. Concrete Component – the actual object whose behavior we want to extend dynamically.
  3. Decorator – implements the same interface as the component and has a component object that it wraps.
  4. 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
Enter fullscreen mode Exit fullscreen mode

Output:

[AUTH] Authenticating user
[LOG] Received request for /articles
Processing request: /articles
[LOG] Response status: 200
[TIMING] Request took 0.12ms

[200, {}, ["OK"]]
Enter fullscreen mode Exit fullscreen mode

In this example:

  • Application is our Concrete Component.
  • Middleware is the Decorator that wraps another component.
  • LoggingMiddleware, AuthenticationMiddleware, and TimingMiddleware are Concrete Decorators.
  • Each middleware adds behavior before, after, or around the original call method.
  • We can add, remove, or reorder middleware without modifying the original Application class.

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)
Enter fullscreen mode Exit fullscreen mode

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)