The world’s leading publication for data science, AI, and ML professionals.

Design Patterns with Python for Machine Learning Engineers: Builder

Learn how to use the Builder design pattern to enhance your code

Photo by Anton Maksimov 5642.su on Unsplash
Photo by Anton Maksimov 5642.su on Unsplash

Introduction

An essential skill for those involved in AI development is to write clean, reusable code. Therefore, today I will introduce another design pattern in Python using Deepnote.

It doesn’t matter how good you are at Deep Learning topics, statistics or anything else, if your code is not clean and easily reusable you will never be able to develop something that will have a major impact. That’s why I think it’s very important for data scientists to have software engineering skills. Design patterns are something that all people who write code should know. Today we are going to talk about the pattern called Builder.

What is a Design Pattern?

A design pattern is simply a general design solution to a recurring problem. Instead of dealing with solving the same problem over and over again, one thinks of a solution that can be used every time the same problem is encountered, and these solutions have already been found! Someone has fortunately already thought of making our lives easier! 🙂

Desgin patterns differ in several kinds. But mainly we have 3 of them:

  • Creational: it’s about the process of creating objects.
  • Structural: it’s about the composition of classes and objects.
  • Behavioural: defines how classes and objects interact and distribute responsibilities among themselves.

Design Patterns (Image by Author)
Design Patterns (Image by Author)

The Builder Design Pattern

The builder is part of that design pattern class called creational, because it precisely simplifies the creation of objects within the code. Imagine that you have a class, which requires an immense number of parameters to be instantiated, or for Pythonians, a class whose init() method expects lots of input parameters.

Imagine you have a class to design a park, perhaps because you are creating the environment for a video game. You can customize the park in various ways, adding and removing various things. You can add games, children, or create a park full of animals and more.

Image By Author
Image By Author

But at the implementation level, how do we deal with implementing all these kinds of parks? The most intuitive solution is probably to create a base Park class, which other classes will then extend to include the various features. But in this case we will end up with four subclasses, and in real projects we will have an immense number of subclasses, which would make our code impractical.

Or we could create just one class, the class Park, and use a huge constructor to which a lot of parameters can be given as input. But the problem is that in most cases the input parameters would be null, because we don’t always want to create a park that has everything, and again the code would look a little ugly.

The solution adopted by the Builder, is to create all the various features we want to include in different methods of the Park class, called builders, instead of having everything inside the constructor. This way we can build the pieces of our Park step-by-step, depending on what we need.

ParkBuilder (Image by Author)
ParkBuilder (Image by Author)

Let’s code!

Let’s see how to implement this design pattern in Python now. In this example what we want to do is to build different types of robots that have different configurations. The following code will consist of 5 basic parts.

  1. Robot Class:
  • First, we create the Robotclass that represents the objects we want to build with all their attributes such ashead, arms, legs, torso, and battery.
# Define the Robot class
class Robot:
    def __init__(self):
        self.head = None
        self.arms = None
        self.legs = None
        self.torso = None
        self.battery = None

2. Builder Interface:

  • The RobotBuilder interface is an abstract class that defines a set of methods for building the different parts of a Robotobject. These methods include reset, build_head, build_arms, build_legs, build_torso, build_battery, and get_robo.
from abc import ABC, abstractmethod

class RobotBuilder(ABC):
    @abstractmethod
    def reset(self):
        pass

    @abstractmethod
    def build_head(self):
        pass

    @abstractmethod
    def build_arms(self):
        pass

    @abstractmethod
    def build_legs(self):
        pass

    @abstractmethod
    def build_torso(self):
        pass

    @abstractmethod
    def build_battery(self):
        pass

    @abstractmethod
    def get_robot(self):
        pass

3. Concrete Builders:

  • Now we have the builder classes that implements the abstract class RobotBuilderwhich are HumanoidRobotBuilder and DroneRobotBuilder. These builders give to robots different settings that differentiate them from each other.
  • Remember that each builder maintains an instance of the Robot it’s constructing.
