TUTORIAL SERIES

Design Patterns in Python: Decorator

Dynamic Behavior Extension

Amir Lavasani
8 min readNov 20, 2023

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Decorator Pattern

The Decorator design pattern is a structural pattern that adds behavior to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.

In other words, it enables you to “decorate” or enhance objects by adding new functionalities without modifying their structure.

This pattern is handy when you want to extend the capabilities of objects in a flexible and reusable way.

When to Use the Decorator Pattern

  1. Add Responsibilities Dynamically: When you want to add behavior or features to objects dynamically at runtime without modifying their code.
  2. Avoid a Complex Inheritance Hierarchy: To avoid creating a deep and complex class hierarchy through subclassing, which can be challenging to maintain.

Practical Example: Game Character Abilities

Consider a scenario in game design where different characters possess a unique set of abilities. As the game evolves, we need to add new abilities.

We’ll use game character abilities, such as the DoubleDamageDecorator, FireballDecorator, and InvisibilityDecorator, as examples. Characters can have different combinations of these abilities at any given moment.

Handling all subclass combinations with inheritance leads to complex code. The Decorator Pattern simplifies this, adding abilities to characters while keeping the code clean and flexible.

Circular layers representing decorators surrounding an inner core object
Dall-E generated image with the following concept: Circular layers representing decorators surrounding an inner core object

Key Components of the Decorator Pattern

The Decorator pattern lets you add behavior to objects without messing with others. To get it right, you need to know the essential parts.

  1. Component: The base that both concrete components and decorators share. It defines the core functions that can be extended.
  2. Concrete Component: The basic objects where we add extra behaviors. Decorators wrap around these.
  3. Decorator: An abstract class that follows the Component interface. It links to a Component and adds its specific functions to the core.
  4. Concrete Decorator: Classes that extend the Decorator abstract class, implementing specific behaviors. You can stack these to create chains of decorators.

These parts are crucial for using the Decorator pattern effectively, letting you add or remove features from objects dynamically while keeping your code clean and manageable.

Decorator design pattern structure diagram. Image from refactoring.guru

Implementing the Decorator Pattern in Python

In this section, we demonstrate the Decorator design pattern’s role in enhancing a character’s abilities. By dynamically applying decorators, such as Double Damage and Invisibility, to a basic character, we create a special character with combined powers.

Step 1: Component

The Component is the base interface that defines the core behavior that can be enhanced. In our example, it represents the game character with a basic attack method.

from abc import ABC, abstractmethod

# Step 1: Component - The base game character
class Character(ABC):
@abstractmethod
def get_description(self):
pass

@abstractmethod
def get_damage(self):
pass

Step 2: Concrete Component

Concrete Components are the basic game characters to which we can add special abilities. In this step, we create the basic character class.

# Step 2: Concrete Component - Basic game character
class BasicCharacter(Character):
def get_description(self):
return "Basic Character"

def get_damage(self):
return 10

Step 3: Decorator

The Decorator is an abstract class that also implements the Component interface. It has a reference to a Component object and adds its specific behavior to the component’s core. In this step, we create the abstract decorator class.

# Step 3: Decorator - Abstract decorator class
class CharacterDecorator(Character, ABC):
def __init__(self, character):
self._character = character

@abstractmethod
def get_description(self):
pass

@abstractmethod
def get_damage(self):
pass

Step 4: Concrete Decorator

Concrete Decorators are classes that extend the Decorator abstract class, implementing specific behaviors. In this step, we create concrete decorator classes for special character abilities, such as Double Damage.

# Step 4: Concrete Decorator
class DoubleDamageDecorator(CharacterDecorator):
def get_description(self):
return self._character.get_description() + " with Double Damage"

def get_damage(self):
return self._character.get_damage() * 2

class FireballDecorator(CharacterDecorator):
def get_description(self):
return self._character.get_description() + " with Fireball"

def get_damage(self):
return self._character.get_damage() + 20

class InvisibilityDecorator(CharacterDecorator):
def get_description(self):
return self._character.get_description() + " with Invisibility"

def get_damage(self):
return self._character.get_damage()

Step 5: Client Code

The Client is responsible for creating and configuring the decorators and using them with the core character. In this step, we demonstrate how to create a character with various abilities.

# Step 5: Client Code - Creating a character with abilities

if __name__ == "__main__":
character = BasicCharacter()
print(character.get_description()) # Output: "Basic Character"
print(character.get_damage()) # Output: 10

# Create different decorators
double_damage_decorator = DoubleDamageDecorator(character)
fireball_decorator = FireballDecorator(character)
invisibility_decorator = InvisibilityDecorator(character)

# Apply decorators individually
print(double_damage_decorator.get_description()) # Output: "Basic Character with Double Damage"
print(double_damage_decorator.get_damage()) # Output: 20

print(fireball_decorator.get_description()) # Output: "Basic Character with Fireball"
print(fireball_decorator.get_damage()) # Output: 30

print(invisibility_decorator.get_description()) # Output: "Basic Character with Invisibility"
print(invisibility_decorator.get_damage()) # Output: 10

# Combine decorators
double_fireball_character = DoubleDamageDecorator(FireballDecorator(character))
print(double_fireball_character.get_description()) # Output: "Basic Character with Double Damage with Fireball"
print(double_fireball_character.get_damage()) # Output: 60

invisibility_double_fireball_character = InvisibilityDecorator(double_fireball_character)
print(invisibility_double_fireball_character.get_description()) # Output: "Basic Character with Invisibility with Double Damage with Fireball"
print(invisibility_double_fireball_character.get_damage()) # Output: 60

