Good evening everyone, today we're going to learn one of my favorite design patterns, the decorator pattern. Please note that this is not about the Python language construct of the same name. This is about a design pattern, the concepts of which extend to most languages.
What is the Decorator Pattern?
Put simply, the decorator pattern is a means to add functionality to an object at runtime. If you're into UML, here's Wikipedia's class diagram of the pattern:
When to Use the Decorator Pattern
Decorators are typically used in cases where it might seem like you want to reach for inheritance, but from a design perspective, it wouldn't make sense.
For example, you may have a class called Dog
and you need it to have a method called eat
, you could probably put eat
in an Animal
super-class that Dog
and other animals inherit, thus gaining the eat
functionality.
But what if you have a Dog
that's on a leash? It might behave differently than a regular dog. We could make a class called LeashedDog
that inherited Dog
. That would give us a Dog
object with all the Dog
functionality as well as the LeashedDog
functionality.
However, let's think about what that implementation might look like. The classes would be like this:
# Leashed dog with inheritance
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print("RUFF!")
class LeashedDog(Dog):
def __init__(name):
super().__init__(name)
def tug_on_leash(self):
print("Let's go!!")
And using these classes:
dog = Dog('Jeff')
dog.bark()
# outputs "RUFF!"
dog = LeashedDog(dog.name)
dog.bark()
# outputs "RUFF!"
dog.tug_on_leash()
# outputs "Let's go!!"
And in this case that might be fine. But in this writer's humble opinion, it's poor design and I'll tell you why.
How I decide when to use a decorator
In the above example, we created a new instance of a dog. Our dog was still named Jeff, it could still bark, but creating a new instance kind of implies that it's a new dog.
The times when you should use decoration vs. inheritance is not always cut and dry. In fact, some might believe the above design is just fine, and really, it is. However, you may need this pattern for something more complex one day, so I'm keeping it simple so you'll understand it better.
If there was a rule-of-thumb for when to use decoration over inheritance, it would be when you have composite classes--classes that have other classes instead of classes that are other classes.
I usually opt for decoration when a new instance (like a new dog) doesn't seem quite right. Instead, I'm adding new abilities to an existing thing. Jeff can't tug on a leash until I put one on him.
Time to get meta
We're about to lightly dip our toes into the ocean of metaprogramming; changing the default behavior of the Python interpreter. There are very, very few times that metaprogramming is the answer to your problems, so do not abuse this technique.
With that warning out of the way, as far as metaprogramming goes, we're actually not going to do anything too daring.
Introducing __getattr__
and getattr
In Python, classes will have what are called "built in" functions normally starting with double underscores (__
). These functions have designated functionality and are used in ways normally hidden from us.
One such method is the __getattr__
method that all classes have. This method is called anytime you try to call a non-existent attribute or method on an object. For example, if we said dog.chase_tail()
, the Python interpreter tries to find chase_tail
in the dog
object, can't, and then calls __getattr__
, raising an AttributeError
.
Another method we're going to use is getattr
. getattr
takes as input parameters an object and stringified name of a function, then returns the function on the object. In our previous example, if we write getattr(dog, 'bark')
, we'll get the function object back. Even better, we can do getattr(dog, 'bark')()
and get the console to print "RUFF!". Isn't that cool?
The decorator in action
How do we want our decorator to act? Well, we want all the methods of Dog
, with an added one, tug_on_leash
. We don't want to use inheritance though, so somehow we have to preserve the functionality the Dog
class.
Here's the plan. We're going to make a class called LeashedDogDecorator
(you should always name design-pattern-component classes with their name like 'decorator', 'observer', etc.). We're going to expect a model
in the initialization--which in our case will be a dog
object--, and we're going to use __getattr__
to delegate calls to Dog
methods to the model
instance variable. That was a lot of words so let's just show it:
# Leashed dog with decoration
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print("RUFF!")
class LeashedDogDecorator:
def __init__(self, model):
self.model = model
self.model_attributes = [attribute for attribute in self.model.__dict__.keys()]
self.model_methods = [m for m in dir(self.model) if not m.startswith('_') and m not in self.model_attributes]
def __getattr__(self, func):
if func in self.model_methods:
def method(*args):
return getattr(self.model, func)(*args)
return method
elif func in self.model_attributes:
return getattr(self.model, func)
else:
raise AttributeError
def tug_on_leash(self):
print("Let's go!!")
YIKES!!
Before we talk about what's going on, let's make sure everything is working. At the bottom of my script I have:
dog = Dog('Jeff')
dog = LeashedDogDecorator(dog)
dog.bark()
dog.tug_on_leash()
print(dog.name)
And when I run this in my console I get
RUFF!
Let's go!!
Jeff
So we know it works!
What the heck is going on?
Let's go line by line starting with the constructor. We pass model
, which we know is going to be an instance of a Dog
class, which we then assign to the decorator class's model
attribute.
Then, we gather the list of attributes associated with the model (which, again, is a Dog
instance) by using the __dict__
built in, which returns a dictionary of a class's attributes and values (we are only interested in the attribute names, hence the keys()
call). Then, we gather all the methods in the class that are not in the model_attributes
list, and do not start with an underscore (because those are usually Python built ins).
Then, we overwrite the built in __getattr__
method for this class. Remember, any time a non-existent method is called on a class, __getattr__
is called. That's why func
is in the method signature, it collects the name of the attribute that was tried.
Now, since LeashedDogDecorator
is technically its own class, methods like bark()
and attributes like name
won't exist on it. That's why we've overwritten the __getattr__
method, to catch attempts to call Dog
methods.
When the attempted call is in the model_methods
list, we have to build a new method, call it with whatever arguments were passed along with it (hence the *args
), and return it. We do this by using getattr
and feeding it the model and name of the called function.
Conversely, when the attempted call is in the model_attributes
list, we know we're looking for an attribute and thus don't want to call it, so we simply return that value. We do this again by using getattr(model, func)
.
The last case in the if..elif..else
block is to raise an AttributeError
any time the called attribute doesn't exist on either the Dog
instance or the LeashedDogDecorator
. If we don't do this, any calls to non-existent attributes on this class will simply fail silently, which we don't want.
Finally, we add the tug_on_leash
method to the decorator, which is the actual decorated functionality. Now the class we're decorating has the new functionality.
Step by Step in the Interpreter
Let's try to fully understand what happens when we call bark()
on our decorated method. The bark
method is called on the LeashedDogDecorator
class, but it doesn't exist. By default, the __getattr__
method is called. When it is, we feed the name bark
and the model
of our dog to getattr
to run the method, giving us the appearance of a Dog
that just happens to now have leash-specific-behavior.
Technicalities, Warnings, and Conclusion
So this was definitely a doozy. You might be wondering why we'd go through all the trouble of writing that decorator class when the inheritance strategy worked just fine, and that's definitely a valid question.
Sometimes, inheritance is better, and in our example, it is probably the right way to go. But in complex systems, simple implementations aren't always viable, or they come at the cost of decreased "velocity" (a project-manager word for developer productivity).
This implementation, given our example, is certainly overkill, but there are ways to abstract away some of the tedious details and write simpler looking decorators. I recently wrote a three part series on Python decorators, with better examples and much much more detail on my personal blog. If you're interested and don't mind my shameless plug, part one can be found here.
A few warnings about using metaprogramming before we go. It is always dangerous to alter the default behavior of the language you're working with. Once you do this, you're essentially voiding the warranty and are on your own. Remember in our example we added the final else
clause to raise an AttributeError
? I only know to do that because when I implemented this in a project of my own and couldn't figure out why I was having errors that were going unreported. You have to be careful.
Another downside to the meta approach is that it slows down execution of your scripts. Sometimes this is acceptable; but all things come at a cost.
Anyway, if you have any questions, comments, or know a better implementation of this pattern (or just want to make fun of my code), feel free to leave a comment or @ me on Twitter. Happy decorating!
Top comments (0)