Welcome back! Having learned about the Adapter and Composite Patterns, you're well on your way to mastering structural design patterns in Python. 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.
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 and implement decorators like MilkDecorator
and SugarDecorator
to add features to the basic coffee object.
To understand the Decorator Pattern, let's think about a coffee shop where you can customize your coffee with various add-ons.
Consider a scenario in a coffee shop where you start with a simple cup of coffee and then enhance it with additional ingredients like milk, sugar, or whipped cream.
In this structure, each decorator extends the functionality of the original component dynamically. You start with a basic coffee and, by wrapping it with different decorators, you can create complex coffee orders. For example, if you have a simple coffee object and want to create a coffee with milk and sugar, you don't need to create a new class for every combination of coffee. Instead, you can layer existing decorators:
Python1simple_coffee = SimpleCoffee() 2milk_coffee = MilkDecorator(simple_coffee) 3milk_and_sugar_coffee = SugarDecorator(milk_coffee)
Decorator allows you to dynamically add or remove functionality to objects without altering their structure. This leads to more modular and maintainable code compared to subclassing for every possible combination of enhancements. In our coffee shop example, you can easily extend the coffee's behavior at runtime by adding decorators like milk, sugar, or any other ingredients.
We start by defining an abstract Coffee
class. This class includes a get_description
method and a cost
method that raises a NotImplementedError
. The SimpleCoffee
class extends Coffee
and provides the implementation for the cost
method.
Python1from abc import ABC, abstractmethod 2 3class Coffee(ABC): 4 def get_description(self): 5 return "Coffee" 6 7 @abstractmethod 8 def cost(self): 9 pass 10 11class SimpleCoffee(Coffee): 12 def cost(self): 13 return 2.0 14 15# Verify SimpleCoffee 16my_coffee = SimpleCoffee() 17print(f"Description: {my_coffee.get_description()}, Cost: {my_coffee.cost()}") 18# Description: Coffee, Cost: 2.0
Next, we create a CoffeeDecorator
class that also extends Coffee
. It takes an instance of Coffee
and delegates the calls to get_description
and cost
methods to the wrapped instance.
Python1class CoffeeDecorator(Coffee): 2 def __init__(self, decorated_coffee): 3 self.decorated_coffee = decorated_coffee 4 5 def get_description(self): 6 return self.decorated_coffee.get_description() 7 8 def cost(self): 9 return self.decorated_coffee.cost()
Following this, we extend CoffeeDecorator
to create a MilkDecorator
class. It overrides the get_description
and cost
methods to add the description and cost of milk to the original coffee.
Python1class MilkDecorator(CoffeeDecorator): 2 def get_description(self): 3 return f"{self.decorated_coffee.get_description()}, Milk" 4 5 def cost(self): 6 return self.decorated_coffee.cost() + 0.5 7 8# Add MilkDecorator 9my_coffee_with_milk = MilkDecorator(my_coffee) 10print(f"Description: {my_coffee_with_milk.get_description()}, Cost: {my_coffee_with_milk.cost()}") 11# Description: Coffee, Milk, Cost: 2.5
Similarly, we create a SugarDecorator
class by extending CoffeeDecorator
. This decorator adds sugar-related description and cost.
Python1class SugarDecorator(CoffeeDecorator): 2 def get_description(self): 3 return f"{self.decorated_coffee.get_description()}, Sugar" 4 5 def cost(self): 6 return self.decorated_coffee.cost() + 0.3 7 8# Add SugarDecorator 9my_coffee_with_milk_and_sugar = SugarDecorator(my_coffee_with_milk) 10print(f"Description: {my_coffee_with_milk_and_sugar.get_description()}, Cost: {my_coffee_with_milk_and_sugar.cost()}") 11# Description: Coffee, Milk, Sugar, Cost: 2.8
Here is the complete code combining all the sections:
Python1from abc import ABC, abstractmethod 2 3class Coffee(ABC): 4 def get_description(self): 5 return "Coffee" 6 7 @abstractmethod 8 def cost(self): 9 pass 10 11class SimpleCoffee(Coffee): 12 def cost(self): 13 return 2.0 14 15class CoffeeDecorator(Coffee): 16 def __init__(self, decorated_coffee): 17 self.decorated_coffee = decorated_coffee 18 19 def get_description(self): 20 return self.decorated_coffee.get_description() 21 22 def cost(self): 23 return self.decorated_coffee.cost() 24 25class MilkDecorator(CoffeeDecorator): 26 def get_description(self): 27 return f"{self.decorated_coffee.get_description()}, Milk" 28 29 def cost(self): 30 return self.decorated_coffee.cost() + 0.5 31 32class SugarDecorator(CoffeeDecorator): 33 def get_description(self): 34 return f"{self.decorated_coffee.get_description()}, Sugar" 35 36 def cost(self): 37 return self.decorated_coffee.cost() + 0.3 38 39# Example usage 40my_coffee = SimpleCoffee() 41print(f"Description: {my_coffee.get_description()}, Cost: {my_coffee.cost()}") 42 43my_coffee_with_milk = MilkDecorator(my_coffee) 44print(f"Description: {my_coffee_with_milk.get_description()}, Cost: {my_coffee_with_milk.cost()}") 45 46my_coffee_with_milk_and_sugar = SugarDecorator(my_coffee_with_milk) 47print(f"Description: {my_coffee_with_milk_and_sugar.get_description()}, Cost: {my_coffee_with_milk_and_sugar.cost()}") 48 49# Outputs: 50# Description: Coffee, Cost: 2.0 51# Description: Coffee, Milk, Cost: 2.5 52# Description: Coffee, Milk, Sugar, Cost: 2.8
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. Excited 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.