# Define a Concrete Builder for a Robot
class HumanoidRobotBuilder(RobotBuilder):
    def __init__(self):
        self.robot = Robot()
        self.reset()

    def reset(self):
        self.robot = Robot()

    def build_head(self):
        self.robot.head = "Humanoid Head"

    def build_arms(self):
        self.robot.arms = "Humanoid Arms"

    def build_legs(self):
        self.robot.legs = "Humanoid Legs"

    def build_torso(self):
        self.robot.torso = "Humanoid Torso"

    def build_battery(self):
        self.robot.battery = "Humanoid Battery"

    def get_robot(self):
        return self.robot
# Define a Concrete Builder for a Robot
class DroneRobotBuilder(RobotBuilder):
    def __init__(self):
        self.robot = Robot()
        self.reset()

    def reset(self):
        self.robot = Robot()

    def build_head(self):
        self.robot.head = "Drone Head"

    def build_arms(self):
        self.robot.arms = "No Arms"

    def build_legs(self):
        self.robot.legs = "No Legs"

    def build_torso(self):
        self.robot.torso = "Drone Torso"

    def build_battery(self):
        self.robot.battery = "Drone Battery"

    def get_robot(self):
        return self.robot

4. RobotDirector:

  • The class called RobotDirector has the task of directing the construction of robots using specific builders it has available.
  • Inside this class, you will find the set_buildermethod to activate the builder you need and dui build_humanoid_robotand build_drone_robot methods to create different types of robots
  • The director’s methods return the final constructed robot objects.
# Define the RobotDirector class with methods to create different robots
class RobotDirector:
    def __init__(self):
        self.builder = None

    def set_builder(self, builder):
        self.builder = builder

    def build_humanoid_robot(self):
        self.builder.reset()
        self.builder.build_head()
        self.builder.build_arms()
        self.builder.build_legs()
        self.builder.build_torso()
        self.builder.build_battery()
        return self.builder.get_robot()

    def build_drone_robot(self):
        self.builder.reset()
        self.builder.build_head()
        self.builder.build_torso()
        self.builder.build_battery()
        return self.builder.get_robot()

5. Client Code:

  • Let’s create a RobotDirector instance.
  • Then create a HumanoidRobotBuilder and set it as the active builder for humanoid robots.
  • We also use the director’s build_humanoid_robot method to create a humanoid robot.
  • Now we can create a DroneRobotBuilder and set it as the active builder for drone robots.
  • Now we have to use the director’s build_drone_robot method to create a drone robot.
  • Finally, we print out the components of both types of robots.
# Client code
if __name__ == "__main__":
    director = RobotDirector()

    humanoid_builder = HumanoidRobotBuilder()
    director.set_builder(humanoid_builder)
    humanoid_robot = director.build_humanoid_robot()

    drone_builder = DroneRobotBuilder()
    director.set_builder(drone_builder)
    drone_robot = director.build_drone_robot()

    print("Humanoid Robot Components:")
    print(f"Head: {humanoid_robot.head}")
    print(f"Arms: {humanoid_robot.arms}")
    print(f"Legs: {humanoid_robot.legs}")
    print(f"Torso: {humanoid_robot.torso}")
    print(f"Battery: {humanoid_robot.battery}")

    print("nDrone Robot Components:")
    print(f"Head: {drone_robot.head}")
    print(f"Arms: {drone_robot.arms}")
    print(f"Legs: {drone_robot.legs}")
    print(f"Torso: {drone_robot.torso}")
    print(f"Battery: {drone_robot.battery}")

That’s it! 😊

Final Thoughts

The Builder pattern decouples the construction of a complex object from its representation. As we have seen in this example, the RobotDirector is in charge of orchestrating the construction process but is not aware of the specific steps to create different types of robots. The concrete builders, which we called HumanoidRobotBuilder and DroneRobotBuilder in this case, provide step-by-step implementations for building specific robot configurations.

The pattern allows flexibility and extensibility, enabling the creation of complex objects with variable attributes while keeping client code clean and easy to use. All this allows us to build complex objects in a clear and consistent way.

If you were interested in this article follow me on Medium! 😁

💼 Linkedin ️| 🐦 Twitter | 💻 Website


You might be interested in some of my past articles as well:


Related Articles