Exploring Builder and Factory Design Patterns

Getting to Enterprise Level Code Development

Archit Pandita
Hashmap, an NTT DATA Company

--

Standardizing your code can define the quality of your code as a developer. Utilizing patterns is one of the common practices you can follow to bring your code to an enterprise-level. This is independent of any programming language.

Today we will take a step towards learning and implementing the 2 most common design patterns.

What are we covering?

  1. What is a design pattern and what are a few scenarios?
  2. Factory Pattern with an example
  3. Builder Pattern with an example
  4. Using a YAML file to control runtime flow and object creations (for builder and factory pattern)

Design Patterns

These are solutions to commonly occurring problems in software design. They help make the software more agile, flexible, and scalable.

We want to make components that are less tightly coupled. This ensures the ease of feature addition and better code design.

Application Scenario: You want to design a restaurant ordering software architecture where you have a separate class for different feature/product categories. You are currently in the early stages, but in the future, you are hoping to add more features to it. You may also want to personalize some features based on users. For example: for the user who likes pizza, you want to recommend more pizza, and for the burger lovers, you want to show different kinds of burgers.

Obviously, we will not design the whole architecture here but look at how Factory and Builder patterns come into the picture.

Here is another full-fledged Machine Learning Operations library by Hashmap utilizing the same techniques: Trainingopz(check out this library if you are interested in ML and DevOps — contains great content and information)

Real-Life Scenario: This scenario helps to visualize these patterns' strategy. Suppose you went to a restaurant and ordered a fried chicken. Once you have given the order, you have no idea about the person cooking your food. From your perspective, you only see the person who took your order. Several minutes later, your fried chicken is served to you.
Note that you did not access the information behind the kitchen doors. If the chef of the restaurant changes tomorrow, you will have no knowledge. Each time you order fried chicken, you receive fried chicken regardless of the person preparing it.

Like this scenario, if in the future your application needs some parts to be replaced or added, your frontend user may or may not experience the change. This makes it easier for a developer to modify the code.

Factory Pattern

This design pattern belongs to the creational design pattern where the class object is not instantiated directly but indirectly via Factory class. The factory class is responsible for object creation, and the logic is hidden from the client/frontend.

The Python library, providah by Hashmapinc, implements the factory for you. You must provide which class you want to create a factory. In fact, providah goes one step further and implements a service locator (not discussed here) that serves as a queryable factory with a global scope.

Suppose you create an online platform for a pizza shop with 2 varieties of pizza, cheese pizza, and onion pizza. The next day you want to add more variety to your pizza. It becomes chaotic for a developer to add more because the code is tightly coupled as below:

Without the factory pattern:

class CheesePizza:
@staticmethod
def make_pizza():
return "Cheese pizza"


class OnionPizza:
@staticmethod
def make_pizza():
return "Onion pizza"


if "__main__" == __name__:
cp = CheesePizza()
op = OnionPizza()

With the factory pattern in this code:

class CheesePizza:
@staticmethod
def make_pizza():
return "Cheese pizza"


class OnionPizza:
@staticmethod
def make_pizza():
return "Onion pizza"


def factory(ptype):
pizzas = {
"CheesePizza": CheesePizza,
"OnionPizza": OnionPizza
}
return pizzas[ptype]()


if "__main__" == __name__:
cp = factory("CheesePizza")
op = factory("OnionPizza")

Now, we have introduced the Factory method, which will work like any other factory by producing products. It will take an argument. Based on that argument it will return you the object of the class you required.
Thus, it’s a solution to replace the straight forward object construction calls with calls to the special factory method. Actually, there will be no difference in the object creation, but they are being called within the factory method.”

Why is it need? Before this, you have to write the object creation code on the client-side explicitly. You will now call Factory() with appropriate parameterization. If you have to add more products in the future, you will add them in the backend without touching the client-side code.

Builder Pattern

In Factory, we created a simple class object, but if the demand is to create a complex class object, you may have performed several object creation steps.
It has two parts:
1) build_steps(): these will be the steps to create an object
2) return_result() : this will return the object that is requested

This pattern also focuses on loose coupling and reusability of code, since we can use the same build_steps to create a different representation of the class.

Using our last pizza shop example, suppose we now have to write code for the process of making pizza:

class FoodProcessor:
# this is where you can order which item you want to cook and this responsible for calling the right step to do so
def __init__(self, arg):
self._pizza = BuildPizza(arg)
self._build_pizza = None

def build_pizza(self):
self._build_pizza = self._pizza.make_pizza()

def serve_pizza(self):
return self._build_pizza


class Pizza:
# This is pizza class where all the ingredients are available, so you can call differ method to modify the process and type of pizza
def __init__(self):
self._pizza = None
self.proceess = []

