Skip to content

Builder

This pattern is useful when creating more complicated objects that would take a lot of initializer arguments. Instead, you call a number of methods for creating the object step by step. The idea is that the builder class should expose methods to add properties to the object its creating, with a final method build to return the object. These methods can be made fluent by returning self from the function, allowing you to chain these calls together. Builders can also be broken down into Facets, which are inherited builders exposed through the base builder class as properties to handle related elements of the object you are creating. This technically violates the open closed principle because you need to add properties to the base builder, however the alternative is creating a chain of inheritance where you instantiate the most specific builder that has access to all the inherited methods which is somewhat clunky.

python
class Person:
    def __init__(self):
        print('Creating an instance of Person')
        # address
        self.street_address = None
        self.postcode = None
        self.city = None
        # employment info
        self.company_name = None
        self.position = None
        self.annual_income = None

    def __str__(self) -> str:
        return f'Address: {self.street_address}, {self.postcode}, {self.city}\n' +\
            f'Employed at {self.company_name} as a {self.postcode} earning {self.annual_income}'
    
    @staticmethod
    def new():
        return PersonBuilder()

class PersonBuilder:  # facade
    # Good practice to take person as an argument
    def __init__(self, person=Person()):
        self.person = person

    @property
    def lives(self):
        return PersonAddressBuilder(self.person)

    @property
    def works(self):
        return PersonJobBuilder(self.person)

    def build(self):
        return self.person

class PersonJobBuilder(PersonBuilder):
    def __init__(self, person):
        super().__init__(person)

    def at(self, company_name):
        self.person.company_name = company_name
        return self

    def as_a(self, position):
        self.person.position = position
        return self

    def earning(self, annual_income):
        self.person.annual_income = annual_income
        return self


class PersonAddressBuilder(PersonBuilder):
    def __init__(self, person):
        super().__init__(person)

    def at(self, street_address):
        self.person.street_address = street_address
        return self

    def with_postcode(self, postcode):
        self.person.postcode = postcode
        return self

    def in_city(self, city):
        self.person.city = city
        return self

if __name__ == '__main__':
    pb = PersonBuilder()
    p = pb\
        .lives\
            .at('123 London Road')\
            .in_city('London')\
            .with_postcode('SW12BC')\
        .works\
            .at('Fabrikam')\
            .as_a('Engineer')\
            .earning(123000)\
        .build()
    print(p)
    person2 = PersonBuilder().build()
    print(person2)