본문 바로가기
Practical Python Design Patterns

Builder Pattern

by 자동매매 2023. 3. 28.

CHAPTER 5 Builder Pattern

Can he fix it? Yes, he can!

Bob the Builder eme Song

If you work in software for any amount of time, you will have to deal with getting input from the user anything from having a player enter their character name in a game to adding data to the cells on a spreadsheet. Forms are often the heart of an application. Actually, most applications are just a special case of the input, transform, and feedback flow of information. There are many input widgets and interfaces, but the most common remain the following:

Text boxes

Dropdown and Select lists Checkboxes

Radio buttons

File upload fields

Buttons

These can be mixed and matched in many different ways to take different types of data as input from the user. What we are interested in for this chapter is how we can write a script that could ease the effort of generating these forms. For our use case, we will be generating HTML webforms, but the same technology can be used, with a couple of tweaks, to generate mobile app interfaces, JSON or XML representations of the form, or whatever else you can dream up. Let s begin by writing a simple function that will generate a little form for us.

75

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_5
CHAPTER 5 BUILDER PATTERN

basic_form_generator.py

def generate_webform(field_list):

generated_fields = "\n".join(

map(

lambda x: ’{0}:

’.format(x), field_list

)

)

return "

{fields}
".format(fields=generated_fields)

if __name__ == "__main__":

fields = ["name", "age", "email", "telephone"] print(generate_webform(fields))

In this simplistic example, the code assumes that the list of fields contains only text fields. The field name is contained in the list as a string. The strings in the list are also the labels used in the form that is returned. For every element in the list, a single field is added to the webform. The webform is then returned as a string containing the HTML code for the generated webform.

If we go one step further, we could have a script take the generated response and build a regular HTML file from it, one you could open in your web browser.

html_ form_ generator.py

def generate_webform(field_list):

generated_fields = "\n".join(

map(

lambda x: ’{0}:

’.format(x), field_list

)

)

return "

{fields}
".format(fields=generated_fields)

def build_html_form(fields):

with open("form_file.html", w) as f:

f.write(

"{}

".format(generate_webform(fields)) )

76
CHAPTER 5 BUILDER PATTERN

if __name__ == "__main__":

fields = ["name", "age", "email", "telephone"] build_html_form(fields)

As I mentioned in the beginning of this chapter, webforms (and forms in general) could have many more field types than simple text fields. We could add more field types to the form using named parameters. Look at the following code, where we will add checkboxes.

html_ form_ generator.py

def generate_webform(text_field_list=[], checkbox_field_list=[]):

generated_fields = "\n".join(

map(

lambda x: ’{0}:

’.format(x), text_field_list

)

)

generated_fields += "\n".join(

map(

lambda x: ’

checkbox_field_list

)

)

return "

{fields}
".format(fields=generated_fields)

def build_html_form(text_field_list=[], checkbox_field_list=[]): with open("form_file.html", ’w’) as f:

f.write(

" {}

".format(

generate_webform(

text_field_list=text_field_list, checkbox_field_list=checkbox_field_list

)

)

)

77
CHAPTER 5 BUILDER PATTERN

if __name__ == "__main__":

text_fields = ["name", "age", "email", "telephone"]

checkbox_fields = ["awesome", "bad"]

build_html_form(text_field_list=text_fields, checkbox_field_list=checkbox_

fields)

There are clear issues with this approach, the first of which is the fact that we cannot deal with fields that have different defaults or options or in fact any information beyond a simple label or field name. We cannot even cater for a difference between the field name and the label used in the form. We are now going to extend the functionality of the form- generator function so we can cater for a large number of field types, each with its own settings. We also have the issue that there is no way to interleave different types of fields. To show you how this would work, we are ripping out the named parameters and replacing them with a list of dictionaries. The function will look in each dictionary in the list and then use the contained information to generate the field as defined in the dictionary.

html_dictionary_ form_ generator.py

def generate_webform(field_dict_list):

generated_field_list = []

for field_dict in field_dict_list:

if field_dict["type"] == "text_field":

generated_field_list.append(

’{0}:

’.format(

field_dict["label"],

field_dict["name"]

)

)

