Decorator Pattern in Python

Decorator Pattern in Python

The power of extension at runtime

ยท

4 min read

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:

ย