Creational patterns
Table of contents
- Builder
- Factories
- Prototype
- Singleton
- Monostate (Singleton variation)
- Lazy Initialization
- Dependency Injection
Creational patterns deal with the construction of objects. An object can be created wholesale (in a single statement) or piecewise (step by step).
Builder
When an object takes long to initialize, provide an API for constructing it succinctly. Separate the construction of a complex object from its representation, allowing the same construction process to create various representations. A Builder is a separate component for building an object.
class Pizza:
def __init__(self):
self.size = None
self.cheese = None
self.pepperoni = False
self.mushrooms = False
self.extra_toppings = []
def __str__(self):
return f"Pizza(Size: {self.size}, Cheese: {self.cheese}, Pepperoni: {self.pepperoni}, Mushrooms: {self.mushrooms}, Extra Toppings: {self.extra_toppings})"
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_size(self, size):
self.pizza.size = size
return self # returning the class object itself to perform chaining (see the explanation below)
def add_cheese(self, cheese_type):
self.pizza.cheese = cheese_type
return self
def add_pepperoni(self):
self.pizza.pepperoni = True
return self
def add_mushrooms(self):
self.pizza.mushrooms = True
return self
def add_topping(self, topping):
self.pizza.extra_toppings.append(topping)
return self
def build(self):
return self.pizza
builder = PizzaBuilder()
custom_pizza = builder.set_size("Large").add_cheese("Cheddar").add_pepperoni().add_topping("Olives").build()
print(custom_pizza)
veggie_pizza = builder.set_size("Medium").add_cheese("Mozzarella").add_mushrooms().add_topping("Bell Peppers").add_topping("Onions").build()
print(veggie_pizza)
Chaining
Because of chaining, methods can be executed one after another in the same statement, as the class object is returned, and then be used to call the next method or modify attributes directly.
class Box:
def __init__(self, length, width):
self.length = length
self.width = width
def set_dimensions(self, length, width):
self.length = length
self.width = width
return self # returning the class object itself
box = Box(5, 10)
box.set_dimensions(7, 14).length = 8
print(box.length, box.width)
Builder facets
A Builder facet is a specialized component of a Builder (a separate class) that focuses on constructing a specific aspect or "facet" of an object, e.g., personal details, job details, or address details. These facets work together to build the complete object, e.g., a user, while maintaining a fluent interface (an interface supporting method chaining).
class Person:
def __init__(self):
self.name = None
self.age = None
# Job details
self.job_title = None
self.company = None
self.salary = None
# Address details
self.street = None
self.city = None
self.postal_code = None
def __str__(self):
return f"Person(Name: {self.name}, Age: {self.age}, Job: {self.job_title} at {self.company} (${self.salary}))"
# The main Builder class to manage all facets
class PersonBuilder:
def __init__(self, person=None):
if person is None:
self.person = Person()
else:
self.person = person
@property
def personal(self):
return PersonPersonalBuilder(self.person)
@property
def job(self):
return PersonJobBuilder(self.person)
def build(self):
return self.person
# Builder facet for personal details
class PersonPersonalBuilder(PersonBuilder):
def __init__(self, parent_builder):
super().__init__(parent_builder)
def set_name(self, name):
self.person.name = name
return self
def set_age(self, age):
self.person.age = age
return self
# Builder facet for job details
class PersonJobBuilder(PersonBuilder):
def __init__(self, parent_builder):
super().__init__(parent_builder)
def set_job_title(self, job_title):
self.person.job_title = job_title
return self
def set_company(self, company):
self.person.company = company
return self
def set_salary(self, salary):
self.person.salary = salary
return self
builder = PersonBuilder()
person = builder.personal.set_name("John Doe").set_age(30).job.set_job_title("Software Engineer").set_company("Tech Corp").set_salary(120000).build()
print(person)
Builder inheritance
Builder facets violate the Open/Closed Principle because every time we create a new sub-builder, we have to add it to the main Builder. Thanks to inheritance, every Builder in the example below is open for extension but closed for modification.
class Person:
def __init__(self):
self.name = None
self.job_title = None
self.salary = None
def __str__(self):
return f"{self.name} works as {self.job_title}, earns {self.salary}"
@staticmethod
def new():
return PersonBuilder()
class PersonBuilder:
def __init__(self):
self.person = Person()
def build(self):
return self.person
class PersonPersonalBuilder(PersonBuilder):
def called(self, name):
self.person.name = name
return self
class PersonJobBuilder(PersonPersonalBuilder):
def job(self, job_title):
self.person.job_title = job_title
return self
class PersonSalaryBuilder(PersonJobBuilder):
def earns(self, salary):
self.person.salary = salary
return self
builder = PersonSalaryBuilder()
person = builder.called("Josh").job("Engineer").earns("100000").build()
print(person)
Factories
Factory Method
A Factory Method is an alternate constructor. It provides an additional way to create class instances, often with a more descriptive name or specialized logic, while keeping the main __init__()
constructor simple. Factory Method allows subclasses to define the type of object to be created, enabling customization of object creation without modifying the core logic. For example, instead of the user's age, we can take his date of birth and then calculate the age. They can be created using the @classmethod
(like shown here) or @staticmethod
decorator. A class can have multiple alternate constructors.
class Person:
def __init__(self, name, age):
self.__name = name
self.__age = age
def __str__(self):
return f"{self.__name}, {self.__age}"
@staticmethod
def from_age(name, age):
return Person(name, age)
@staticmethod
def from_birth_year(name, birth_year):
return Person(name, 2024 - birth_year)
e = Person.from_age("Josh", 25)
e2 = Person.from_birth_year("John", 1980)
print(e)
print(e2)
The example above doesn't adhere to the SRP. We should separate the Factory Methods into a different class and use them from there.
class PersonFactory:
@staticmethod
def from_age(name, age):
return Person(name, age)
@staticmethod
def from_birth_year(name, birth_year):
return Person(name, 2024 - birth_year)
class Person:
factory = PersonFactory() # this is optional (for convenience)
def __init__(self, name, age):
self.__name = name
self.__age = age
def __str__(self):
return f"{self.__name}, {self.__age}"
e = Person.factory.from_age("Josh", 25)
e2 = Person.factory.from_birth_year("John", 1980)
print(e)
print(e2)
Abstract Factory
An Abstract Factory defines how Factories should be implemented (like an abstract class defines how classes should be implemented). Hierarchies of Factories can be used to create related objects.
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def drive(self):
pass
class Car(Vehicle):
def drive(self):
return "Driving a car"
class Bike(Vehicle):
def drive(self):
return "Riding a bike"
class VehicleFactory(ABC): # an Abstract Factory
@abstractmethod
def create_vehicle(self):
pass
class CarFactory(VehicleFactory): # a concrete Factory
def create_vehicle(self):
return Car()
class BikeFactory(VehicleFactory):
def create_vehicle(self):
return Bike()
def vehicle_client(factory: VehicleFactory):
vehicle = factory.create_vehicle()
print(vehicle.drive())
vehicle_client(CarFactory())
vehicle_client(BikeFactory())
Prototype
Prototype allows object cloning instead of creating new instances from scratch, improving efficiency and flexibility. Sometimes it is more efficient to clone an object and modify it, instead of initializing a new one. A Prototype is a partially or fully initialized object that we can copy (clone) and modify.
import copy
class Car:
def __init__(self, model, color, engine):
self.model = model
self.color = color
self.engine = engine
def clone(self):
return copy.deepcopy(self) # creating a deep copy of the class object
def __str__(self):
return f"{self.color} {self.model} with {self.engine} engine"
car1 = Car("Tesla Model S", "Red", "Electric") # creating an original object
car2 = car1.clone() # cloning the object
car2.color = "Blue" # modifying the cloned object
print(car1)
print(car2)
Prototype Factory
Factories can provide a convenient API for using Prototypes.
import copy
class Car:
def __init__(self, model, color, engine):
self.model = model
self.color = color
self.engine = engine
def clone(self):
return copy.deepcopy(self) # creating a deep copy of the class object
def __str__(self):
return f"{self.color} {self.model} with {self.engine} engine"
class CarFactory:
prototypes = {}
@classmethod
def register_prototype(cls, key, engine):
cls.prototypes[key] = Car("", "", engine)
@classmethod
def new_car(cls, key, model, color):
prototype = cls.prototypes.get(key)
if prototype:
car = prototype.clone()
car.model = model
car.color = color
return car
raise ValueError("Prototype not found!")
CarFactory.register_prototype("electric", "Electric")
CarFactory.register_prototype("combustion", "V8")
car1 = CarFactory.new_car("electric", "Tesla Model S", "Blue")
car2 = CarFactory.new_car("combustion", "Ford Mustang", "White")
print(car1)
print(car2)
Singleton
Singleton ensures a class has only one instance and provides a global point of access to it. Sometimes, it doesn't make sense to have more than one object in the system, e.g., a database or an object Factory, because the initializer is expensive or is just unnecessary. A Singleton is a component that is instantiated only once (we want to prevent anyone from creating additional copies).
class Database:
_instance = None # a class-level attribute to store the single instance
def __new__(cls, *args, **kwargs):
if cls._instance is None: # checking if an instance already exists
cls._instance = super().__new__(cls) # creating and storing the single instance
return cls._instance
def get_data(self):
print("Loading data")
s1 = Database()
s2 = Database()
s1.get_data()
s2.get_data()
print(s1 is s2) # these two instances are really the same instance
The class in the example above can only have one instance, but if we add an __init__()
method to it, it will execute two times. This can be fixed with a decorator or a metaclass (they can be used to enforce a true Singleton).
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs) # creating and storing the instance
return instances[cls] # returning the existing instance
return get_instance # returning the wrapper function
@singleton
class Database:
def __init__(self):
print("Loading database")
s1 = Database()
s2 = Database()
print(s1 is s2)
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs) # creating and storing the instance
return cls._instances[cls] # returning the existing instance
class Database(metaclass = SingletonMeta):
def __init__(self):
print("Loading database")
s1 = Database()
s2 = Database()
print(s1 is s2)
Singletons ensure a single instance of a class, which is useful for managing shared resources like database connections or logging systems, preventing redundant object creation. However, they can introduce hidden dependencies and make unit testing difficult due to the global state, leading to unexpected side effects (such as unintended data modifications, stale state across tests, and difficulties in resetting the state for different parts of an application). Additionally, they violate the Single Responsibility Principle (SRP) by controlling both their lifecycle and primary functionality, making the code harder to maintain and extend.
Monostate (Singleton variation)
Monostate allows multiple instances of a class to share the same state by making all instances point to a common dictionary (__dict__()
). Unlike the Singleton pattern (which ensures only one instance exists), Monostate allows multiple instances but guarantees they behave as if they are the same object in terms of state.
class Monostate:
_state = {}
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
instance.__dict__ = cls._state # all instances share the same state
return instance
def __init__(self, value):
self.value = value # all instances share the same "value" attribute
a = Monostate("Value A")
b = Monostate("Value B")
print(a.value)
print(b.value)
Lazy Initialization
Lazy Initialization delays the creation of an object or a resource until it is actually needed, rather than when the object is first created. This is useful for optimizing resource usage, especially when the initialization of an object is resource-intensive or unnecessary if the object is never used. The example below adheres to this design pattern because it ensures that the database connection and any associated resources are only initialized when the get_data()
method is first called, i.e., when the data is needed for the first time.
class LazyDatabase:
def __init__(self):
self._db_connection = None
def _initialize(self):
print("Initializing database connection...")
self._db_connection = "Database connection established"
def get_data(self):
if self._db_connection is None:
self._initialize() # initializing only when data is requested
print("Fetching data from database")
db = LazyDatabase()
db.get_data() # the database is initialized only when we call get_data()
db.get_data() # accessing the data again, no re-initialization occurs
Dependency Injection
Dependency Injection allows us to inject dependencies (such as services, objects, or configurations) into a class rather than having the class create them itself. This promotes loose coupling (meaning the class has less direct knowledge or control over the specific implementation details of its dependencies), making the code more modular, flexible, and easier to test. With DI, the class doesn't need to worry about how its dependencies are created or managed. Instead, they are provided externally (often by a framework or a container), which simplifies testing and maintenance.
class Database:
def connect(self):
print("Connecting to the database...")
class Service:
def __init__(self, database: Database):
self.database = database # injecting the dependency through the constructor
def execute(self):
print("Service is running...")
self.database.connect() # using the injected dependency
db = Database()
service = Service(db) # injecting the Database instance into the Service
service.execute()