elif field_dict["type"] == "checkbox": generated_field_list.append(

field_dict["id"],

field_dict["value"],

field_dict["label"]

)

)

78
CHAPTER 5 BUILDER PATTERN

generated_fields = "\n".join(generated_field_list)

return "

{fields}
".format(fields=generated_fields)

def build_html_form(field_list):

with open("form_file.html", ’w’) as f:

f.write( " {}

".format(

generate_webform(field_list)

)

)

if __name__ == "__main__":

field_list = [

{

"type": "text_field",

"label": "Best text you have ever written", "name": "best_text"

},

{

"type": "checkbox",

"id": "check_it",

"value": "1",

"label": "Check for one",

},

{

"type": "text_field",

"label": "Another Text field",

"name": "text_field2"

}

] build_html_form(field_list)

The dictionary contains a type field, which is used to select which type of field to add. Then, you have some optional elements as part of the dictionary, like label, name, and options (in the case of a select list). You could use this as a base to create

79
CHAPTER 5 BUILDER PATTERN

a form generator that would generate any form you could imagine. By now you should be picking up a bad smell. Stacking loops and conditional statements inside and on top of each other quickly becomes unreadable and unmaintainable. Let s clean up the code a bit.

We are going to take the meat of each field s generation code and place that into a separate function that takes the dictionary and returns a snippet of HTML code that represents that field. The key in this step is to clean up the code without changing any of the functionality, input, or output of the main function.

cleaned_html_dictionary_ form_ generator.py

def generate_webform(field_dict_list):

generated_field_list = []

for field_dict in field_dict_list:

if field_dict["type"] == "text_field":

field_html = generate_text_field(field_dict) elif field_dict["type"] == "checkbox":

field_html = generate_checkbox(field_dict)

generated_field_list.append(field_html)

generated_fields = "\n".join(generated_field_list)

return "

{fields}
".format(fields=generated_fields)

def generate_text_field(text_field_dict):

return ’{0}:

’.format(

text_field_dict["label"],

text_field_dict["name"]

)

def generate_checkbox(checbox_dict):

return ’

format(

checkbox_dict["id"],

checkbox_dict["value"],

checkbox_dict["label"]

)

80
CHAPTER 5 BUILDER PATTERN

def build_html_form(field_list):

with open("form_file.html", ’w’) as f:

f.write( " {}

".format(

generate_webform(field_list)

)

)

if __name__ == "__main__":

field_list = [

{

"type": "text_field",

"label": "Best text you have ever written", "name": "best_text"

},

{

"type": "checkbox",

"id": "check_it",

"value": "1",

"label": "Check for one",

},

{

"type": "text_field",

"label": "Another Text field",

"name": "text_field2"

}

] build_html_form(field_list)

The if statements are still there, but at least now the code is a little cleaner. These sprawling if statements will keep growing as we add more field types to the form generator. Let s try to better the situation by using an object-oriented approach. We could use polymorphism to deal with some of the specific fields and the issues we are having in generating them.

81
CHAPTER 5 BUILDER PATTERN

oop_html_ form_ generator.py

class HtmlField(object):

def __init__(self, **kwargs):

self.html = ""

if kwargs[’field_type’] == "text_field":

self.html = self.construct_text_field(kwargs["label"], kwargs["field_name"])

elif kwargs[’field_type’] == "checkbox":

self.html = self.construct_checkbox(kwargs["field_id"], kwargs["value"], kwargs["label"])

def construct_text_field(self, label, field_name):

return ’{0}:

’.format(

label,

field_name

)

def construct_checkbox(self, field_id, value, label):

return ’

field_id,

value,

label

)

def __str__(self):

return self.html

def generate_webform(field_dict_list):

generated_field_list = []

for field in field_dict_list:

try:

generated_field_list.append(str(HtmlField(**field))) except Exception as e:

print("error: {}".format(e))

82
CHAPTER 5 BUILDER PATTERN

generated_fields = "\n".join(generated_field_list)

return "

{fields}
".format(fields=generated_fields)

def build_html_form(field_list):

with open("form_file.html", ’w’) as f:

