본문 바로가기
Mastering Python Design Patterns

The Builder Pattern

by 자동매매 2023. 3. 22.

2 The Builder Pattern

In the previous chapter, we covered the first two creational patterns, the factory method and abstract factory, which both offer approaches to improve the way we create objects in nontrivial cases.

Now, imagine that we want to create an object that is composed of multiple parts and the composition needs to be done step by step. The object is not complete unless all its parts are fully created. That's where the builder design pattern can help us. The builder pattern separates the construction of a complex object from its representation. By keeping the construction separate from the representation, the same construction can be used to create several different representations (j.mp/builderpat).

A practical example can help us understand what the purpose of the builder pattern is. Suppose that we want to create an HTML page generator. The basic structure (construction part) of an HTML page is always the same: it begins with and finishes with

, inside the HTML section are the and elements, inside the head section are the and elements, and so forth. But the representation of the page can differ. Each page has its own title, its own headings, and different contents. Moreover, the page is usually built in steps: one function adds the title, another adds the main heading, another the footer, and so on. Only after the whole structure of a page is complete can it be shown to the client using a final render function. We can take it even further and extend the HTML generator so that it can generate totally different HTML pages. One page might contain tables, another page might contain image galleries, yet another page contains the contact form, and so on.

The Builder Pattern Chapter 38*

The HTML page generation problem can be solved using the builder pattern. In this pattern, there are two main participants:

  • The builder: The component responsible for creating the various parts of a complex object. In this example, these parts are the title, heading, body, and the footer of the page.
    • The director: The component that controls the building process using a builder instance. It calls the builder's functions for setting the title, the heading, and so on. And, using a different builder instance allows us to create a different HTML page without touching any of the code of the director.

In this chapter, we will discuss:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

In our everyday life, the builder design pattern is used in fast-food restaurants. The same procedure is always used to prepare a burger and the packaging (box and paper bag), even if there are many different kinds of burgers (classic, cheeseburger, and more) and different packages (small-sized box, medium-sized box, and so forth). The difference between a classic burger and a cheeseburger is in the representation, and not in the construction procedure. In this case, the director is the cashier who gives instructions about what needs to be prepared to the crew, and the builder is the person from the crew that takes care of the specific order.

We can also find software examples:

  • The HTML example that was mentioned at the beginning of the chapter is actually used by django-widgy (https:/ / wid. gy/ ), a third-party tree editor for Django that can be used as a content management system (CMS). The django- widgy editor contains a page builder that can be used for creating HTML pages with different layouts.
  • The django-query-builder library (https:/ / github.com/ ambitioninc/django- query-builder) is another third-party Django library that relies on the builder pattern. This library can be used for building SQL queries dynamically, allowing you to control all aspects of a query and create a different range of queries, from simple to very complex ones.

Use cases

We use the builder pattern when we know that an object must be created in multiple steps, and different representations of the same construction are required. These requirements exist in many applications such as page generators (for example, the HTML page generator mentioned in this chapter), document converters, and user interface (UI) form creators (j.mp/pipbuild).

Some online resources mention that the builder pattern can also be used as a solution to the telescopic constructor problem. The telescopic constructor problem occurs when we are forced to create a new constructor for supporting different ways of creating an object. The problem is that we end up with many constructors and long parameter lists, which are hard to manage. An example of the telescopic constructor is listed at the Stack Overflow website (j.mp/sobuilder). Fortunately, this problem does not exist in Python, because it can be

solved in at least two ways:

At this point, the distinction between the builder pattern and the factory pattern might not be very clear. The main difference is that a factory pattern creates an object in a single step, whereas a builder pattern creates an object in multiple steps, and almost always through the use of a director. Some targeted implementations of the builder pattern, such as Java's StringBuilder, bypass the use of a director, but that's the exception to the rule.

Another difference is that while a factory pattern returns a created object immediately, in the builder pattern the client code explicitly asks the director to return the final object when it needs it (j.mp/builderpat).

The new computer analogy might help you to distinguish between a builder pattern and a factory pattern. Assume that you want to buy a new computer. If you decide to buy a specific, preconfigured computer model, for example, the latest Apple 1.4 GHz Mac Mini, you use the factory pattern. All the hardware specifications are already predefined by the manufacturer, who knows what to do without consulting you. The manufacturer typically receives just a single instruction. Code-wise, this would look like the following (apple_factory.py):

MINI14 = '1.4GHz Mac mini'

class AppleFactory:

class MacMini14:

def __init__(self):

self.memory = 4 # in gigabytes

