Skip to Content
Course content

96: The Decorator Pattern in Ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a pull request for a junior dev working on a massive e-commerce platform. He had added a method to the User model called display_name_with_membership_badge. It looked innocent enough, but when I scrolled through the user.rb file, I realized it was already 1,200 lines long. The model was doing everything: database queries, password hashing, and now, deciding which HTML string to return for a UI badge. He was stuck in that classic trap where the model becomes a "God Object" because that's the only place that has access to the data.

This is exactly where the Decorator pattern saves your sanity. Instead of polluting your business logic with presentation concerns, you wrap your object in a "decorator" that adds the extra behavior. In Ruby, we can do this elegantly without writing a mountain of boilerplate code.

Stop Cramming Presentation into Your Models

When you find yourself writing methods in a model that return formatted strings, HTML, or currency symbols, you're violating the Single Responsibility Principle. Your User model should care about who the user is, not how their name looks in a sidebar. But if you move that logic to a helper method, you end up with a disconnected set of functions that are hard to test and maintain.

The Decorator pattern allows you to create a specialized object that "decorates" the original one. It maintains the same interface as the original object but adds new methods. If the decorator doesn't know how to handle a method call, it simply passes that call along to the wrapped object. It's like a thin skin over your model.

Leveraging SimpleDelegator for Clean Wrappers

You could implement this by manually forwarding every method call, but that's a waste of time. Ruby provides a built-in class called SimpleDelegator in the standard library that handles the heavy lifting for you. I almost always use this when I need a quick, clean decorator.

require 'delegate'

class User
  attr_reader :first_name, :last_name, :membership_level

  def initialize(first_name, last_name, membership_level)
    @first_name = first_name
    @last_name = last_name
    @membership_level = membership_level
  end
end

class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end

  def membership_badge
    case membership_level
    when :gold then "[Gold Member] #{full_name}"
    when :silver then "[Silver Member] #{full_name}"
    else full_name
    end
  end
end

user = User.new("Jane", "Doe", :gold)
decorated_user = UserDecorator.new(user)

puts decorated_user.full_name         # => Jane Doe
puts decorated_user.membership_badge  # => [Gold Member] Jane Doe
puts decorated_user.first_name        # => Jane (delegated to original User object)

Notice how decorated_user.first_name still works? Since UserDecorator inherits from SimpleDelegator, any method it doesn't explicitly define is automatically passed to the user instance we passed into the constructor. It's seamless.

Stacking Behavior Dynamically

One of the coolest things about this pattern is that decorators can be stacked. Since the decorator mimics the interface of the wrapped object, you can wrap a decorator in another decorator. I've used this in the past to apply different layers of formatting depending on whether a user is viewing a page in "Admin Mode" or "Guest Mode."

You might have a BaseUserDecorator for general formatting and an AdminUserDecorator that adds sensitive system IDs to the display. By stacking them, you don't have to create a massive inheritance tree of classes; you just compose the behavior you need at runtime. It keeps your code flexible and prevents that 1,200-line model file from ever happening in the first place.




📋 Practical Task

Build a Product Pricing Decorator

You are working on a store application. The Product model contains raw data (price, name, and a discount percentage), but the frontend needs specific formatting for different scenarios.

Your Task:

  • Create a Product class with name, price (float), and discount_percentage (integer).
  • Create a ProductDecorator using SimpleDelegator.
  • Implement a formatted_price method in the decorator that returns the price as a string with a currency symbol (e.g., "$19.99").
  • Implement a sale_price method that calculates the price after the discount and returns it as a formatted string.
  • Implement a price_display method that returns the sale_price if a discount exists, otherwise it returns the formatted_price.

Test your implementation by creating a product with a discount and one without, wrapping both in the decorator, and printing their price_display.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.