8.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
8.5.1. Pattern¶

8.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)
8.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()
8.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')
)
8.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')
8.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', 42, 178.0, 75.5)
>>> mark = Person(
... firstname='Mark',
... lastname='Watney',
... email='mwatney@nasa.gov',
... age=42,
... height=178.0,
... weight=75.5,
... )
8.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)
... )
8.5.8. Assignments¶
"""
* Assignment: DesignPatterns Creational BuilderEmail
* Complexity: easy
* Lines of code: 15 lines
* Time: 5 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
3. Run doctests - all must succeed
Polish:
1. Stwórz klasę `Email`
2. Użyj wzorca builder, aby ustawić:
a. `recipient: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
b. `sender: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
c. `subject: str` koduje do bajtów
d. `body: str` koduje na bajty
3. Uruchom doctesty - wszystkie muszą się powieść
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')
... )
>>> 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'}
"""
from base64 import b64encode
class Email:
recipient: str
sender: str
subject: str
body: str
attachment: bytes
"""
* Assignment: DesignPatterns Creational BuilderEmail
* Complexity: easy
* Lines of code: 15 lines
* Time: 5 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:
1. Stwórz klasę `Email`
2. Użyj wzorca builder, aby ustawić:
a. `recipient: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
b. `sender: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
c. `subject: str` koduje do bajtów
d. `body: str` koduje na bajty
e. `attachment: bytes` zakodowane w standardzie base64
3. Uruchom doctesty - wszystkie muszą się powieść
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
"""
* 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: 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:
1. Stwórz klasę `Texture`
2. Użyj wzorca builder, aby ustawić:
a. `file: str`
b. `width: int` wartość większa niż 0
c. `height: int` wartość większa niż 0
d. `quality: int` od 1 do 100 procent
3. Uruchom doctesty - wszystkie muszą się powieść
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': 'img/dragon/alive.png', 'height': 100, 'width': 50, 'quality': 75}
"""
class Texture:
file: str
width: int
height: int
quality: int