self.hdd = 500 # in gigabytes

self.gpu = 'Intel HD Graphics 5000'

def __str__(self):

info = (f'Model: {MINI14}',

f'Memory: {self.memory}GB',

f'Hard Disk: {self.hdd}GB',

f'Graphics Card: {self.gpu}') return '\n'.join(info)

def build_computer(self, model):

if model == MINI14:

return self.MacMini14()

else:

msg = f"I don't know how to build {model}" print(msg)

Now, we add the main part of the program, the snippet which uses the AppleFactory class:

if __name__ == '__main__':

afac = AppleFactory()

mac_mini = afac.build_computer(MINI14) print(mac_mini)

Another option would be to buy a custom PC. In this case, you use the builder pattern. You are the director that gives orders to the manufacturer (builder) about your ideal computer specifications. Code-wise, this looks like the following (computer_builder.py):

We define the Computer class as follows:

class Computer:

def __init__(self, serial_number):

self.serial = serial_number

self.memory = None # in gigabytes self.hdd = None # in gigabytes

self.gpu = None

def __str__(self):

info = (f'Memory: {self.memory}GB',

f'Hard Disk: {self.hdd}GB',

f'Graphics Card: {self.gpu}') return '\n'.join(info)

  • We define the ComputerBuilder class:

class ComputerBuilder:

def __init__(self):

self.computer = Computer('AG23385193')

def configure_memory(self, amount): self.computer.memory = amount

def configure_hdd(self, amount): self.computer.hdd = amount

def configure_gpu(self, gpu_model): self.computer.gpu = gpu_model

  • We define the HardwareEngineer class as follows:

class HardwareEngineer:

def __init__(self):

self.builder = None

def construct_computer(self, memory, hdd, gpu):

self.builder = ComputerBuilder()

steps = (self.builder.configure_memory(memory), self.builder.configure_hdd(hdd), self.builder.configure_gpu(gpu))

[step for step in steps]

@property

def computer(self):

return self.builder.computer

  • We end our code with the main() function, followed by the trick to call it when the file is called from the command line:

def main():

engineer = HardwareEngineer() engineer.construct_computer(hdd=500, memory=8,

gpu='GeForce GTX 650 Ti') computer = engineer.computer

print(computer)

if __name__ == '__main__': main()

The basic changes are the introduction of a builder, ComputerBuilder, a

director, HardwareEngineer, and the step-by-step construction of a computer, which now supports different configurations (notice that memory, hdd, and gpu are parameters and are not preconfigured). What do we need to do if we want to support the construction of tablets? Implement this as an exercise.

You might also want to change the computer's serial_number into something that is different for each computer, because as it is now, it means that all computers will have the same serial number (which is impractical).

Implementation

Let's see how we can use the builder design pattern to make a pizza-ordering application. The pizza example is particularly interesting because a pizza is prepared in steps that should follow a specific order. To add the sauce, you first need to prepare the dough. To add the topping, you first need to add the sauce. And you can't start baking the pizza unless both the sauce and the topping are placed on the dough. Moreover, each pizza usually requires a different baking time, depending on the thickness of its dough and the topping used.

We start by importing the required modules and declaring a few Enum parameters (j.mp/pytenum) plus a constant that is used many times in the application. The STEP_DELAY constant is used to add a time delay between the different steps of preparing a pizza (prepare the dough, add the sauce, and so on) as follows:

from enum import Enum

import time

PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready') PizzaDough = Enum('PizzaDough', 'thin thick')

PizzaSauce = Enum('PizzaSauce', 'tomato creme_fraiche')

PizzaTopping = Enum('PizzaTopping',

'mozzarella double_mozzarella bacon ham mushrooms red_onion oregano')

STEP_DELAY = 3 # in seconds for the sake of the example

Our end product is a pizza, which is described by the Pizza class. When using the builder pattern, the end product does not have many responsibilities, since it is not supposed to be instantiated directly. A builder creates an instance of the end product and makes sure that it is properly prepared. That's why the Pizza class is so minimal. It basically initializes all data to sane default values. An exception is the prepare_dough() method.

The prepare_dough() method is defined in the Pizza class instead of a builder for two reasons. First, to clarify the fact that the end product is typically minimal, which does not mean that you should never assign it any responsibilities. Second, to promote code reuse through composition.

So, we define our Pizza class as follows:

class Pizza:

def __init__(self, name): self.name = name

self.dough = None

self.sauce = None

self.topping = []

def __str__(self):

return self.name

def prepare_dough(self, dough):

