Chapter 9. Python Design Patterns II

This chapter carries on from the previous chapter by introducing 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

Adapter pattern

Unlike most of the patterns we reviewed in Chapter 8, 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 keyboard adapters that allow USB keyboards to be plugged into PS/2 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 translating job; translating 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:

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 to translate it directly in multiple places whenever we need to access this code.

For example, imagine we have the following pre-existing 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. Most programs we write are going to be interacting 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 has been provided by a third party and we don't know what its internals are, or we simply aren't allowed to change them, 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 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.

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

	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 as a pattern, but, as usual, there are other ways to do it. 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:

	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. All we've done here is add a split method that takes a single argument (which we ignore) and returns a tuple of year, month, day. This works flawlessly with our AgeCalculator, because that code calls strip on a specially formatted string, and strip, in that case returns a tuple of year, month, 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(2010, 2, 23)
>>> a = AgeCalculator(bd)
>>> a.calculate_age(today)
34

In this particular instance, such an adapter would be hard to maintain, as we'll soon forget why we needed to add a strip method to a date class. The method name is quite ambiguous. That can be the nature of adapters, but if we had created an adapter explicitly instead of using inheritance, it's more obvious what its purpose is.

Instead of inheritance, you can sometimes also use monkey-patching to add a method to an existing class. It won't work with the datetime object, as it won'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.

It can also be possible to use a function as an adapter; this doesn't really fit the adapter pattern properly, but often, you can simply pass data into a function and return it in the proper format for entry into another interface.

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

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