Builder pattern
Hey there Pythonistas! Have you ever found yourself in a situation where you needed to create complex objects with lots of different properties? Or maybe you’ve been frustrated with having to remember a bunch of arguments to pass to a constructor every time you want to create an object. Fear not, because the builder pattern is here to save the day!
In this blog post, we’ll be diving into the builder pattern in Python and how it can make your life as a developer a whole lot easier. We’ll explore the basics of the pattern, how to implement it in Python, and some real-world use cases where the builder pattern really shines. So sit back, grab a cup of coffee (or tea, we don’t judge), and let’s get building!
Let’s start from the definition.
The builder pattern is a creational design pattern that helps simplify the creation of complex objects by separating the object creation process into multiple steps. It allows you to construct objects step by step, and lets you produce different types and representations of an object using the same construction code. By using the builder pattern, you can make your code more flexible, maintainable, and easier to read.
And now we can write some code. First, we create a Transaction class and a TransactionBuilder class. The TransactionBuilder class has methods to set each of the transaction’s properties. Once all the properties are set, the builder returns the fully constructed transaction object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class Transaction: def __init__(self): self.date = None self.description = None self.amount = None self.atomic_type = None class TransactionBuilder: def __init__(self): self.transaction = Transaction() def set_date(self, date): self.transaction.date = date return self def set_description(self, description): self.transaction.description = description return self def set_amount(self, amount): self.transaction.amount = amount return self def set_atomic_type(self, atomic_type): self.transaction.atomic_type = atomic_type return self def build(self): return self.transaction |
To create a transaction object using the builder pattern, we can do the following:
1 2 3 4 5 6 7 |
builder = TransactionBuilder() transaction = builder\ .set_date('2023-04-11')\ .set_description('Grocery shopping')\ .set_amount(50.00)\ .set_atomic_type('debit')\ .build() |
In this example, we’ve created a Transaction object step by step using the TransactionBuilder class. We’ve set the date, description, amount, and atomic_type using the builder’s methods, and then we’ve called the
build()
method to get the fully constructed transaction object.
Sounds easy, right? But what if we have more complex object, so more than one builder is needed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
class Person: def __init__(self): # address self.street_address = None self.postcode = None self.city = None # employment info self.company_name = None self.position = None self.annual_income = None class PersonBuilder: def __init__(self, person=None): if person is None: self.person = Person() else: 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 |
In the example above we can see that Person class has 2 types of fields related to address and employment info. So as the first step we can create PersonBuilder which is simple builder with few additions.
Firstly, we will initialise the ‘person’ object by default with the Person() class. However, if the ‘person’ object already exists, we can utilize it instead. By doing so, we avoid creating a new instance of the ‘person’ object every time and instead reuse the existing one. This approach enables us to create a fluent interface and easily switch between builders.
Secondly, we will add two properties that will return two child builders: PersonJobBuilder and PersonAddressBuilder.
As the result we can use builder like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
pb = PersonBuilder() p = pb\ .lives\ .at('123 London Road')\ .in_city('London')\ .with_postcode('SW12BC')\ .works\ .at('Fabrikam')\ .as_a('Engineer')\ .earning(123000)\ .build() person2 = PersonBuilder().build() |
Let’s follow this logic step by step:
- We created instance of PersonBuilder with empty person, this means self.person = Person() code was executed in __init__ method
- We are getting property .lives that returns PersonAddressBuilder(self.person)
- Inside PersonAddressBuilder we are calling constructor of the parent class, this time person is not empty, so we use existing person instance
- This way we can fluently call .works property and continue to build our person object, without creation of new one