self.dough = dough

print(f'preparing the {self.dough.name} dough of your {self}...') time.sleep(STEP_DELAY)

print(f'done with the {self.dough.name} dough')

There are two builders: one for creating a margarita pizza (MargaritaBuilder) and another for creating a creamy bacon pizza (CreamyBaconBuilder). Each builder creates a Pizza instance and contains methods that follow the pizza-making procedure: prepare_dough(), add_sauce(), add_topping(), and bake(). To be precise,

prepare_dough() is just a wrapper to the prepare_dough() method of the Pizza class.

Notice how each builder takes care of all the pizza-specific details. For example, the topping of the margarita pizza is double mozzarella and oregano, while the topping of the creamy bacon pizza is mozzarella, bacon, ham, mushrooms, red onion, and oregano.

This part of our code is laid out as follows:

We define the MargaritaBuilder class as follows:

class MargaritaBuilder:

def __init__(self):

self.pizza = Pizza('margarita')

self.progress = PizzaProgress.queued

self.baking_time = 5 # in seconds for the sake of the example

def prepare_dough(self):

self.progress = PizzaProgress.preparation self.pizza.prepare_dough(PizzaDough.thin)

def add_sauce(self):

print('adding the tomato sauce to your margarita...') self.pizza.sauce = PizzaSauce.tomato time.sleep(STEP_DELAY)

print('done with the tomato sauce')

def add_topping(self):

topping_desc = 'double mozzarella, oregano'

topping_items = (PizzaTopping.double_mozzarella, PizzaTopping.oregano)

print(f'adding the topping ({topping_desc}) to your margarita')

self.pizza.topping.append([t for t in topping_items])

time.sleep(STEP_DELAY)

print(f'done with the topping ({topping_desc})')

def bake(self):

self.progress = PizzaProgress.baking

print(f'baking your margarita for {self.baking_time} seconds')

time.sleep(self.baking_time)

self.progress = PizzaProgress.ready

print('your margarita is ready')

  • We define the CreamyBaconBuilder class as follows:

class CreamyBaconBuilder:

def __init__(self):

self.pizza = Pizza('creamy bacon')

self.progress = PizzaProgress.queued

self.baking_time = 7 # in seconds for the sake of the example

def prepare_dough(self):

self.progress = PizzaProgress.preparation self.pizza.prepare_dough(PizzaDough.thick)

def add_sauce(self):

print('adding the crème fraîche sauce to your creamy bacon')

self.pizza.sauce = PizzaSauce.creme_fraiche time.sleep(STEP_DELAY) print('done with the crème fraîche sauce')

def add_topping(self):

topping_desc = 'mozzarella, bacon, ham, mushrooms,

red onion, oregano'

topping_items = (PizzaTopping.mozzarella, PizzaTopping.bacon, PizzaTopping.ham, PizzaTopping.mushrooms, PizzaTopping.red_onion, PizzaTopping.oregano)