def add_sauce_to_base(self):
self.proceess.append("sauce")

def add_cheese(self):
self.proceess.append("cheese")

def microwave_pizza(self):
self.proceess.append("microwave")


class BuildPizza:
# this is like a kitchen and here we execute the steps to prepare the pizza as needed
def __init__(self, arg):
self._pizza_type = arg

def make_pizza(self):
pizza = Pizza()
if self._pizza_type == "cheese":
pizza.add_sauce_to_base()
pizza.add_cheese()
pizza.microwave_pizza()
return pizza


if "__main__" == __name__:
# this is our client side code
director = FoodProcessor("cheese")
director.build_pizza()
print(director.serve_pizza())

In the future, if we have to modify or add more steps to cook pizza, I can easily update that in the builder code. In updating the builder code, the rest of the client code will not get impacted.

How did we use the config or template to make code more flexible and reconfigurable?

Rather than explicitly mentioning or asking client code to pass an argument to build an object, we use a YAML file to be read by code to build the configuration. Thus, client code does not have to specify the parameters every time and use YAML (or JSON if you like). This gives you a much bigger picture of the application configuration and workings.

Let’s create a configuration file called process.yml

We first write down different processes in YAML. Here, it will be taking orders, then cooking the meal, and then packaging for delivery.

TakeOrder:
type: pizza
MakeOrder:
type: pizza
PackOrder:
type: pizza

components.py

Let's write a definition of the classes required.

class Product:  # Simple product class, can be use for pizza, burger             etc
def __init__(self, arg):
self.name = arg
self.order = False
self.make = False
self.pack = False

class TakeOrder: # this will set the order status of meal
def __init__(self, arg):
arg.order = True

class MakeOrder: # this will be set if meal is being prepared
def __init__(self, arg):
arg.make = True

class PackOrder: # this will set that meal is packed and done
def __init__(self, arg):
arg.pack = True

builder.py

This is responsible for building the complex class object. This is like an assembly line of products where each part of the product is getting enriched by the process mention in process.yml

from components import Product
from components import MakeOrder
from components import TakeOrder
from components import PackOrder
import yaml
import sys
from providah.factories.package_factory import PackageFactory as pf

class Builder:
def __init__(self):
self.build_obj = None
with open('process.yml', 'r') as stream:
self.process = yaml.safe_load(stream)

def build(self, arg):
product = Product(arg)

for key in self.process.keys():
"""
Here we are dynamically creating all the object which work as a obj enricher of our product
using classes mention as key in component.yml, Thus making less code change need in future

"""
# pf.register("MakeOrder", MakeOrder)
# pf.register("TakeOrder", TakeOrder)
# pf.register("PackOrder", PackOrder)
# below line is basically running above three lines dynamically
pf.register(str(key), getattr(sys.modules[__name__], key))

# using python Providah for factory https://pypi.org/project/providah/
# you can create your own factory and call it here
pf.create(key=key, configuration={"obj": product})

return product

main.py

This script will work as client-side code. You specified which food item you want, the rest of the building logic is hidden from the client. In the future, if we have to add a food item, all we need is to add that in component and process.yml.

from builder  import Builder
building = Builder()
result = building.build("pizza")
print(result.name, result.make, result.order, result.pack)

Summary

We went through a factory pattern to see how we can delegate the task of instantiating class to function. This pattern has only one task to do, give the required class object. Next, we discussed how the builder pattern is used to create a more complex class.

Builder and Factory patterns are very similar to each other. The main difference lies in the class's complexity. Builders have build steps and result method.

Finally, we discussed how to write the YAML file and our code that YAML took to build the required object.

It may seem like too much to do when you start building new applications but treat it as an investment, which will definitely give you good returns in the future.

Keep playing, keep learning :)

Ready to Accelerate Your Digital Transformation?

At Hashmap, we work with our clients to build better together.

If you’d like additional assistance in this area, Hashmap, an NTT DATA Company, offers a range of enablement workshops and consulting service packages as part of our consulting service offerings, and would be glad to work through your specifics. Reach out to us here.

Feel free to share on other channels, and be sure and keep up with all new content from Hashmap here. To listen in on a casual conversation about all things data engineering and the cloud, check out Hashmap’s podcast Hashmap on Tap as well on Spotify, Apple, Google, and other popular streaming apps.

Other Tools and Content For You

Archit Pandita is a Python developer and Data Engineer with Hashmap, an NTT DATA Company, providing Data, Cloud, IoT and AI/ML solutions and consulting expertise across industries with a group of innovative technologists and domain experts accelerating high-value business outcomes for our customers.
Have a question? Don’t hesitate to reach out to connect or exchange more information. I’m happy to help:
Archit’s LinkedIn

--

--