Delegate and Decorate in Python: Part 2 – The Decorator Pattern

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.