print(f'adding the topping ({topping_desc}) to your

creamy bacon')

self.pizza.topping.append([t for t in topping_items]) time.sleep(STEP_DELAY)

print(f'done with the topping ({topping_desc})')

def bake(self):

self.progress = PizzaProgress.baking

print(f'baking your creamy bacon for {self.baking_time} seconds')

time.sleep(self.baking_time)

self.progress = PizzaProgress.ready

print('your creamy bacon is ready')

The director in this example is the waiter. The core of the Waiter class is the construct_pizza() method, which accepts a builder as a parameter and executes all the pizza-preparation steps in the right order. Choosing the appropriate builder, which can even be done at runtime, gives us the ability to create different pizza styles without modifying any of the code of the director (Waiter). The Waiter class also contains the pizza() method, which returns the end product (prepared pizza) as a variable to the caller as follows:

class Waiter:

def __init__(self):

self.builder = None

def construct_pizza(self, builder): self.builder = builder

steps = (builder.prepare_dough, builder.add_sauce, builder.add_topping, builder.bake)

[step() for step in steps]

@property

def pizza(self):

return self.builder.pizza

The validate_style() function is similar to the validate_age() function as described in Chapter 1, *The Factory Pattern. It is used to make sure that the user gives valid input, which in this case is a character that is mapped to a pizza builder. The m character uses the MargaritaBuilder class and the c character uses the CreamyBaconBuilder class. These mappings are in the builder parameter. A tuple is returned, with the first element set to True if the input is valid, or False if it is invalid as follows:

def validate_style(builders):

try:

input_msg = 'What pizza would you like, [m]argarita or [c]reamy bacon? '

pizza_style = input(input_msg)

builder = builderspizza_style

valid_input = True

except KeyError:

error_msg = 'Sorry, only margarita (key m) and creamy

bacon (key c) are available' print(error_msg)

return (False, None)

return (True, builder)

The last part is the main() function. The main() function contains code for instantiating a pizza builder. The pizza builder is then used by the Waiter director for preparing the pizza. The created pizza can be delivered to the client at any later point:

def main():

builders = dict(m=MargaritaBuilder, c=CreamyBaconBuilder) valid_input = False

while not valid_input:

valid_input, builder = validate_style(builders) print()

waiter = Waiter()

waiter.construct_pizza(builder)

pizza = waiter.pizza

print()

print(f'Enjoy your {pizza}!')

Here is the summary of the implementation (see the complete code in the builder.py file):

  1. We start with a couple of imports we need, for the standard Enum class and time module.
  2. We declare the variables for a few constants: PizzaProgress, PizzaDough, PizzaSauce, PizzaTopping, and STEP_DELAY.
  3. We define our Pizza class.
  4. We define the classes for two builders, MargaritaBuilder and CreamyBaconBuilder.
  5. We define our Waiter class.
  6. We add the validate_style() function to improve things regarding exception handling.
  7. Finally, we have the main() function, followed by the snippet for calling it when the program is run. In the main function, the following happens:
  • We make it possible to choose the pizza builder based on the user's input, after validation via the validate_style() function.
    • The pizza builder is used by the waiter for preparing the pizza.
      • The created pizza is then delivered.

Here is the output produced by calling the python builder.py command to execute this example program:

But... supporting only two pizza types is a shame. Feel like getting a Hawaiian pizza builder? Consider using inheritance after thinking about the advantages and disadvantages. Check the ingredients of a typical Hawaiian pizza and decide which class you need to extend: MargaritaBuilder or CreamyBaconBuilder?. Perhaps both

(j.mp/pymulti)?

In his book, Effective Java (2nd edition), Joshua Bloch describes an interesting variation of the builder pattern where calls to builder methods are chained. This is accomplished by defining the builder itself as an inner class and returning itself from each of the setter-like methods on it. The build() method returns the final object. This pattern is called the

fluent builder. Here's a Python implementation, which was kindly provided by a reviewer of the book:

class Pizza:

def __init__(self, builder):

self.garlic = builder.garlic

self.extra_cheese = builder.extra_cheese

def __str__(self):

garlic = 'yes' if self.garlic else 'no'

cheese = 'yes' if self.extra_cheese else 'no'

info = (f'Garlic: {garlic}', f'Extra cheese: {cheese}') return '\n'.join(info)

class PizzaBuilder:

def __init__(self):

self.extra_cheese = False self.garlic = False

def add_garlic(self): self.garlic = True return self

def add_extra_cheese(self): self.extra_cheese = True return self

def build(self):

return Pizza(self)

if __name__ == '__main__':

pizza = Pizza.PizzaBuilder().add_garlic().add_extra_cheese().build() print(pizza)

Adapt the pizza example to make use of the fluent builder pattern. Which version of the two do you prefer? What are the pros and cons of each version?

Summary

In this chapter, we have seen how to use the builder design pattern. We use the builder pattern for creating an object in situations where using the factory pattern (either a factory method or an abstract factory) is not a good option. A builder pattern is usually a better candidate than a factory pattern when the following applies:

  • We want to create a complex object (an object composed of many parts and created in different steps that might need to follow a specific order).
    • Different representations of an object are required, and we want to keep the construction of an object decoupled from its representation.
      • We want to create an object at one point in time, but access it at a later point.

We saw how the builder pattern is used in fast-food restaurants for preparing meals, and how two third-party Django packages, django-widgy, and django-query-builder, use it for generating HTML pages and dynamic SQL queries, respectively. We focused on the differences between a builder pattern and a factory pattern, and provided a preconfigured (factory) and customer (builder) computer order analogy to clarify them.

In the Implementation section, we looked at how to create a pizza-ordering application with preparation dependencies. There were many recommended and interesting exercises in this chapter, including implementing a fluent builder.

In the next chapter, you will learn about other useful creational patterns.

[ 38 ] )

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

The Decorator Pattern  (0) 2023.03.22
The Adapter Pattern  (0) 2023.03.22
Other Creational Patterns  (0) 2023.03.22
The Factory Pattern  (0) 2023.03.22
Index  (0) 2023.03.22

댓글