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)
... )