8. Decorator Pattern#
8.1. Introduction#
The Decorator Pattern is a structural design pattern that allows you to dynamically add behavior or responsibilities to objects without modifying their code. This pattern is useful for adhering to the Open/Closed Principle, which states that classes should be open for extension but closed for modification.
8.2. Motivation#
In software development, there are scenarios where you need to add responsibilities to individual objects dynamically and transparently. The Decorator Pattern addresses this by providing a flexible alternative to subclassing for extending functionality.
8.3. Implementation#
Let’s dive into the implementation of the Decorator Pattern. In this pattern, we define a component interface that declares common operations, concrete components that implement the interface, and decorator classes that wrap concrete components to extend their behavior.
We define a Beverage
interface with cost
and description
methods.
from abc import ABC, abstractmethod
# Component
class Beverage(ABC):
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
We define an Espresso
class as a concrete component that implements the Beverage
interface.
# Concrete Component
class Espresso(Beverage):
def cost(self):
return 1.99
def description(self):
return "Espresso"
We define an AddOnDecorator
abstract class that also implements the Beverage
interface and wraps a Beverage
object.
# Decorator
class AddOnDecorator(Beverage, ABC):
def __init__(self, beverage):
self._beverage = beverage
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
We define MilkDecorator
and MochaDecorator
classes as concrete decorators that extend the behavior of the wrapped Beverage
object.
# Concrete Decorators
class MilkDecorator(AddOnDecorator):
def cost(self):
return self._beverage.cost() + 0.50
def description(self):
return self._beverage.description() + ", Milk"
class MochaDecorator(AddOnDecorator):
def cost(self):
return self._beverage.cost() + 0.75
def description(self):
return self._beverage.description() + ", Mocha"
Now, let’s see how we can use the Decorator Pattern in a real-life example of a coffee shop where customers can add various ingredients to their beverages.
# Client code
espresso = Espresso()
print(f"{espresso.description()} - ${espresso.cost()}")
# Add milk to the espresso
espresso_with_milk = MilkDecorator(espresso)
print(f"{espresso_with_milk.description()} - ${espresso_with_milk.cost()}")
# Add mocha to the espresso with milk
espresso_with_milk_and_mocha = MochaDecorator(espresso_with_milk)
print(f"{espresso_with_milk_and_mocha.description()} - ${espresso_with_milk_and_mocha.cost()}")
Espresso - $1.99
Espresso, Milk - $2.49
Espresso, Milk, Mocha - $3.24
In this example:
We create an
Espresso
object representing a simple coffee.We decorate the
Espresso
object withMilkDecorator
to add milk.We further decorate the
Espresso
object withMochaDecorator
to add mocha.We print the description and cost of each beverage to see how the decorators dynamically add behavior to the
Espresso
.
8.4. Benefits & Drawbacks#
Benefits
Allows for the dynamic addition of behavior to objects without modifying their code.
Promotes adherence to the Open/Closed Principle.
Provides flexibility and composability in extending object behavior.
Drawbacks
Can lead to a large number of small classes, increasing code complexity.
May introduce performance overhead due to the additional layers of abstraction.