1.2. Builder Pattern
The Builder Pattern is similar to the Abstract Factory Pattern in that both patterns are designed for creating complex objects that are composed of other objects. What makes the Builder Pattern distinct is that the builder not only provides the methods for building a complex object, it also holds the representation of the entire complex object itself.
This pattern allows the same kind of compositionality as the Abstract Factory Pattern (i.e., complex objects are built out of one or more simpler objects), but is particularly suited to cases where the representation of the complex object needs to be kept separate from the composition algorithms.
We will show an example of the Builder Pattern in a program that can produce forms—either web forms using HTML, or GUI forms using Python and Tkinter. Both forms work visually and support text entry; however, their buttons are non-functional.* The forms are shown in Figure 1.2; the source code is in formbuilder.py.
Figure 1.2 The HTML and Tkinter forms on Windows
Let’s begin by looking at the code needed to build each form, starting with the top-level calls.
htmlForm = create_login_form(HtmlFormBuilder()) with open(htmlFilename,"w"
, encoding="utf-8"
) as file: file.write(htmlForm) tkForm = create_login_form(TkFormBuilder()) with open(tkFilename,"w"
, encoding="utf-8"
) as file: file.write(tkForm)
Here, we have created each form and written it out to an appropriate file. In both cases we use the same form creation function (create_login_form()), parameterized by an appropriate builder object.
def create_login_form(builder): builder.add_title("Login"
) builder.add_label("Username"
,0
,0
, target="username"
) builder.add_entry("username"
,0
,1
) builder.add_label("Password"
,1
,0
, target="password"
) builder.add_entry("password"
,1
,1
, kind="password"
) builder.add_button("Login"
,2
,0
) builder.add_button("Cancel"
,2
,1
) return builder.form()
This function can create any arbitrary HTML or Tkinter form—or any other kind of form for which we have a suitable builder. The builder.add_title() method is used to give the form a title. All the other methods are used to add a widget to the form at a given row and column position.
Both HtmlFormBuilder and TkFormBuilder inherit from an abstract base class, AbstractFormBuilder.
class AbstractFormBuilder(metaclass=abc.ABCMeta): @abc.abstractmethod def add_title(self, title): self.title = title @abc.abstractmethod def form(self): pass @abc.abstractmethod def add_label(self, text, row, column, **kwargs): pass ...
Any class that inherits this class must implement all the abstract methods. We have elided the add_entry() and add_button() abstract methods because, apart from their names, they are identical to the add_label() method. Incidentally, we are required to make the AbstractFormBuilder have a metaclass of abc.ABCMeta to allow it to use the abc module’s @abstractmethod decorator. (See §2.4, 48 for more on decorators.)
Giving a class a metaclass of abc.ABCMeta means that the class cannot be instantiated, and so must be used as an abstract base class. This makes particular sense for code being ported from, say, C++ or Java, but does incur a tiny runtime overhead. However, many Python programmers use a more laid back approach: they don’t use a metaclass at all, and simply document that the class should be used as an abstract base class.
class HtmlFormBuilder(AbstractFormBuilder): def __init__(self): self.title ="HtmlFormBuilder"
self.items = {} def add_title(self, title): super().add_title(escape(title)) def add_label(self, text, row, column, **kwargs): self.items[(row, column)] = ('<td><label for="{}">{}:</label></td>'
.format(kwargs["target"
], escape(text))) def add_entry(self, variable, row, column, **kwargs): html ="""<td><input name="{}" type="{}" /></td>"""
.format( variable, kwargs.get("kind"
,"text"
)) self.items[(row, column)] = html ...
Here is the start of the HtmlFormBuilder class. We provide a default title in case the form is built without one. All the form’s widgets are stored in an items dictionary that uses row, column 2-tuple keys, and the widgets’ HTML as values.
We must reimplement the add_title() method since it is abstract, but since the abstract version has an implementation we can simply call that implementation. In this case we must preprocess the title using the html.escape() function (or the xml.sax.saxutil.escape() function in Python 3.2 or earlier).
The add_button() method (not shown) is structurally similar to the other add_...() methods.
def form(self): html = ["<!doctype html>\n<html><head><title>{}</title></head>"
"<body>"
.format(self.title),'<form><table border="0">'
] thisRow = None for key, value in sorted(self.items.items()): row, column = key if thisRow is None: html.append(" <tr>"
) elif thisRow != row: html.append(" </tr>\n <tr>"
) thisRow = row html.append(" "
+ value) html.append(" </tr>\n</table></form></body></html>"
) return"\n"
.join(html)
The HtmlFormBuilder.form() method creates an HTML page consisting of a <form>, inside of which is a <table>, inside of which are rows and columns of widgets. Once all the pieces have been added to the html list, the list is returned as a single string (with newline separators to make it more human-readable).
class TkFormBuilder(AbstractFormBuilder): def __init__(self): self.title ="TkFormBuilder"
self.statements = [] def add_title(self, title): super().add_title(title) def add_label(self, text, row, column, **kwargs): name = self._canonicalize(text) create ="""self.{}Label = ttk.Label(self, text="{}:")"""
.format( name, text) layout ="""self.{}Label.grid(row={}, column={}, sticky=tk.W, \
padx="0.75m", pady="0.75m")"""
.format(name, row, column) self.statements.extend((create, layout)) ... def form(self): return TkFormBuilder.TEMPLATE.format(title=self.title, name=self._canonicalize(self.title, False), statements="\n "
.join(self.statements))
This is an extract from the TkFormBuilder class. We store the form’s widgets as a list of statements (i.e., as strings of Python code), two statements per widget.
The add_label() method’s structure is also used by the add_entry() and add_button() methods (neither of which is shown). These methods begin by getting a canonicalized name for the widget and then make two strings: create, which has the code to create the widget and layout, which has the code to lay out the widget in the form. Finally, the methods add the two strings to the list of statements.
The form() method is very simple: it just returns a TEMPLATE string parameterized by the title and the statements.
TEMPLATE ="""#!/usr/bin/env python3
import tkinter as tk
import tkinter.ttk as ttk
class {name}Form(tk.Toplevel):
def __init__(self, master):
super().__init__(master)
self.withdraw() # hide until ready to show
self.title("{title}")
{statements}
self.bind("<Escape>", lambda *args: self.destroy())
self.deiconify() # show when widgets are created and laid out
if self.winfo_viewable():
self.transient(master)
self.wait_visibility()
self.grab_set()
self.wait_window(self)
if __name__ == "__main__":
application = tk.Tk()
window = {name}Form(application)
application.protocol("WM_DELETE_WINDOW", application.quit)
application.mainloop()
"""
The form is given a unique class name based on the title (e.g., LoginForm, ; ). The window title is set early on (e.g., “Login”, ), and this is followed by all the statements to create and lay out the form’s widgets ().
The Python code produced by using the template can be run stand-alone thanks to the if __name__ ... block at the end.
def _canonicalize(self, text, startLower=True): text = re.sub(r"\W+"
,""
, text) if text[0
].isdigit(): return"_"
+ text return text if not startLower else text[0
].lower() + text[1
:]
The code for the _canonicalize() method is included for completeness. Incidentally, although it looks as if we create a fresh regex every time the function is called, in practice Python maintains a fairly large internal cache of compiled regexes, so for the second and subsequent calls, Python just looks up the regex rather than recompiling it.*