This code structure allows you to stack multiple decorators on a character to enhance their abilities. The decorators modify the character’s description and damage as they are applied.

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Advantages of the Decorator Pattern

The Decorator design pattern offers several notable advantages:

  1. Extensibility without Subclassing: It lets you add more abilities to an object without creating new subclasses. This makes your code more flexible and avoids changing existing code.
  2. Runtime Behavior Changes: Decorators allow you to change an object’s behavior while the program is running. You can add or remove things an object can do as needed.
  3. Combining Abilities: You can put multiple decorators around an object, combining different abilities. This simplifies making complex, customized objects.
  4. Single Responsibility Principle: The Decorator pattern follows a rule that each class should do one thing well. This makes code cleaner and easier to manage.

Considerations and Potential Drawbacks

While the Decorator design pattern offers many advantages, it also comes with some disadvantages:

  1. Abundance of Small Elements: Implementing decorators may lead to a proliferation of small elements in the design. This can result in numerous decorator classes, making the overall design harder to manage and understand.
  2. Complexity and Learning Curve: The Decorator pattern is not beginner-friendly. It requires a solid understanding of object-oriented programming principles, and beginners may find it challenging to grasp initially.
  3. Debugging Challenges: Debugging can be challenging due to the extended decorator components. Identifying the source of issues in a complex decorator stack may require additional effort and careful inspection.
  4. Potential High Complexity: The architecture of a system utilizing the Decorator pattern can become highly complex, particularly when there are many decorators and intricate interactions between them. This increased complexity can make the system harder to maintain and understand.
Dall-E generated image with the following concept: Multiple transparent layers, each representing a different decorator

Common Mistakes to Avoid

When working with the Decorator Pattern, it’s essential to be aware of common mistakes to ensure a solid implementation:

  1. Complex Decorators: Avoid crafting overly complex decorators that handle too many tasks. Each decorator should serve a specific purpose, avoiding overwhelming complexity.
  2. Decorator Order Matters: The order in which decorators are applied is crucial. Apply decorators in a logical sequence to achieve the desired behavior.
  3. Neglecting the Base Class: Keep the base class, like our Game Character, as straightforward as possible. Neglecting this can lead to unnecessary code complications.

Python Decorator vs. Decorator Design Pattern

Python Function Decorator

Python function decorators are a feature of the Python programming language that allows you to modify the behavior of functions or methods. They are used to add functionality to existing functions without modifying their source code.

A Python function decorator is essentially a function that takes another function as an argument, extends its behavior, and returns a new function. It’s a Pythonic way of achieving aspects like logging, caching, or authentication.

Code Example: Python Function Decorator

def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()

In this example, the my_decorator function is a Python function decorator that wraps the say_hello function, adding behavior before and after it is called.

Key Differences

In summary, Python function decorators are focused on modifying individual functions, while the Decorator Design Pattern is used for enhancing and extending the behavior of objects through a composition of decorators.

Purpose

  • Python Function Decorator: Primarily used for modifying the behavior of functions or methods.
  • Decorator Design Pattern: Used for dynamically enhancing the functionalities of objects or classes.

Application

  • Python Function Decorator: Typically used for concerns like logging, caching, or authentication within a single function or method.
  • Decorator Design Pattern: Used for extending the behavior of objects by stacking multiple decorators.

Inheritance

  • Python Function Decorator: Does not use inheritance, as it extends the behavior of a single function directly.
  • Decorator Design Pattern: Utilizes inheritance and composition, where decorators have the same interface as the components they decorate.

Flexibility

  • Python Function Decorator: Limited to modifying a single function or method.
  • Decorator Design Pattern: Offers more flexibility, allowing multiple decorators to be applied to an object or class in a specific order.
Dall-E generated image with the following concept: Abstract tree trunk with growth rings. Each ring signifies a decorator, and the rings’ sequence illustrates how decorators add complexity and depth to an object,

Relations with Other Patterns — TL;DR;

The Decorator pattern has connections with other design patterns, each serving unique purposes.

Decorator vs. Adapter

  • Changing Interfaces: The Adapter transforms an object’s interface entirely, while the Decorator typically extends or maintains the interface. Also, the Decorator supports recursive composition, which the Adapter does not.

Decorator vs. Proxy

  • Interface Handling: Proxy and Decorator both enhance objects but differ in interface handling. Proxy keeps the interface unchanged, acting as a stand-in, while the Decorator expands it to add functionalities.
  • Control of Composition: Despite structural similarities, Proxy and Decorator differ in intent. Proxy handles object life cycles, while Decorator focuses on feature addition, with client control.

Decorator vs. Chain of Responsibility

  • Composition and Execution: Chain of Responsibility and Decorator share similar structures, relying on composition for execution. In Chain of Responsibility, handlers can work independently and stop the request. Decorators extend behavior while keeping the flow.

Decorator vs. Composite

  • Composition in Structures: Composite and Decorator employ similar structures with composition. But the Decorator adds responsibilities to objects, while the Composite aggregates results.
  • Cooperation: The Decorator and Composite can cooperate, where the Decorator can enhance specific objects within the Composite structure.

Strategy Pattern

  • Changing Skin vs. Guts: The decorator changes the “skin” or additional responsibilities, while Strategy alters the “guts” or core behavior.

Conclusion

The Decorator pattern dynamically enhances object features, offering flexibility and code reusability in Python.

This article introduced its principles, applications, and practical Python implementation for more elegant, extendable software design.

Hope you enjoyed the Decorator pattern exploration 🙌 Happy coding! 👨‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Decorator Pattern
  3. Head First Design Patterns (Book)
  4. Decorator Design Pattern
  5. scaler Decorator Design Pattern

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