Do you know why the other developers in your team hate you? not because they are jealous of your unique skills in overusing inheritance, Oh, wait... maybe it is.
What problem does it solve? ๐ค
The Decorator Pattern is one of the OOP's well-known patterns, which also has a similar variation in functional programming like method chaining. This Pattern solves a common problem when it comes to inheritance.
For instance, in an application where a lot of people could have different types of clothes like shirts, pants, sunglasses, and jackets.
class Person:
def __init__(self, n: str) -> None:
self.name = n
print(f'{n}, just spawned')
def speak(self) -> str:
return "I'm ${n}"
class PersonWithSunglasses(Person):
def speak(self) -> None:
return "I'm a person with sunglasses"
class PersonWithJacket(Person):
def speak(self) -> None:
return "I'm a person with a jacket"
class PersonWithPants(Person):
def speak(self) -> None:
return "I'm a person with pants"
class PersonWithPantsAndJacket(Person):
def speak(self) -> None:
return "I'm a person with pants and a jacket"
class PersonWithPantsAndSunglasses(Person):
def speak(self) -> None:
return "I'm a person with pants and sunglasses"
class PersonWithSunglassesAndJacket(Person):
def speak(self) -> None:
return "I'm a person with sunglasses and a jacket"
#
# And so...
#
person = PersonWithPantsAndJacket("Joe")
print(person.speak()) # Outputs: I'm a person with Pants and Jacket
Did you notice? that once we wanted more customizable objects, the code started to become complex, ugly, and repetitive?
Just imagine what it takes if you want to add or remove only one more type of clothes.
Decorator Pattern vs Python Decorators โ
Decorator Pattern isn't the same as Python Decorators @
, the Decorator Pattern is an OOP design pattern where functionality is added to an object at the runtime, where Python Decorators leverages Python's support for closures to add functionality for methods at definition time.
This means Python Decorators cannot be added, replaced, or removed from a function while the app is running.
Decorator Pattern vs Strategy Pattern โ
These two patterns are pretty much the same, the only difference between these two is that Strategy replaces the whole functionality of some function in an object.
But Decorator Pattern are meant to append a new functionality after wrapping
Implementation โ
Using the previous Person
example, let's implement the decorator design, where I don't have to keep adding these repetitive objects.
We will do this by having the regular Person
without any change
class Person:
def __init__(self, n: str) -> None:
self.name = n
print(f'{n}, just spawned')
def speak(self) -> str:
return f"I'm ${self.name}"
We will have an abstract class called PersonDecorator
that extends Person
, in addition to setting a reference in the constructor.
class Person:
def __init__(self, n: str) -> None:
self.name = n
print(f'{n}, just spawned')
def speak(self) -> str:
return f"I'm ${self.name}"
class PersonDecorator:
def __init__(self, person: Person) -> None:
self._person = person
And last, will define only the main objects a person could be decorated with
- Sunglasses
- Pants
- Jacket
- Shirts
Each of these classes will extend PersonDecorator
class WithSunglasses(PersonDecorator):
def speak(self) -> None:
return f"{self._person.speak()} with sunglasses"
class WithJacket(PersonDecorator):
def speak(self) -> None:
return f"{self._person.speak()} with a jacket"
class WithShirt(PersonDecorator):
def speak(self) -> None:
return f"{self._person.speak()} with a shirt"
class WithPants(PersonDecorator):
def speak(self) -> None:
return f"{self._person.speak()} with pants"
And that's it, let's test it
person0 = Person("Mike")
person1 = WithJacket(WithSunglasses(Person("Joe")))
person2 = WithPants(Person("Zoe"))
person3 = WithShirt(WithPants(Person("John")))
print(person0.speak())
print(person1.speak())
print(person2.speak())
print(person3.speak())
Outputs:
Mike, just spawned
Joe, just spawned
Zoe, just spawned
John, just spawned
I'm a Mike
I'm a Joe with sunglasses with a jacket
I'm a Zoe with pants
I'm a John with pants with a shirt
The Dark Side of Decorators ๐ป
While yes, developers see this Pattern as a Hero, our guy still has his share of problems
- Sometimes, it may result in many small classes that could make a design harder to read and understand.
- it's hard to introduce with other design patterns, most of the time you will need to refactor old code to be able to implement it.
- it's hard to come up with decorators that are not affected by the order of implementation.
Conclusion
The Decorator Pattern is trivial to implement in Python, due to Python's duck-typed nature, and it goes in line with the first SOLID principle aka the Single Responsibility principle .
I hope you have learned something new about the Decorator Pattern, if you have anything in mind, questions, or suggestions, don't hesitate to leave a comment.
I'm planning to cover the most common design patterns. if you are interested, you can follow me to get notified of any new articles I publish in the future or check the below list of my published articles about design patterns: