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

../../_images/designpatterns-builder-pattern.png

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

../../_images/designpatterns-builder-problem.png

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

../../_images/designpatterns-builder-solution.png

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

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