Chapter 12. Python Design Patterns II

In this chapter we will be introduced to several more design patterns. Once again, we'll cover the canonical examples as well as any common alternative implementations in Python. We'll be discussing:

  • The adapter pattern
  • The facade pattern
  • Lazy initialization and the flyweight pattern
  • The command pattern
  • The abstract factory pattern
  • The composition pattern

The adapter pattern

Unlike most of the patterns we reviewed in Chapter 8, Strings and Serialization, the adapter pattern is designed to interact with existing code. We would not design a brand new set of objects that implement the adapter pattern. Adapters are used to allow two pre-existing objects to work together, even if their interfaces are not compatible. Like the display adapters that allow VGA projectors to be plugged into HDMI ports, an adapter object sits between two different interfaces, translating between them on the fly. The adapter object's sole purpose is to perform this translation job. Adapting may entail a variety of tasks, such as converting arguments to a different format, rearranging the order of arguments, calling a differently named method, or supplying default arguments.

In structure, the adapter pattern is similar to a simplified decorator pattern. Decorators typically provide the same interface that they replace, whereas adapters map between two different interfaces. Here it is in UML form:

The adapter pattern

Here, Interface1 is expecting to call a method called make_action(some, arguments). We already have this perfect Interface2 class that does everything we want (and to avoid duplication, we don't want to rewrite it!), but it provides a method called different_action(other, arguments) instead. The Adapter class implements the make_action interface and maps the arguments to the existing interface.

The advantage here is that the code that maps from one interface to another is all in one place. The alternative would be really ugly; we'd have to perform the translation in multiple places whenever we need to access this code.

For example, imagine we have the following preexisting class, which takes a string date in the format "YYYY-MM-DD" and calculates a person's age on that day:

class AgeCalculator:
    def __init__(self, birthday):
        self.year, self.month, self.day = (
                int(x) for x in birthday.split('-'))

    def calculate_age(self, date):
        year, month, day = (
                int(x) for x in date.split('-'))
        age = year - self.year
        if (month,day) < (self.month,self.day):
            age -= 1
        return age

This is a pretty simple class that does what it's supposed to do. But we have to wonder what the programmer was thinking, using a specifically formatted string instead of using Python's incredibly useful built-in datetime library. As conscientious programmers who reuse code whenever possible, most of the programs we write will interact with datetime objects, not strings.

We have several options to address this scenario; we could rewrite the class to accept datetime objects, which would probably be more accurate anyway. But if this class had been provided by a third party and we don't know or can't change its internal structure, we need to try something else. We could use the class as it is, and whenever we want to calculate the age on a datetime.date object, we could call datetime.date.strftime('%Y-%m-%d') to convert it to the proper format. But that conversion would be happening in a lot of places, and worse, if we mistyped the %m as %M, it would give us the current minute instead of the entered month! Imagine if you wrote that in a dozen different places only to have to go back and change it when you realized your mistake. It's not maintainable code, and it breaks the DRY principle.

Instead, we can write an adapter that allows a normal date to be plugged into a normal AgeCalculator class:

import datetime
class DateAgeAdapter:
    def _str_date(self, date):
        return date.strftime("%Y-%m-%d")

    def __init__(self, birthday):
        birthday = self._str_date(birthday)
        self.calculator = AgeCalculator(birthday)

    def get_age(self, date):
        date = self._str_date(date)
        return self.calculator.calculate_age(date)

This adapter converts datetime.date and datetime.time (they have the same interface to strftime) into a string that our original AgeCalculator can use. Now we can use the original code with our new interface. I changed the method signature to get_age to demonstrate that the calling interface may also be looking for a different method name, not just a different type of argument.

Creating a class as an adapter is the usual way to implement this pattern, but, as usual, there are other ways to do it in Python. Inheritance and multiple inheritance can be used to add functionality to a class. For example, we could add an adapter on the date class so that it works with the original AgeCalculator class:

import datetime
class AgeableDate(datetime.date):
    def split(self, char):
        return self.year, self.month, self.day

It's code like this that makes one wonder if Python should even be legal. We have added a split method to our subclass that takes a single argument (which we ignore) and returns a tuple of year, month, and day. This works flawlessly with the original AgeCalculator class because the code calls strip on a specially formatted string, and strip, in that case, returns a tuple of year, month, and day. The AgeCalculator code only cares if strip exists and returns acceptable values; it doesn't care if we really passed in a string. It really works:

>>> bd = AgeableDate(1975, 6, 14)
>>> today = AgeableDate.today()
>>> today
AgeableDate(2015, 8, 4)
>>> a = AgeCalculator(bd)
>>> a.calculate_age(today)
40

It works but it's a stupid idea. In this particular instance, such an adapter would be hard to maintain. We'd soon forget why we needed to add a strip method to a date class. The method name is ambiguous. That can be the nature of adapters, but creating an adapter explicitly instead of using inheritance usually clarifies its purpose.

Instead of inheritance, we can sometimes also use monkey-patching to add a method to an existing class. It won't work with the datetime object, as it doesn't allow attributes to be added at runtime, but in normal classes, we can just add a new method that provides the adapted interface that is required by calling code. Alternatively, we could extend or monkey-patch the AgeCalculator itself to replace the calculate_age method with something more amenable to our needs.

Finally, it is often possible to use a function as an adapter; this doesn't obviously fit the actual design of the adapter pattern, but if we recall that functions are essentially objects with a __call__ method, it becomes an obvious adapter adaptation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset