11.5. Builder¶
EN: Builder
PL: Budowniczy
Type: object
Why: To separate the construction of an object from its representation
Why: The same construction algorithm can be applied to different representations
Usecase: Export data to different formats
11.5.1. Pattern¶

11.5.2. Problem¶
Violates Open/Close Principle
Tight coupling between Presentation class with formats
PDF has pages, Movies has frames, this knowledge belongs to somewhere else
Duplicated code
Magic number

from enum import Enum
class Slide:
text: str
def __init__(self, text: str) -> None:
self.text = text
def get_text(self) -> str:
return self.text
#%% Formats
class PresentationFormat(Enum):
PDF = 1
IMAGE = 2
POWERPOINT = 3
MOVIE = 4
class PDFDocument:
def add_page(self, text: str) -> None:
print('Adding a page to PDF')
class Movie:
def add_frame(self, text: str, duration: int) -> None:
print('Adding a frame to a movie')
#%% Main
class Presentation:
slides: list[Slide]
def __init__(self) -> None:
self.slides = []
def add_slide(self, slide: Slide) -> None:
self.slides.append(slide)
def export(self, format: PresentationFormat) -> None:
if format == PresentationFormat.PDF:
pdf = PDFDocument()
pdf.add_page('Copyright')
for slide in self.slides:
pdf.add_page(slide.get_text())
elif format == PresentationFormat.MOVIE:
movie = Movie()
movie.add_frame('Copyright', duration=3)
for slide in self.slides:
movie.add_frame(slide.get_text(), duration=3)
11.5.3. Solution¶
Use the builder pattern to separate the exporting logic from the presentation format
The same exporting logic belongs to the different formats

from enum import Enum
class Slide:
text: str
def __init__(self, text: str) -> None:
self.text = text
def get_text(self) -> str:
return self.text
class PresentationBuilder:
def add_slide(self, slide: Slide) -> None:
raise NotImplementedError
#%% Formats
class PresentationFormat(Enum):
PDF = 1
IMAGE = 2
POWERPOINT = 3
MOVIE = 4
class PDFDocument:
def add_page(self, text: str) -> None:
print('Adding a page to PDF')
class Movie:
def add_frame(self, text: str, duration: int) -> None:
print('Adding a frame to a movie')
class PDFDocumentBuilder(PresentationBuilder):
document: PDFDocument
def __init__(self):
self.document = PDFDocument()
def add_slide(self, slide: Slide) -> None:
self.document.add_page(slide.get_text())
def get_pdf_document(self) -> PDFDocument:
return self.document
class MovieBuilder(PresentationBuilder):
movie: Movie
def __init__(self):
self.movie = Movie()
def add_slide(self, slide: Slide) -> None:
self.movie.add_frame(slide.get_text(), duration=3)
def get_movie(self) -> Movie:
return self.movie
#%% Main
class Presentation:
slides: list[Slide]
def __init__(self) -> None:
self.slides = []
def add_slide(self, slide: Slide) -> None:
self.slides.append(slide)
def export(self, builder: PresentationBuilder) -> None:
builder.add_slide(Slide('Copyright'))
for slide in self.slides:
builder.add_slide(slide)
if __name__ == '__main__':
presentation = Presentation()
presentation.add_slide(Slide('Slide 1'))
presentation.add_slide(Slide('Slide 2'))
builder = PDFDocumentBuilder()
presentation.export(builder)
movie = builder.get_pdf_document()
builder = MovieBuilder()
presentation.export(builder)
movie = builder.get_movie()
11.5.4. Use Case - 0x01¶
class ReadCSV:
filename: str
delimiter: str
encoding: str
chunksize: int
def __init__(self, filename):
self.filename = filename
def withChunksize(self, value):
self.chunksize = value
return self
def withDelimiter(self, value):
self.delimiter = value
return self
def withEncoding(self, value):
self.encoding = value
return self
if __name__ == '__main__':
data = (
ReadCSV('myfile.csv')
.withChunksize(10_1000)
.withDelimiter(',')
.withEncoding('UTF-8')
)
11.5.5. Use Case - 0x02¶
When language does not have keyword arguments to functions and methods
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html
>>> def read_csv(filepath_or_buffer, sep=', ', delimiter=None, header='infer',
... names=None, index_col=None, usecols=None, squeeze=False,
... prefix=None, mangle_dupe_cols=True, dtype=None, engine=None,
... converters=None, true_values=None, false_values=None,
... skipinitialspace=False, skiprows=None, nrows=None,
... na_values=None, keep_default_na=True, na_filter=True,
... verbose=False, skip_blank_lines=True, parse_dates=False,
... infer_datetime_format=False, keep_date_col=False,
... date_parser=None, dayfirst=False, iterator=False,
... chunksize=None, compression='infer', thousands=None,
... decimal=b'.', lineterminator=None, quotechar='"',
... quoting=0, escapechar=None, comment=None, encoding=None,
... dialect=None, tupleize_cols=None, error_bad_lines=True,
... warn_bad_lines=True, skipfooter=0, doublequote=True,
... delim_whitespace=False, low_memory=True, memory_map=False,
... float_precision=None): ...
>>> data = read_csv('myfile.csv', ', ', None, 'infer', None, None, None,
... False, None, True, None, None, None, None, None, False,
... None, None, None, True, True, False, True, False, False,
... False, None, False, False, None, 'infer', None, b'.',
... None, '"', 0, None, None, None, None, None, True, True,
... 0, True, False, True, False, None)
>>> data = read_csv('myfile.csv',
... chunksize=10_000,
... delimiter=',',
... encoding='utf-8')
11.5.6. Use Case - 0x02¶
>>> class Person:
... def __init__(self, firstname, lastname, email, age, height, weight):
... self.firstname = firstname
... self.lastname = lastname
... self.email = email
... self.age = age
... self.height = height
... self.weight = weight
>>> mark = Person( 'Mark', 'Watney', 'mwatney@nasa.gov', 40, 185, 75)
>>> mark = Person(
... firstname='Mark',
... lastname='Watney',
... email='mwatney@nasa.gov',
... age=40,
... height=185,
... weight=75,
... )
11.5.7. Use Case - 0x02¶
>>> class Person:
... def __init__(self, firstname, lastname, is_astronaut, is_retired,
... is_alive, friends, assignments, missions, assigned):
... ...
>>> mark = Person('Mark', 'Watney', True, False, True, None, 1, 17, False)
>>> mark = Person(
... firstname = 'Mark',
... lastname = 'Watney',
... is_astronaut = True,
... is_retired = False,
... is_alive = True,
... friends = None,
... assignments = 1,
... missions = 17,
... assigned = False,
... )
>>> class Person:
... def __init__(self):
... ...
...
... def withFirstname(self, firstname):
... self.firstname = firstname
... return self
...
... def withLastname(self, lastname):
... self.lastname = lastname
... return self
...
... def withIsAstronaut(self, is_astronaut):
... self.is_astronaut = is_astronaut
... return self
...
... def withIsRetired(self, is_retired):
... self.is_retired = is_retired
... return self
...
... def withIsAlive(self, is_alive):
... self.is_alive = is_alive
... return self
...
... def withFriends(self, friends):
... self.friends = friends
... return self
...
... def withAssignments(self, assignments):
... self.assignments = assignments
... return self
...
... def withMissions(self, missions):
... self.missions = missions
... return self
...
... def withAssigned(self, assigned):
... self.assigned = assigned
... return self
>>>
>>>
>>> mark = (
... Person()
... .withFirstname('Mark')
... .withLastname('Watney')
... .withIsAstronaut(True)
... .withIsRetired(False)
... .withIsAlive(True)
... .withFriends(None)
... .withAssignments(1)
... .withMissions(17)
... .withAssigned(False)
... )
11.5.8. Assignments¶
"""
* Assignment: DesignPatterns Creational BuilderTexture
* Complexity: easy
* Lines of code: 18 lines
* Time: 8 min
English:
1. Create class `Texture`
2. Use builder pattern to set:
a. `file: Path` convert from `str`
b. `width: int` value greater than 0
c. `height: int` value greater than 0
d. `quality: int` from 1 to 100 percent
3. Run doctests - all must succeed
Polish:
TODO: Polish translation
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from pprint import pprint
>>> result = (
... Texture()
... .with_file('img/dragon/alive.png')
... .with_height(100)
... .with_width(50)
... .with_quality(75))
>>>
>>> vars(result)
{'file': PosixPath('img/dragon/alive.png'), 'height': 100, 'width': 50, 'quality': 75}
"""
from pathlib import Path
class Texture:
file: Path
width: int
height: int
quality: int
"""
* Assignment: DesignPatterns Creational BuilderEmail
* Complexity: easy
* Lines of code: 15 lines
* Time: 13 min
English:
1. Create class `Email`
2. Use builder pattern to set:
a. `recipient: str` verify email address using regex
b. `sender: str` verify email address using regex
c. `subject: str` encode to bytes
d. `body: str` encode to bytes
e. `attachment: bytes` base64 encoded
3. Run doctests - all must succeed
Polish:
TODO: Polish translation
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from pprint import pprint
>>> result = (
... Email()
... .with_recipient('mwatney@nasa.gov')
... .with_sender('mlewis@nasa.gov')
... .with_subject('Hello from Mars')
... .with_body('Greetings from Red Planet')
... .with_attachment('myfile.txt'.encode())
... )
>>> pprint(vars(result), width=72, sort_dicts=False)
{'recipient': 'mwatney@nasa.gov',
'sender': 'mlewis@nasa.gov',
'subject': 'Hello from Mars',
'body': 'Greetings from Red Planet',
'attachment': b'bXlmaWxlLnR4dA=='}
"""
from base64 import b64encode
class Email:
recipient: str
sender: str
subject: str
body: str
attachment: bytes
""""
You’re building a word processor similar to Word. The user can
add text or image elements to a document and then export it to
a variety of different formats such as HTML, text, and so on.
Look at the implementation of the Document class in the builder
package. Note that if the selected format is HTML, all text and
image elements are written to an HTML document. If the selected
format is text, however, only text elements are written to a text
file. You can run the code in the Demo class to see this in action.
What are the problems with the current design? Refactor this design
using the builder pattern.
"""