f.write( " {}

".format(

generate_webform(field_list)

)

)

if __name__ == "__main__":

field_list = [

{

"field_type": "text_field",

"label": "Best text you have ever written", "field_name": "Field One"

},

{

"field_type": "checkbox", "field_id": "check_it", "value": "1",

"label": "Check for on",

}, {

"field_type": "text_field", "label": "Another Text field", "field_name": "Field One"

}

] build_html_form(field_list)

For every alternative set of parameters, we need another constructor, so our code very quickly breaks down and becomes a mess of constructors, which is commonly known as the telescoping constructor anti-pattern. Like design patterns, anti-patterns are mistakes you will see regularly due to the nature of the software design and

83
CHAPTER 5 BUILDER PATTERN

development process in the real world. With some languages, like Java, you can overload the constructor with many different options in terms of the parameters accepted and what is then constructed in turn. Also, notice that the if conditional in the constructor __init__() could be defined more clearly.

I m sure you have a lot of objections to this implementation, and you should. Before we clean it up a little more, we are going to quickly look at anti-patterns.

Anti-Patterns

An anti-pattern is, as the name suggests, the opposite of a software pattern. It is also something that appears regularly in the process of software development. Anti-patterns are usually a general way to work around or attempt to solve a specific type of problem, but are also the wrong way to solve the problem. This does not mean that design patterns are always the right way. What it means is that our goal is to write clean code that is easy to debug, easy to update, and easy to extend. Anti-patterns lead to code that is exactly the opposite of these stated goals. They produce bad code.

The problem with the telescoping constructor anti-pattern is that it results in several constructors, each with a specific number of parameters, all of which then delegate to a default constructor (if the class is properly written).

The builder pattern does not use numerous constructors; it uses a builder object instead. This builder object receives each initialization parameter step by step and then returns the resulting constructed object as a single result. In the webform example, we want to add the different fields and get the generated webform as a result. An added benefit of the builder pattern is that it separates the construction of an object from

the way the object is represented, so we could change the representation of the object without changing the process by which it is constructed.

In the builder pattern, the two main players are the Builder and the Director.

The Builder is an abstract class that knows how to build all the components of the final object. In our case, the Builder class will know how to build each of the field types. It can assemble the various parts into a complete form object.

The Director controls the process of building. There is an instance (or instances)

of Builder that the Director uses to build the webform. The output from the Director is a fully initialized object the webform, in our example. The Director implements the set of instructions for setting up a webform from the fields it contains. This set of instructions is independent of the types of the individual fields passed to the director.

84
CHAPTER 5 BUILDER PATTERN

A generic implementation of the builder pattern in Python looks like this:

form_builder.py

from abc import ABCMeta, abstractmethod class Director(object, metaclass=ABCMeta):

def __init__(self):

self._builder = None

@abstractmethod def construct(self):

pass

def get_constructed_object(self):

return self._builder.constructed_object

class Builder(object, metaclass=ABCMeta):

def __init__(self, constructed_object):

self.constructed_object = constructed_object

class Product(object): def __init__(self):

pass

def __repr__(self):

pass

class ConcreteBuilder(Builder): pass

class ConcreteDirector(Director): pass

You see we have an abstract class, Builder, which forms the interface for creating objects (product). Then, the ConcreteBuilder provides an implementation for Builder. The resulting object is able to construct other objects.

85
CHAPTER 5 BUILDER PATTERN

Using the builder pattern, we can now redo our webform generator. from abc import ABCMeta, abstractmethod

class Director(object, metaclass=ABCMeta):

def __init__(self):

self._builder = None

def set_builder(self, builder):

self._builder = builder

@abstractmethod

def construct(self, field_list):

pass

def get_constructed_object(self):

return self._builder.constructed_object

class AbstractFormBuilder(object, metaclass=ABCMeta):

def __init__(self):

self.constructed_object = None

@abstractmethod

def add_text_field(self, field_dict):

pass

@abstractmethod

def add_checkbox(self, checkbox_dict):

pass

@abstractmethod

def add_button(self, button_dict):

pass

class HtmlForm(object): def __init__(self):

self.field_list = []

