Lesson 3
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:

Ruby
1class Coffee 2 def description 3 "Coffee" 4 end 5 6 def cost 7 raise NotImplementedError, "You must implement the cost method" 8 end 9end 10 11class SimpleCoffee < Coffee 12 def cost 13 2.0 14 end 15end

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:

Ruby
1class CoffeeDecorator < Coffee 2 def initialize(coffee) 3 # The @decorated_coffee variable holds a reference to the coffee object being wrapped by the decorator. 4 # This allows the decorator to call the original methods of the coffee object and extend its behavior. 5 @decorated_coffee = coffee 6 end 7 8 def description 9 @decorated_coffee.description 10 end 11end

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:

Ruby
1class MilkDecorator < CoffeeDecorator 2 def description 3 @decorated_coffee.description + ", Milk" 4 end 5 6 def cost 7 @decorated_coffee.cost + 0.5 8 end 9end 10 11class SugarDecorator < CoffeeDecorator 12 def description 13 @decorated_coffee.description + ", Sugar" 14 end 15 16 def cost 17 @decorated_coffee.cost + 0.2 18 end 19end

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

Ruby
1coffee = SimpleCoffee.new 2puts "Description: #{coffee.description}, Cost: #{coffee.cost}" # Output: Description: Coffee, Cost: 2 3 4coffee_with_milk = MilkDecorator.new(coffee) 5puts "Description: #{coffee_with_milk.description}, Cost: #{coffee_with_milk.cost}" # Output: Description: Coffee, Milk, Cost: 2.5 6 7coffee_with_milk_and_sugar = SugarDecorator.new(coffee_with_milk) 8puts "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.

Key Components of the Decorator Pattern
  • Component: Defines the interface for objects that can have responsibilities added to them dynamically. In our example, the Coffee class is the component that defines the basic interface for coffee objects.
  • ConcreteComponent: Represents the basic object to which additional responsibilities can be added. In our example, the SimpleCoffee class is a concrete component that implements the basic coffee object.
  • Decorator: Maintains a reference to a Component object and defines an interface that conforms to the Component interface. In our example, the CoffeeDecorator class is the decorator that extends the functionality of the Coffee object.
  • ConcreteDecorator: Adds new responsibilities to the Component object. In our example, the MilkDecorator and SugarDecorator classes are concrete decorators that add milk and sugar to the coffee object, respectively.
Use Cases

Here are a few scenarios where the Decorator Pattern can be effectively applied:

  • Extending UI Components: You can use decorators to add new features like borders, shadows, or animations to user interface components without modifying their code.
  • Logging Systems: Decorators can be used to add logging functionality to objects at runtime, allowing you to log different aspects of the object's behavior.
  • Graphical Rendering Engines: Decorators can be used to add visual effects like blur, sepia, or grayscale to images without altering the original image data.
  • Security Systems: Decorators can be used to add encryption or authentication features to objects without changing their core functionality.
Pros and Cons

Let's explore the benefits and drawbacks of using the Decorator Pattern:

  • Pros:
    • Flexibility: Decorators allow you to add new features to objects dynamically and transparently.
    • Open-Closed Principle: The Decorator Pattern follows the Open-Closed Principle, allowing you to extend the functionality of objects without modifying their code.
    • Composability: Decorators can be combined in various ways to create complex object configurations.
    • Separation of Concerns: Decorators separate the core functionality of objects from the additional features, making the code more modular and maintainable.
  • Cons:
    • Complexity: The Decorator Pattern can lead to a large number of small classes when many decorators are used, which may increase code complexity.
    • Ordering: The order in which decorators are applied can affect the behavior of the object, which may require careful design and testing.
Why It Matters

Mastering the Decorator Pattern is important because it allows you to extend the functionality of objects at runtime without altering their structure. This leads to more flexible and maintainable code compared to using subclassing for every new feature.

In our coffee example, you can easily add new features like milk and sugar by layering decorators, which can be combined in various ways. Whether you need to add functionality to user interface components, logging systems, or graphical rendering engines, the Decorator Pattern provides a powerful and elegant solution.

Ready to see how this unfolds? Let's get started with the practice section, where you'll implement and extend the Decorator Pattern step-by-step.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.