Using Python types for fun and profit: the Builder

Introduction

The builder pattern is a creational design pattern, i.e. it is a pattern for creating or instantiang objects of classes. It is used for breaking down the construction process, into smaller, more manageable and testable steps.

It looks like this:

Let’s break this down into its parts:

  1. The Director. This is the client class for the Builder, and it wants some product to be built.
  2. The Builder interface. This is the generic interface for any Builder, and it contains the methods to build a Product.
  3. The ConcreteBuilder. This is the class where we built the Product. Because we only use an interface, ConcreteBuilders are swapped in and out to build different products.
  4. The product we want to build is in the Product class. This could also define an interface, or be referenced through one of its superclasses.

This is all rather abstract, so we will build an example.

Implementation in Python

We start by defining the basic Bicycle. In our example the main properties of a bike are its type and its number of wheels:

class Bicycle:
_number_of_wheels: int = None
_bike_type: str = None

def __init__(self):
self._bike_type = ""
self._number_of_wheels = 0

def set_bike_type(self, bike_type: str):
self._bike_type = bike_type

def set_number_of_wheels(self, number_of_wheels: int):
self._number_of_wheels = number_of_wheels

def __str__(self) -> str:
return f"Type: {self._bike_type} with {self._number_of_wheels} wheels"

Some points:

  1. The instance variables, _number_of_wheels and _bike_type are declared in advance and with type annotations.
  2. We need some setter methods because our instance variables are private
  3. The __str__() method is a standard method in Python to render the string representation of an object.

Now we come to the BicycleBuilder. In other languages this would have been an interface, a protocol or a trait. But since Python has no concept this, we will just define a class with empty (i.e. pass-through) methods:

class BicycleBuilder:
    def set_number_of_wheels(self):
        pass

    def set_type(self):
        pass

    def get_result(self) -> Bicycle:
        pass

The first two methods are self-explanatory. However, the get_result() returns a finished product, and therefore has a returntype: Bicycle.

Now we can implement our first concrete builder:

class ATBBuilder(BicycleBuilder):
    _vehicle: Bicycle = None

    def __init__(self):
        self._vehicle = Bicycle()

    def set_number_of_wheels(self):
        self._vehicle.set_number_of_wheels(2)

    def set_type(self):
        self._vehicle.set_bike_type("ATBBike")

    def get_result(self) -> Bicycle:
        return self._vehicle

Some points:

  • Like in our Bicycle class we define our instance variable in advance.
  • In our constructor we construct a basic bicycle with no type and zero wheels.
  • The set_number_of_wheels() and the set_type() method set these parameters
  • Once we constructed the bicycle to our satisfaction we can return it. We depend on the client classes to make sure the bike is in a finished state. A possible extension would be to have a test to see whether our product is ready, and throw an exception or show an error when it is not.

The StreetBikeBuilder is built along the same lines:

class StreetBikeBuilder(BicycleBuilder):
    _vehicle: Bicycle = None

    def __init__(self):
        self._vehicle = Bicycle()

    def set_number_of_wheels(self):
        self._vehicle.set_number_of_wheels(3)

    def set_type(self):
        self._vehicle.set_bike_type("StreetBike")

    def get_result(self) -> Bicycle:
        return self._vehicle

Now we need some coordinator to put the parts together, this will be the BikeEngineer class:

class BikeEngineer:
    _builder: BicycleBuilder = None

    def __init__(self, builder: BicycleBuilder):
        self._builder = builder

    def construct_bike(self) -> Bicycle:
        self._builder.set_number_of_wheels()
        self._builder.set_type()
        return self._builder.get_result()

Some notes:

  • The BikeEngineer class needs to know which bike to build, therefore we initialize it with a subclass of BicycleBuilder
  • In the constructBike() method we constructed a bicycle and return the finished product.

Time to test

Now we can test our code:

if __name__ == "__main__":
    atb_builder: ATBBuilder = ATBBuilder()
    street_bike_builder: StreetBikeBuilder = StreetBikeBuilder()

    engineer: BikeEngineer = BikeEngineer(atb_builder)
    atb = engineer.construct_bike()
    print(atb)

    engineer = BikeEngineer(street_bike_builder)
    street = engineer.construct_bike()
    print(street)

This code is quite simple:

  1. We initialize two builder objects.
  2. We pass these to an engineer object, construct the bike, and print the result.

As you see, an engineer can

  1. Construct different objects
  2. Follow its own ‘recipe’ when doing so

Conclusion

As you can see it is quite simple to implement this pattern. Because the code is clear and easy to read, it is also quite clear how versatile this pattern is. The type-annotations also clarify the intent of the code.

This pattern is great if you want or need to hide the construction of certain objects.

Leave a Reply

Your email address will not be published. Required fields are marked *