86
CHAPTER 5 BUILDER PATTERN

def __repr__(self):

return "

{}
".format("".join(self.field_list))

class HtmlFormBuilder(AbstractFormBuilder):

def __init__(self):

self.constructed_object = HtmlForm()

def add_text_field(self, field_dict):

self.constructed_object.field_list.append(

’{0}:

’.format(

field_dict[’label’],

field_dict[’field_name’]

)

)

def add_checkbox(self, checkbox_dict):

self.constructed_object.field_list.append(

checkbox_dict[’field_id’],

checkbox_dict[’value’],

checkbox_dict[’label’]

)

)

def add_button(self, button_dict): self.constructed_object.field_list.append(

’.format( button_dict[’text’]

)

)

class FormDirector(Director):

def __init__(self):

Director.__init__(self)

87
CHAPTER 5 BUILDER PATTERN

def construct(self, field_list):

for field in field_list:

if field["field_type"] == "text_field":

self._builder.add_text_field(field) elif field["field_type"] == "checkbox":

self._builder.add_checkbox(field) elif field["field_type"] == "button":

self._builder.add_button(field)

if __name__ == "__main__":

director = FormDirector() html_form_builder = HtmlFormBuilder() director.set_builder(html_form_builder)

field_list = [

{

"field_type": "text_field",

"label": "Best text you have ever written", "field_name": "Field One"

}, {

"field_type": "checkbox", "field_id": "check_it", "value": "1",

"label": "Check for on",

}, {

"field_type": "text_field", "label": "Another Text field", "field_name": "Field One"

}, {

"field_type": "button", "text": "DONE"

}

]

88
CHAPTER 5 BUILDER PATTERN

director.construct(field_list)

with open("form_file.html", ’w’) as f:

f.write( " {0!r}

".format(

director.get_constructed_object()

)

)

Our new script can only build forms with text fields, checkboxes, and a basic button. You can add more field types as an exercise.

One of the ConcreteBuilder classes has the logic needed for the creation of the webform you want. Director calls the create() method. In this way, the logic for

creating different kinds of products gets abstracted. Note how the builder pattern focuses on building the form step by step. This is in stark contrast to the abstract factory, where a family of products can be created and returned immediately using polymorphism.

Since the builder pattern separates the construction of a complex object from its representation, you can now use the same construction process to create forms in different representations, from native app interface code to a simple text field.

An added benefit of this separation is a reduction in object size, which leads to cleaner code. We have better control over the construction process and the modular nature allow us to easily make changes to the internal representation of objects.

The biggest downside is that this pattern requires that we create ConcreteBuilder builders for each type of product that you want to create.

So, when should you not use the builder pattern? When you are constructing mutable objects that provide a means of modifying the configuration values after the object has been constructed. You should also avoid this pattern if there are very few constructors, the constructors are simple, or there are no reasonable default values for any of the constructor parameters and all of them must be specified explicitly.

A Note on Abstraction

When we talk about abstraction, in the most basic terms we refer to the process of associating names to things. Machine code is abstracted in C using labels for the Machine instructions, which are a set of hexadecimal instructions, which in turn is an

89
CHAPTER 5 BUILDER PATTERN

abstraction of the actual binary numbers in the machine. With Python, we have a further level of abstraction in terms of objects and functions (even though C also has functions and libraries).

Exercises

Implement a radio group generation function as part of your form

builder.

Extend the form builder to handle post-submit data and save the

incoming information in a dictionary.

Implement a version of the form builder that uses a database table

schema to build a form interface to the database table.

Code up an admin interface generator that auto-generates forms to

deal with information to and from a database. You can pass a schema in as a string to the form builder and then have it return the HTML for the form, plus some submit URL.

Extend the form builder so you have two new objects, one to create

an XML version of the form and another to generate a JSON object containing the form information.

90

'Practical Python Design Patterns' 카테고리의 다른 글

Decorator Pattern  (0) 2023.03.28
Adapter Pattern  (0) 2023.03.28
Factory Pattern  (0) 2023.03.28
The Prototype Pattern  (0) 2023.03.28
The Singleton Pattern  (0) 2023.03.28

댓글