Note: This is not about Python’s language feature called decorators (with the @
symbol), but about the design patterns called “decorator” and “delegate.”
In the previous article, we learned how to implement the Delegation pattern in Python. With this knowledge, we’ll now learn about the Decorator pattern, which will make use of delegation. We’ll learn about when we might use it and implement a couple of examples.
What is Decoration?
The decorator pattern is what we use when we want to add functionality to an object, but not by extending that object’s class. This pattern is often needed to maintain our adherence to the Single Responsibility Principle (SRP) of Object Oriented Design because it allows behavior to be added dynamically when needed without affecting the underlying class.
Like delegation, the decorator pattern is often used when inheritance might solve the problem, but doesn’t actually make sense in terms of design or is otherwise infeasible. Oftentimes, decorators are used in user interface development when data presented by a class or method might be expected to be displayed on a variety of mediums (web page, spreadsheet, API response, etc).
For an example, we’re going to make a few classes to imitate a simple human resources system.
The HR System
Let’s build a couple simple classes, one for employees and one for a “year to date” (YTD) salary report. This report will list how much an employee has been paid from the start of the year until now. The two classes will look like this:
from datetime import datetime
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class SalaryYTDReport:
def __init__(self, employees):
self.employees = employees
self.data = []
def prep_data(self):
self.data = []
for employee in self.employees:
emp_sal_dict = {'Employee': employee.name, 'YTD Salary': self.calculate_YTD_salary(employee.salary)}
self.data.append(emp_sal_dict)
def calculate_YTD_salary(self, salary):
current_month = datetime.today().month
ytd_salary = (salary/12) * current_month
ytd_salary = round(ytd_salary, 2)
return ytd_salary
The SalaryYTDReport
class creates an array of employee dictionaries. Eventually, we’ll want to display this information, right? But there are so many options to do so; we could display it in the terminal, on a web page, or in a spreadsheet.
Is it then the SalaryYTDReport
class’s responsibility to define the methods required to display the data in these formats? Well, if you believe in the Single Responsibility Principle (SRP), then no. This class gathers the data its name implies it reports and structures it in a report-like format. Displaying the data is not SalaryYTDReport
‘s prerogative.
So what should we do? If the only tool you ever reach for is inheritance, you might think creating a Reporting
class containing the report-displaying methods that SalaryYTDReport
should inherit is the proper strategy. Or you might think it should go the other way, a Reporting
class that inherits the SalaryYTDReport
class whose responsible only for formatting the data properly.
These solutions might work for small or even mid scale operations. But what if the company has employees that work hourly rather than for a salary? What if different departments want to implement their own report displaying methods, making any of the ones we come up with dead code to them? Enter the decorator.
The Report Decorator
To maintain the SRP of the SalaryYTDClass
, its only responsibility should be to report the YTD salary data. Any additional functionality to this class will require the class to be renamed. This of course isn’t a bad thing, but it should highlight the fact that anything above what the class is currently doing is too much functionality.
We’re going to make a class that will essentially add reporting functionality to any report class (in our case, the SalaryYTDReport
class). Let’s say we know that we will only ever display this report in HTML on our web page, but our controller layer is doing other things with the report data. In that case, we need a class that will maintain the functionality of the SalaryYTDReport
class as well as provide methods to display the report in HTML.
This is where the things we learned about delegation will come in handy. We will create a decorator class that will delegate the base functionality of SalaryYTDReport
while adding the ability to display the report in HTML. In other words, we are going to decorate the SalaryYTDReport
class with HTML formatting abilities.
We will use the same delegation approach we used in part one of this series, but with a very important difference. In the SalaryYTDReport
class, we want to access both prep_data()
as well as data
. The problem with the approach we took in part one is that it only works with class methods, not attributes. In other words, if we approach this problem the exact same way we did in part one, report.data
will throw an error.
To remedy this, we’re going to use Python’s builtin __dict__
to figure out attributes, while using our old dir
approach to figure out methods. Then we will edit our __getattr__
rewrite just a bit. Altogether, the class will look like this:
class HTMLReportDecorator:
def __init__(self, report):
self.html_report = []
self.report = report
self.report_methods = [f for f in dir(SalaryYTDReport) if not f.startswith('_')]
self.report_attributes = [a for a in report.__dict__.keys()]
def __getattr__(self, func):
if func in self.report_methods:
def method(*args):
return getattr(self.report, func)(*args)
return method
elif func in self.report_attributes:
return getattr(self.report, func)
else:
raise AttributeError
def report_data(self):
self.html_report = []
self.prep_data()
for row in self.data:
name = f"<b>{row['Employee']}</b>"
ytd = f"<i>{row['YTD Salary']}</i>"
html_row = f"{name}: {ytd}<br />"
self.html_report.append(html_row)
Notice how we handle the attributes vs. the functions of the base class now. In the case of methods, we return a function and call it, otherwise we simply return the requested attribute.
Now, we will create a SalaryYTDReport
object, and decorate it by use that object to instantiate an instance of the HTMLReportDecorator
, which we will assign to the same variable we used to instantiate the SalaryYTDReport
. This is a typical practice because we still use the decorated object as if it were the base object with a few extra features:
>>> from report import *
>>> emp1 = Employee('Bob', 100000)
>>> emp2 = Employee('Jan', 150000)
>>> emp3 = Employee('Erik', 30)
>>> report = SalaryYTDReport([emp1, emp2, emp3])
>>> report = HTMLReportDecorator(report)
>>> # Show it still acts like SalaryYTDReport:
>>> for employee in report.employees:
... print(employee.name)
Bob
Jan
Erik
>>> # Show it has the decorator's functionality too:
>>> report.report_data() # Creates the report
>>> report.html_report # Shows the report
['<b>Bob</b>: <i>66666.67</i><br />', '<b>Jan</b>: <i>100000.0</i><br />', '<b>Erik</b>: <i>20.0</i><br />']
We have successfully decorated the report object. The object will behave exactly like its base class self (SalaryYTDReport
in this case) while still having the functionality of the decorating class.
Logging Methods
The preceding example shows us a pretty basic implementation of the decorator pattern where we essentially add functionality to a class without rewriting that class. If you think about it, this isn’t really all that impressive. Let’s implement a use-case for the decorator method that’s a little more complex: logging
Let’s say for some reason, we wanted to log the start and finish of our methods. This could be done for a variety of reasons like future analysis of user behavior or for gathering performance insights. Let’s implement a solution for the latter.
We’re going to write a decorator for the SalaryYTDReport
that times and logs every method call. It would look something like this:
import time # We're going to use a sleep() to illustrate our logging
...
class PerformanceLogReportDecorator:
def __init__(self, report):
self.report = report
self.report_methods = [f for f in dir(SalaryYTDReport) if not f.startswith('_')]
self.report_attributes = [a for a in report.__dict__.keys()]
def __getattr__(self, func):
if func in self.report_methods:
def method(*args):
return self.log(func, *args)
return method
elif func in self.report_attributes:
return getattr(self.report, func)
else:
raise AttributeError
def log(self, func, *args):
start = datetime.now()
getattr(self.report, func)(*args)
time.sleep(1) # Putting this here to show logging is actually happening
end = datetime.now()
microseconds = (end-start).microseconds
print(f"{func} ran in {microseconds} microseconds")
Again, we use it just as we would the report
class, but since we decorate it with the PerformanceLogReportDecorator
class, we get the bonus functionality of logging:
>>> from report import *
>>> emp1 = Employee('Bob', 100000)
>>> emp2 = Employee('Jan', 150000)
>>> emp3 = Employee('Erik', 30)
>>> report = SalaryYTDReport([emp1, emp2, emp3])
>>> report = PerformanceLogReportDecorator(report)
>>> report.prep_data()
prep_data ran in 709 microseconds
In a real world context, instead of printing to the console like we are here, you’d write to a log file or database for future analysis.
Decorator Conclusion
Our examples are pretty basic, but all things considered, the decorator pattern is a bit basic as well. Like most design patterns, the challenge isn’t so much in the implementation but in recognizing when you need to use them. Now that you have some knowledge and experience with delegation and decoration, you are better equipped to recognize when these techniques are necessary.