Exploring the Decorator Pattern with Ruby

Exploring the Decorator Pattern with Ruby

Welcome back! Having learned about the Adapter and Composite Patterns, you're well on your way to mastering structural design patterns in Ruby. In this lesson, we'll dive into the Decorator Pattern, another important structural pattern that allows us to add new functionalities to objects dynamically and transparently. This is particularly useful when you want to enhance the behavior of objects without modifying their code.

What You'll Learn

The Decorator Pattern enables you to wrap an object with additional behavior in a flexible and reusable way. You'll learn how to use this pattern to extend the functionality of objects in a clean and maintainable manner. For example, let's consider a simple coffee ordering system. By the end of this lesson, you will be able to:

  • Create a basic Coffee class
  • Implement decorators like MilkDecorator and SugarDecorator to add features to the basic coffee object using Ruby's object-oriented features

Here's a snippet from the code you'll be working with:

We start by defining a basic Coffee class with a description method that returns the name of the coffee and a cost method that returns the price of the coffee:

class Coffee
  def description
    "Coffee"
  end

  def cost
    raise NotImplementedError, "You must implement the cost method"
  end
end

class SimpleCoffee < Coffee
  def cost
    2.0
  end
end

Next, we define a CoffeeDecorator class that acts as a wrapper for the Coffee class. This serves as the base class for all decorators that add new features to the coffee object, such as milk or sugar - note, that the @decorated_coffee variable holds a reference to the coffee object being wrapped by the decorator:

class CoffeeDecorator < Coffee
  def initialize(coffee)
    # The @decorated_coffee variable holds a reference to the coffee object being wrapped by the decorator.
    # This allows the decorator to call the original methods of the coffee object and extend its behavior.
    @decorated_coffee = coffee
  end

  def description
    @decorated_coffee.description
  end
end

Finally, we implement concrete decorators like MilkDecorator and SugarDecorator that add milk and sugar to the coffee, respectively. These decorators extend the functionality of the decorated coffee object by adding new features:

class MilkDecorator < CoffeeDecorator
  def description
    @decorated_coffee.description + ", Milk"
  end

  def cost
    @decorated_coffee.cost + 0.5
  end
end

class SugarDecorator < CoffeeDecorator
  def description
    @decorated_coffee.description + ", Sugar"
  end

  def cost
    @decorated_coffee.cost + 0.2
  end
end

Let's now see how you can use these classes to create and customize coffee orders using the Decorator Pattern:

coffee = SimpleCoffee.new
puts "Description: #{coffee.description}, Cost: #{coffee.cost}" # Output: Description: Coffee, Cost: 2

coffee_with_milk = MilkDecorator.new(coffee)
puts "Description: #{coffee_with_milk.description}, Cost: #{coffee_with_milk.cost}" # Output: Description: Coffee, Milk, Cost: 2.5

coffee_with_milk_and_sugar = SugarDecorator.new(coffee_with_milk)
puts "Description: #{coffee_with_milk_and_sugar.description}, Cost: #{coffee_with_milk_and_sugar.cost}" # Output: Description: Coffee, Milk, Sugar, Cost: 2.7

Notice how decorators like MilkDecorator and SugarDecorator can be combined to create customized coffee orders. This allows you to add new features to the coffee object at runtime without modifying its code.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal