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
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:
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.
The facade pattern is designed to provide a simple interface to a complex system of components. For complex tasks, we may need to interact with these objects directly, but there is often a "typical" usage for the system for which these complicated interactions aren't necessary. The facade pattern allows us to define a new object that encapsulates this typical usage of the system. Any time we want access to common functionality, we can use the single object's simplified interface. If another part of the project needs access to more complicated functionality, it is still able to interact with the system directly. The UML diagram for the facade pattern is really dependent on the subsystem, but in a cloudy way, it looks like this:
A facade is, in many ways, like an adapter. The primary difference is that the facade is trying to abstract a simpler interface out of a complex one, while the adapter is only trying to map one existing interface to another.
Let's write a simple facade for an e-mail application. The low-level library for sending e-mail in Python, as we saw in Chapter 7, Python Object-oriented Shortcuts, is quite complicated. The two libraries for receiving messages are even worse.
It would be nice to have a simple class that allows us to send a single e-mail, and list the e-mails currently in the inbox on an IMAP or POP3 connection. To keep our example short, we'll stick with IMAP and SMTP: two totally different subsystems that happen to deal with e-mail. Our facade performs only two tasks: sending an e-mail to a specific address, and checking the inbox on an IMAP connection. It makes some common assumptions about the connection, such as the host for both SMTP and IMAP is at the same address, that the username and password for both is the same, and that they use standard ports. This covers the case for many e-mail servers, but if a programmer needs more flexibility, they can always bypass the facade and access the two subsystems directly.
The class is initialized with the hostname of the e-mail server, a username, and a password to log in:
import smtplib import imaplib class EmailFacade: def __init__(self, host, username, password): self.host = host self.username = username self.password = password
The
send_email
method formats the e-mail address and message, and sends it using smtplib
. This isn't a complicated task, but it requires quite a bit of fiddling to massage the "natural" input parameters that are passed into the facade to the correct format to enable smtplib
to send the message:
def send_email(self, to_email, subject, message): if not "@" in self.username: from_email = "{0}@{1}".format( self.username, self.host) else: from_email = self.username message = ("From: {0}\r\n" "To: {1}\r\n" "Subject: {2}\r\n\r\n{3}").format( from_email, to_email, subject, message) smtp = smtplib.SMTP(self.host) smtp.login(self.username, self.password) smtp.sendmail(from_email, [to_email], message)
The if
statement at the beginning of the method is catching whether or not the username
is the entire "from" e-mail address or just the part on the left side of the @
symbol; different hosts treat the login details differently.
Finally, the code to get the messages currently in the inbox is a ruddy mess; the IMAP protocol is painfully over-engineered, and the imaplib
standard library is only a thin layer over the protocol:
def get_inbox(self): mailbox = imaplib.IMAP4(self.host) mailbox.login(bytes(self.username, 'utf8'), bytes(self.password, 'utf8')) mailbox.select() x, data = mailbox.search(None, 'ALL') messages = [] for num in data[0].split(): x, message = mailbox.fetch(num, '(RFC822)') messages.append(message[0][1]) return messages
Now, if we add all this together, we have a simple facade class that can send and receive messages in a fairly straightforward manner, much simpler than if we had to interact with these complex libraries directly.
Although it is rarely named in the Python community, the facade pattern is an integral part of the Python ecosystem. Because Python emphasizes language readability, both the language and its libraries tend to provide easy-to-comprehend interfaces to complicated tasks. For example, for
loops, list
comprehensions, and generators are all facades into a more complicated iterator protocol. The defaultdict
implementation is a facade that abstracts away annoying corner cases when a key doesn't exist in a dictionary. The third-party requests library is a powerful facade over less readable libraries for HTTP requests.
The flyweight pattern is a memory optimization pattern. Novice Python programmers tend to ignore memory optimization, assuming the built-in garbage collector will take care of them. This is often perfectly acceptable, but when developing larger applications with many related objects, paying attention to memory concerns can have a huge payoff.
The flyweight pattern basically ensures that objects that share a state can use the same memory for that shared state. It is often implemented only after a program has demonstrated memory problems. It may make sense to design an optimal configuration from the beginning in some situations, but bear in mind that premature optimization is the most effective way to create a program that is too complicated to maintain.
Let's have a look at the UML diagram for the flyweight pattern:
Each Flyweight has no specific state; any time it needs to perform an operation on SpecificState, that state needs to be passed into the Flyweight by the calling code. Traditionally, the factory that returns a flyweight is a separate object; its purpose is to return a flyweight for a given key identifying that flyweight. It works like the singleton pattern we discussed in Chapter 10, Python Design Patterns I; if the flyweight exists, we return it; otherwise, we create a new one. In many languages, the factory is implemented, not as a separate object, but as a static method on the Flyweight
class itself.
Think of an inventory system for car sales. Each individual car has a specific serial number and is a specific color. But most of the details about that car are the same for all cars of a particular model. For example, the Honda Fit DX model is a bare-bones car with few features. The LX model has A/C, tilt, cruise, and power windows and locks. The Sport model has fancy wheels, a USB charger, and a spoiler. Without the flyweight pattern, each individual car object would have to store a long list of which features it did and did not have. Considering the number of cars Honda sells in a year, this would add up to a huge amount of wasted memory. Using the flyweight pattern, we can instead have shared objects for the list of features associated with a model, and then simply reference that model, along with a serial number and color, for individual vehicles. In Python, the flyweight factory is often implemented using that funky __new__
constructor, similar to what we did with the singleton pattern. Unlike singleton, which only needs to return one instance of the class, we need to be able to return different instances depending on the keys. We could store the items in a dictionary and look them up based on the key. This solution is problematic, however, because the item will remain in memory as long as it is in the dictionary. If we sold out of LX model Fits, the Fit flyweight is no longer necessary, yet it will still be in the dictionary. We could, of course, clean this up whenever we sell a car, but isn't that what a garbage collector is for?
We can solve this by taking advantage of Python's weakref
module. This module provides a WeakValueDictionary
object, which basically allows us to store items in a dictionary without the garbage collector caring about them. If a value is in a weak referenced dictionary and there are no other references to that object stored anywhere in the application (that is, we sold out of LX models), the garbage collector will eventually clean up for us.
Let's build the factory for our car flyweights first:
import weakref class CarModel: _models = weakref.WeakValueDictionary() def __new__(cls, model_name, *args, **kwargs): model = cls._models.get(model_name) if not model: model = super().__new__(cls) cls._models[model_name] = model return model
Basically, whenever we construct a new flyweight with a given name, we first look up that name in the weak referenced dictionary; if it exists, we return that model; if not, we create a new one. Either way, we know the __init__
method on the flyweight will be called every time, regardless of whether it is a new or existing object. Our __init__
method can therefore look like this:
def __init__(self, model_name, air=False, tilt=False,
cruise_control=False, power_locks=False,
alloy_wheels=False, usb_charger=False):
if not hasattr(self, "initted"):
self.model_name = model_name
self.air = air
self.tilt = tilt
self.cruise_control = cruise_control
self.power_locks = power_locks
self.alloy_wheels = alloy_wheels
self.usb_charger = usb_charger
self.initted=True
The if
statement ensures that we only initialize the object the first time __init__
is called. This means we can call the factory later with just the model name and get the same flyweight object back. However, because the flyweight will be garbage-collected if no external references to it exist, we have to be careful not to accidentally create a new flyweight with null values.
Let's add a method to our flyweight that hypothetically looks up a serial number on a specific model of vehicle, and determines if it has been involved in any accidents. This method needs access to the car's serial number, which varies from car to car; it cannot be stored with the flyweight. Therefore, this data must be passed into the method by the calling code:
def check_serial(self, serial_number):
print("Sorry, we are unable to check "
"the serial number {0} on the {1} "
"at this time".format(
serial_number, self.model_name))
We can define a class that stores the additional information, as well as a reference to the flyweight:
class Car: def __init__(self, model, color, serial): self.model = model self.color = color self.serial = serial def check_serial(self): return self.model.check_serial(self.serial)
We can also keep track of the available models as well as the individual cars on the lot:
>>> dx = CarModel("FIT DX") >>> lx = CarModel("FIT LX", air=True, cruise_control=True, ... power_locks=True, tilt=True) >>> car1 = Car(dx, "blue", "12345") >>> car2 = Car(dx, "black", "12346") >>> car3 = Car(lx, "red", "12347")
Now, let's demonstrate the weak referencing at work:
>>> id(lx) 3071620300 >>> del lx >>> del car3 >>> import gc >>> gc.collect() 0 >>> lx = CarModel("FIT LX", air=True, cruise_control=True, ... power_locks=True, tilt=True) >>> id(lx) 3071576140 >>> lx = CarModel("FIT LX") >>> id(lx) 3071576140 >>> lx.air True
The id
function tells us the unique identifier for an object. When we call it a second time, after deleting all references to the LX model and forcing garbage collection, we see that the ID has changed. The value in the CarModel __new__
factory dictionary was deleted and a fresh one created. If we then try to construct a second CarModel
instance, however, it returns the same object (the IDs are the same), and, even though we did not supply any arguments in the second call, the air
variable is still set to True
. This means the object was not initialized the second time, just as we designed.
Obviously, using the flyweight pattern can be more complicated than just storing features on a single car class. When should we choose to use it? The flyweight pattern is designed for conserving memory; if we have hundreds of thousands of similar objects, combining similar properties into a flyweight can have an enormous impact on memory consumption. It is common for programming solutions that optimize CPU, memory, or disk space result in more complicated code than their unoptimized brethren. It is therefore important to weigh up the tradeoffs when deciding between code maintainability and optimization. When choosing optimization, try to use patterns such as flyweight to ensure that the complexity introduced by optimization is confined to a single (well documented) section of the code.
The command pattern adds a level of abstraction between actions that must be done, and the object that invokes those actions, normally at a later time. In the command pattern, client code creates a Command
object that can be executed at a later date. This object knows about a receiver object that manages its own internal state when the command is executed on it. The Command
object implements a specific interface (typically it has an execute
or do_action
method, and also keeps track of any arguments required to perform the action. Finally, one or more Invoker
objects execute the command at the correct time.
Here's the UML diagram:
A common example of the command pattern is actions on a graphical window. Often, an action can be invoked by a menu item on the menu bar, a keyboard shortcut, a toolbar icon, or a context menu. These are all examples of Invoker
objects. The actions that actually occur, such as Exit
, Save
, or Copy
, are implementations of CommandInterface
. A GUI window to receive exit, a document to receive save, and ClipboardManager
to receive copy commands, are all examples of possible Receivers
.
Let's implement a simple command pattern that provides commands for Save
and Exit
actions. We'll start with some modest receiver classes:
import sys class Window: def exit(self): sys.exit(0) class Document: def __init__(self, filename): self.filename = filename self.contents = "This file cannot be modified" def save(self): with open(self.filename, 'w') as file: file.write(self.contents)
These mock classes model objects that would likely be doing a lot more in a working environment. The window would need to handle mouse movement and keyboard events, and the document would need to handle character insertion, deletion, and selection. But for our example these two classes will do what we need.
Now let's define some invoker classes. These will model toolbar, menu, and keyboard events that can happen; again, they aren't actually hooked up to anything, but we can see how they are decoupled from the command, receiver, and client code:
class ToolbarButton: def __init__(self, name, iconname): self.name = name self.iconname = iconname def click(self): self.command.execute() class MenuItem: def __init__(self, menu_name, menuitem_name): self.menu = menu_name self.item = menuitem_name def click(self): self.command.execute() class KeyboardShortcut: def __init__(self, key, modifier): self.key = key self.modifier = modifier def keypress(self): self.command.execute()
Notice how the various action methods each call the execute
method on their respective commands? This code doesn't show the command
attribute being set on each object. They could be passed into the __init__
function, but because they may be changed (for example, with a customizable keybinding editor), it makes more sense to set the attributes on the objects afterwards.
Now, let's hook up the commands themselves:
class SaveCommand: def __init__(self, document): self.document = document def execute(self): self.document.save() class ExitCommand: def __init__(self, window): self.window = window def execute(self): self.window.exit()
These commands are straightforward; they demonstrate the basic pattern, but it is important to note that we can store state and other information with the command if necessary. For example, if we had a command to insert a character, we could maintain state for the character currently being inserted.
Now all we have to do is hook up some client and test code to make the commands work. For basic testing, we can just include this at the end of the script:
window = Window() document = Document("a_document.txt") save = SaveCommand(document) exit = ExitCommand(window) save_button = ToolbarButton('save', 'save.png') save_button.command = save save_keystroke = KeyboardShortcut("s", "ctrl") save_keystroke.command = save exit_menu = MenuItem("File", "Exit") exit_menu.command = exit
First we create two receivers and two commands. Then we create several of the available invokers and set the correct command on each of them. To test, we can use python3 -i filename.py
and run code like exit_menu.click()
, which will end the program, or save_keystroke.keystroke()
, which will save the fake file.
Unfortunately, the preceding examples do not feel terribly Pythonic. They have a lot of "boilerplate code" (code that does not accomplish anything, but only provides structure to the pattern), and the Command
classes are all eerily similar to each other. Perhaps we could create a generic command object that takes a function as a callback?
In fact, why bother? Can we just use a function or method object for each command? Instead of an object with an execute()
method, we can write a function and use that as the command directly. This is a common paradigm for the command pattern in Python:
import sys
class Window:
def exit(self):
sys.exit(0)
class MenuItem:
def click(self):
self.command()
window = Window()
menu_item = MenuItem()
menu_item.command = window.exit
Now that looks a lot more like Python. At first glance, it looks like we've removed the command pattern altogether, and we've tightly connected the menu_item
and Window
classes. But if we look closer, we find there is no tight coupling at all. Any callable can be set up as the command on the MenuItem
, just as before. And the Window.exit
method can be attached to any invoker. Most of the flexibility of the command pattern has been maintained. We have sacrificed complete decoupling for readability, but this code is, in my opinion, and that of many Python programmers, more maintainable than the fully abstracted version.
Of course, since we can add a __call__
method to any object, we aren't restricted to functions. The previous example is a useful shortcut when the method being called doesn't have to maintain state, but in more advanced usage, we can use this code as well:
class Document: def __init__(self, filename): self.filename = filename self.contents = "This file cannot be modified" def save(self): with open(self.filename, 'w') as file: file.write(self.contents) class KeyboardShortcut: def keypress(self): self.command() class SaveCommand: def __init__(self, document): self.document = document def __call__(self): self.document.save() document = Document("a_file.txt") shortcut = KeyboardShortcut() save_command = SaveCommand(document) shortcut.command = save_command
Here we have something that looks like the first command pattern, but a bit more idiomatic. As you can see, making the invoker call a callable instead of a command object with an execute method has not restricted us in any way. In fact, it's given us more flexibility. We can link to functions directly when that works, yet we can build a complete callable command object when the situation calls for it.
The command pattern is often extended to support undoable commands. For example, a text program may wrap each insertion in a separate command with not only an execute
method, but also an undo
method that will delete that insertion. A graphics program may wrap each drawing action (rectangle, line, freehand pixels, and so on) in a command that has an undo
method that resets the pixels to their original state. In such cases, the decoupling of the command pattern is much more obviously useful, because each action has to maintain enough of its state to undo that action at a later date.
The abstract factory pattern is normally used when we have multiple possible implementations of a system that depend on some configuration or platform issue. The calling code requests an object from the abstract factory, not knowing exactly what class of object will be returned. The underlying implementation returned may depend on a variety of factors, such as current locale, operating system, or local configuration.
Common examples of the abstract factory pattern include code for operating-system independent toolkits, database backends, and country-specific formatters or calculators. An operating-system-independent GUI toolkit might use an abstract factory pattern that returns a set of WinForm widgets under Windows, Cocoa widgets under Mac, GTK widgets under Gnome, and QT widgets under KDE. Django provides an abstract factory that returns a set of object relational classes for interacting with a specific database backend (MySQL, PostgreSQL, SQLite, and others) depending on a configuration setting for the current site. If the application needs to be deployed in multiple places, each one can use a different database backend by changing only one configuration variable. Different countries have different systems for calculating taxes, subtotals, and totals on retail merchandise; an abstract factory can return a particular tax calculation object.
The UML class diagram for an abstract factory pattern is hard to understand without a specific example, so let's turn things around and create a concrete example first. We'll create a set of formatters that depend on a specific locale and help us format dates and currencies. There will be an abstract factory class that picks the specific factory, as well as a couple example concrete factories, one for France and one for the USA. Each of these will create formatter objects for dates and times, which can be queried to format a specific value. Here's the diagram:
Comparing that image to the earlier simpler text shows that a picture is not always worth a thousand words, especially considering we haven't even allowed for factory selection code here.
Of course, in Python, we don't have to implement any interface classes, so we can discard DateFormatter
, CurrencyFormatter
, and FormatterFactory
. The formatting classes themselves are pretty straightforward, if verbose:
class FranceDateFormatter: def format_date(self, y, m, d): y, m, d = (str(x) for x in (y,m,d)) y = '20' + y if len(y) == 2 else y m = '0' + m if len(m) == 1 else m d = '0' + d if len(d) == 1 else d return("{0}/{1}/{2}".format(d,m,y)) class USADateFormatter: def format_date(self, y, m, d): y, m, d = (str(x) for x in (y,m,d)) y = '20' + y if len(y) == 2 else y m = '0' + m if len(m) == 1 else m d = '0' + d if len(d) == 1 else d return("{0}-{1}-{2}".format(m,d,y)) class FranceCurrencyFormatter: def format_currency(self, base, cents): base, cents = (str(x) for x in (base, cents)) if len(cents) == 0: cents = '00' elif len(cents) == 1: cents = '0' + cents digits = [] for i,c in enumerate(reversed(base)): if i and not i % 3: digits.append(' ') digits.append(c) base = ''.join(reversed(digits)) return "{0}€{1}".format(base, cents) class USACurrencyFormatter: def format_currency(self, base, cents): base, cents = (str(x) for x in (base, cents)) if len(cents) == 0: cents = '00' elif len(cents) == 1: cents = '0' + cents digits = [] for i,c in enumerate(reversed(base)): if i and not i % 3: digits.append(',') digits.append(c) base = ''.join(reversed(digits)) return "${0}.{1}".format(base, cents)
These classes use some basic string manipulation to try to turn a variety of possible inputs (integers, strings of different lengths, and others) into the following formats:
USA |
France | |
---|---|---|
Date |
mm-dd-yyyy |
dd/mm/yyyy |
Currency |
$14,500.50 |
14 500€50 |
There could obviously be more validation on the input in this code, but let's keep it simple and dumb for this example.
Now that we have the formatters set up, we just need to create the formatter factories:
class USAFormatterFactory: def create_date_formatter(self): return USADateFormatter() def create_currency_formatter(self): return USACurrencyFormatter() class FranceFormatterFactory: def create_date_formatter(self): return FranceDateFormatter() def create_currency_formatter(self): return FranceCurrencyFormatter()
Now we set up the code that picks the appropriate formatter. Since this is the kind of thing that only needs to be set up once, we could make it a singleton—except singletons aren't very useful in Python. Let's just make the current formatter a module-level variable instead:
country_code = "US" factory_map = { "US": USAFormatterFactory, "FR": FranceFormatterFactory} formatter_factory = factory_map.get(country_code)()
In this example, we hardcode the current country code; in practice, it would likely introspect the locale, the operating system, or a configuration file to choose the code. This example uses a dictionary to associate the country codes with factory classes. Then we grab the correct class from the dictionary and instantiate it.
It is easy to see what needs to be done when we want to add support for more countries: create the new formatter classes and the abstract factory itself. Bear in mind that Formatter
classes might be reused; for example, Canada formats its currency the same way as the USA, but its date format is more sensible than its Southern neighbor.
Abstract factories often return a singleton object, but this is not required; in our code, it's returning a new instance of each formatter every time it's called. There's no reason the formatters couldn't be stored as instance variables and the same instance returned for each factory.
Looking back at these examples, we see that, once again, there appears to be a lot of boilerplate code for factories that just doesn't feel necessary in Python. Often, the requirements that might call for an abstract factory can be more easily fulfilled by using a separate module for each factory type (for example: the USA and France), and then ensuring that the correct module is being accessed in a factory module. The package structure for such modules might look like this:
localize/ __init__.py backends/ __init__.py USA.py France.py …
The trick is that __init__.py
in the localize
package can contain logic that redirects all requests to the correct backend. There is a variety of ways this could be done.
If we know that the backend is never going to change dynamically (that is, without a restart), we can just put some if
statements in __init__.py
that check the current country code, and use the usually unacceptable from .backends.USA import *
syntax to import all variables from the appropriate backend. Or, we could import each of the backends and set a current_backend
variable to point at a specific module:
from .backends import USA, France if country_code == "US": current_backend = USA
Depending on which solution we choose, our client code would have to call either localize.format_date
or localize.current_backend.format_date
to get a date formatted in the current country's locale. The end result is much more Pythonic than the original abstract factory pattern, and, in typical usage, just as flexible.
The composite pattern allows complex tree-like structures to be built from simple components. These components, called composite objects, are able to behave sort of like a container and sort of like a variable depending on whether they have child components. Composite objects are container objects, where the content may actually be another composite object.
Traditionally, each component in a composite object must be either a leaf node (that cannot contain other objects) or a composite node. The key is that both composite and leaf nodes can have the same interface. The UML diagram is very simple:
This simple pattern, however, allows us to create complex arrangements of elements, all of which satisfy the interface of the component object. Here is a concrete instance of such a complicated arrangement:
The composite pattern is commonly useful in file/folder-like trees. Regardless of whether a node in the tree is a normal file or a folder, it is still subject to operations such as moving, copying, or deleting the node. We can create a component interface that supports these operations, and then use a composite object to represent folders, and leaf nodes to represent normal files.
Of course, in Python, once again, we can take advantage of duck typing to implicitly provide the interface, so we only need to write two classes. Let's define these interfaces first:
class Folder: def __init__(self, name): self.name = name self.children = {} def add_child(self, child): pass def move(self, new_path): pass def copy(self, new_path): pass def delete(self): pass class File: def __init__(self, name, contents): self.name = name self.contents = contents def move(self, new_path): pass def copy(self, new_path): pass def delete(self): pass
For each folder (composite) object, we maintain a dictionary of children. Often, a list is sufficient, but in this case, a dictionary will be useful for looking up children by name. Our paths will be specified as node names separated by the /
character, similar to paths in a Unix shell.
Thinking about the methods involved, we can see that moving or deleting a node behaves in a similar way, regardless of whether or not it is a file or folder node. Copying, however, has to do a recursive copy for folder nodes, while copying a file node is a trivial operation.
To take advantage of the similar operations, we can extract some of the common methods into a parent class. Let's take that discarded Component
interface and change it to a base class:
class Component: def __init__(self, name): self.name = name def move(self, new_path): new_folder =get_path(new_path) del self.parent.children[self.name] new_folder.children[self.name] = self self.parent = new_folder def delete(self): del self.parent.children[self.name] class Folder(Component): def __init__(self, name): super().__init__(name) self.children = {} def add_child(self, child): pass def copy(self, new_path): pass class File(Component): def __init__(self, name, contents): super().__init__(name) self.contents = contents def copy(self, new_path): pass root = Folder('') def get_path(path): names = path.split('/')[1:] node = root for name in names: node = node.children[name] return node
We've created the move
and delete
methods on the Component
class. Both of them access a mysterious parent
variable that we haven't set yet. The move
method uses a module-level get_path
function that finds a node from a predefined root node, given a path. All files will be added to this root node or a child of that node. For the move
method, the target should be a currently existing folder, or we'll get an error. As with many of the examples in technical books, error handling is woefully absent, to help focus on the principles under consideration.
Let's set up that mysterious parent
variable first; this happens, in the folder's add_child
method:
def add_child(self, child): child.parent = self self.children[child.name] = child
Well, that was easy enough. Let's see if our composite file hierarchy is working properly:
$ python3 -i 1261_09_18_add_child.py >>> folder1 = Folder('folder1') >>> folder2 = Folder('folder2') >>> root.add_child(folder1) >>> root.add_child(folder2) >>> folder11 = Folder('folder11') >>> folder1.add_child(folder11) >>> file111 = File('file111', 'contents') >>> folder11.add_child(file111) >>> file21 = File('file21', 'other contents') >>> folder2.add_child(file21) >>> folder2.children {'file21': <__main__.File object at 0xb7220a4c>} >>> folder2.move('/folder1/folder11') >>> folder11.children {'folder2': <__main__.Folder object at 0xb722080c>, 'file111': <__main__.File object at 0xb72209ec>} >>> file21.move('/folder1') >>> folder1.children {'file21': <__main__.File object at 0xb7220a4c>, 'folder11': <__main__.Folder object at 0xb722084c>}
Yes, we can create folders, add folders to other folders, add files to folders, and move them around! What more could we ask for in a file hierarchy?
Well, we could ask for copying to be implemented, but to conserve trees, let's leave that as an exercise.
The composite pattern is extremely useful for a variety of tree-like structures, including GUI widget hierarchies, file hierarchies, tree sets, graphs, and HTML DOM. It can be a useful pattern in Python when implemented according to the traditional implementation, as the example earlier demonstrated. Sometimes, if only a shallow tree is being created, we can get away with a list of lists or a dictionary of dictionaries, and do not need to implement custom component, leaf, and composite classes. Other times, we can get away with implementing only one composite class, and treating leaf and composite objects as a single class. Alternatively, Python's duck typing can make it easy to add other objects to a composite hierarchy, as long as they have the correct interface.
Before diving into exercises for each design pattern, take a moment to implement the copy
method for the File
and Folder
objects in the previous section. The File
method should be quite trivial; just create a new node with the same name and contents, and add it to the new parent folder. The copy
method on Folder
is quite a bit more complicated, as you first have to duplicate the folder, and then recursively copy each of its children to the new location. You can call the copy()
method on the children indiscriminately, regardless of whether each is a file or a folder object. This will drive home just how powerful the composite pattern can be.
Now, as with the previous chapter, look at the patterns we've discussed, and consider ideal places where you might implement them. You may want to apply the adapter pattern to existing code, as it is usually applicable when interfacing with existing libraries, rather than new code. How can you use an adapter to force two interfaces to interact with each other correctly?
Can you think of a system complex enough to justify using the facade pattern? Consider how facades are used in real-life situations, such as the driver-facing interface of a car, or the control panel in a factory. It is similar in software, except the users of the facade interface are other programmers, rather than people trained to use them. Are there complex systems in your latest project that could benefit from the facade pattern?
It's possible you don't have any huge, memory-consuming code that would benefit from the flyweight pattern, but can you think of situations where it might be useful? Anywhere that large amounts of overlapping data need to be processed, a flyweight is waiting to be used. Would it be useful in the banking industry? In web applications? At what point does the flyweight pattern make sense? When is it overkill?
What about the command pattern? Can you think of any common (or better yet, uncommon) examples of places where the decoupling of action from invocation would be useful? Look at the programs you use on a daily basis, and imagine how they are implemented internally. It's likely that many of them use the command pattern for one purpose or another.
The abstract factory pattern, or the somewhat more Pythonic derivatives we discussed, can be very useful for creating one-touch-configurable systems. Can you think of places where such systems are useful?
Finally, consider the composite pattern. There are tree-like structures all around us in programming; some of them, like our file hierarchy example, are blatant; others are fairly subtle. What situations might arise where the composite pattern would be useful? Can you think of places where you can use it in your own code? What if you adapted the pattern slightly; for example, to contain different types of leaf or composite nodes for different types of objects?
In this chapter, we went into detail on several more design patterns, covering their canonical descriptions as well as alternatives for implementing them in Python, which is often more flexible and versatile than traditional object-oriented languages. The adapter pattern is useful for matching interfaces, while the facade pattern is suited to simplifying them. Flyweight is a complicated pattern and only useful if memory optimization is required. In Python, the command pattern is often more aptly implemented using first class functions as callbacks. Abstract factories allow run-time separation of implementations depending on configuration or system information. The composite pattern is used universally for tree-like structures.
In the next chapter, we'll discuss how important it is to test Python programs, and how to do it.