×

Builder Design Pattern with Python

Builder Design Patterns Python Feature

The builder pattern is a type of creational design pattern, designed to provide a flexible solution to various object creation problems in object-oriented programming.

The Builder design pattern intends to separate the construction of a complex object from its actual representation so that we can use the same construction process to create different representations for the same object.

The Builder design pattern solves problems like:

  • How can a class create different representations of a complex object?
  • How can a class that includes creating a complex object be simplified?

Simply said, Builder design patterns represent a method to builds a complex object using simple objects and a step-by-step approach.

Let us understand, by an example, where the builder pattern will help us. Let’s say there is a manufacturer producing cars (The complex product). A car can have many features and components such as autonomous driving, sunroof, supported fuel type(electric, petrol, Diesel) etc…

Consider, the Car class below-

class Car():
    '''The Product'''
    def __init__(self):
        self.autonomous_driving = False
        self.sunroof = False
        self.fuel = None

The manufacturer can produce different models depending upon the features he chooses to provide in a car. Let’s define methods to add or subtract the features.

class Car():
    '''The Product'''
    def __init__(self):
        self.autonomous_driving = False
        self.sunroof = False
        self.fuel = None
     
    #Methods to add features
    def addAutonomous_driving(self):
        self.autonomous_driving = True
    def addSunroof(self):
        self.sunroof = True
    def addFuel(self, fuel="Electric"):
        self.fuel = fuel
 
    #Returning information about car
    def __str__(self):
        return f'Autonomous driving: {self.autonomous_driving} | Sunroof: {self.sunroof} | Fuel: {self.fuel}'

The class Car has all the features and methods to add them. Now for each instance, all the features would be added via explicit method calls (addAutonomous_driving, addSunroof), depending upon the car model to produce.

#First instance name ModelOneModelOne = Car()
ModelOne.addAutonomous_driving()
ModelOne.addSunroof()
ModelOne.addFuel("Petrol")
print("Details of car:", ModelOne)
 
#Second instance named ModelTwo
ModelTwo = Car()
ModelTwo.addAutonomous_driving()
ModelTwo.addFuel()
print("Details of car:", ModelTwo)

Let’s create another instance(object).

#First instance name ModelOneModelOne = Car()
ModelOne.addAutonomous_driving()
ModelOne.addSunroof()
ModelOne.addFuel("Petrol")
print("Details of car:", ModelOne)
 
#Second instance named ModelTwo
ModelTwo = Car()
ModelTwo.addAutonomous_driving()
ModelTwo.addFuel()
print("Details of car:", ModelTwo)

#Third instance named ModelThree
ModelThree = Car()
ModelThree.addAutonomous_driving()
ModelThree.addSunroof()
ModelThree.addFuel("Diesel")
print("Details of car:", ModelThree)

Observe the complexity in the creation of each instance(object) via explicit method calls, if the number of instances increases the code will become more complex and chaotic. This is where the builder pattern helps us.

The Builder Pattern resolves this issue and brings order to this chaos by removing the complexity involved. This is achieved by segregating the entire process into four roles. Consider creating different models of car with the Builder design pattern’s four roles-

  • First- The Product: Complex object to be made. (e.g Car)
class Car:
    '''The Product'''
    def __init__(self):
        self.autonomous_driving = None
        self.sunroof = None
        self.fuel = None

    #Returning information of the car 
    def __str__(self):
        return f'Autonomous_driving: {self.autonomous_driving} | Sunroof: {self.sunroof} | Fuel: {self.fuel}'
  • Second- Abstract Builder Interface: Provides necessary interfaces required to build the object. It is abstract because it is not instantiated, it is only inherited by the Builder.
class AbstractBuilder:
    '''Abstract Builder Interface'''
    def __init__(self):
        self.car = None
    def createNewCar(self):
        self.car = Car()
  • Third- Builder: Inherits the Abstract Builder and implements the above interfaces of the Abstract Builder class; provides methods to create components of the product.
class ConcreteBuilder(AbstractBuilder):
    '''Actual Builder'''
    def addAutonomous_driving(self, AD):
        self.car.autonomous_driving = AD
    def addSunroof(self, SR):
        self.car.sunroof = SR
    def addFuel(self, fuel):
        self.car.fuel = fuel
  • Fourth- Director: In charge of creating the product, assembling various components and then delivering it. It uses the concrete builder object.
class Director:
    '''Director'''
    def __init__(self, builder):
        self._builder = builder
    def constructCar(self, AD=False, SR=False, fuel="Electric"):
        self._builder.createNewCar()
        self._builder.addAutonomous_driving(AD)
        self._builder.addSunroof(SR)
        self._builder.addFuel(fuel)
        return self._builder.car

#Instantiation of Builder
concreteBuilder = ConcreteBuilder()
#Calling Director
director = Director(concreteBuilder)

The whole procedure is- Abstract builder class creates an instance of the product(Car). The Concrete builder knows the implementation of all the methods(i.e methods that add features), to the instance created by the abstract builder.

Then there is this Director who asks the Concrete builder to implement the methods(i.e. asks to add a specific feature to the product). Clearly, variation in the product is a passing of the parameter to the director. And now creating a new instance is a matter of just two lines.

#Getting Our Product
ModelOne = director.constructCar()
print("Details of car:", ModelOne)

ModelTwo = director.constructCar(True, True, "Diesel")
print("Details of car:", ModelTwo)

ModelThree = director.constructCar(True, False, "Petrol")
print("Details of car:", ModelThree)

Builder patterns with increasing instances become more economic and systematic. This approach also makes it more flexible to add and remove options from the product that has to be created.

Construction of complex objects need not have only one Builder, rather there can be several Builders depending upon the complexity of the builder itself.

Pros and Cons of Builder Design Patterns

Pros

  • Various representations of the products with the same implementation.
  • Since each entity has its own specific task, hence upholds the Single Responsibility Principle.

Cons

  • Builder pattern requires creating multiple new classes that’s why the complexity of code may increase.
  • It requires the builder class to be mutable, hence the Builder pattern may violate, Open-Closed Principle.