📝 Topics Covered
- 1. What are Creational Patterns?
- 2. Factory Method Design Pattern
- 3. Builder Design Pattern
- 4. Singleton Design Pattern
- 5. Prototype Design Pattern
- 6. Abstract Factory Design Pattern
1. What are Creational Patterns?
Creational design patterns deal with object creation mechanisms. They increase flexibility and reuse of existing code by abstracting the instantiation process. Rather than instantiating concrete classes directly, these patterns isolate how objects are created, composed, and represented.
We will cover the five core Creational patterns in this guide:
- Factory Method: Defer instantiation logic to subclasses.
- Builder: Construct complex objects step-by-step.
- Singleton: Restrict class instantiation to a single shared instance.
- Prototype: Clone fully configured objects to avoid expensive instantiation.
- Abstract Factory: Create families of related or dependent objects.
2. Factory Method Design Pattern
Factory design pattern is a creational design pattern that separates the logic of creating objects from the client code. The factory class is responsible for creating objects based on client parameters, shielding the client from concrete instantiation details.
🎥 Tutorial: Factory Method Pattern
- Key Takeaways:
- Avoids Tight Coupling: Eliminates direct dependencies between the creator class and concrete classes.
- Open-Closed Principle: You can introduce new subclasses without breaking existing client code.
- Shared Abstraction: The factory exposes a common shared type or interface, allowing high-level polymorphism.
🎥 Real-World Factory Pattern Use Cases
2.1 Factory Example 1: Vehicle Production
The Tightly-Coupled Way (Violates OCP):
If we want to introduce a new Truck vehicle, the client must modify their nested conditional blocks directly.
# Interface
class Vehicle:
def create_vehicle(self):
pass
class Car(Vehicle):
def create_vehicle(self):
print('Car created')
class Bike(Vehicle):
def create_vehicle(self):
print("Bike created")
# Client code has to change every time a new class is introduced
if __name__ == '__main__':
vehicle_type = 'car'
if vehicle_type == 'car':
car = Car()
car.create_vehicle()
elif vehicle_type == 'bike':
bike = Bike()
bike.create_vehicle()
The Clean Factory Way (Adheres to OCP):
By introducing a VehicleFactory, the instantiation details are encapsulated entirely.
class Vehicle:
def create_vehicle(self):
pass
class Car(Vehicle):
def create_vehicle(self):
print('Car created')
class Bike(Vehicle):
def create_vehicle(self):
print("Bike created")
class VehicleFactory:
@staticmethod
def get_vehicle(vehicle_type):
if vehicle_type == 'car':
return Car()
elif vehicle_type == 'bike':
return Bike()
raise ValueError(f"Unknown vehicle type: {vehicle_type}")
# Usage
if __name__ == '__main__':
vehicle = VehicleFactory.get_vehicle('bike')
vehicle.create_vehicle()
2.2 Factory Example 2: Milkshake Factory
from enum import Enum
class MilkshakeName(Enum):
OREO = 0
BUTTERSCOTCH = 1
VANILLA = 2
class Milkshake:
def __init__(self):
self.name = None
class ButterscotchMilkshake(Milkshake):
def __init__(self):
super().__init__()
self.name = "Butterscotch Milkshake"
print(f"Prepared: {self.name}")
class VanillaMilkshake(Milkshake):
def __init__(self):
super().__init__()
self.name = 'Vanilla Milkshake'
print(f"Prepared: {self.name}")
class OreoMilkshake(Milkshake):
def __init__(self):
super().__init__()
self.name = "Oreo Milkshake"
print(f"Prepared: {self.name}")
class MilkshakeFactory:
@staticmethod
def create_milkshake(milkshake_code):
if milkshake_code == MilkshakeName.OREO:
return OreoMilkshake()
elif milkshake_code == MilkshakeName.BUTTERSCOTCH:
return ButterscotchMilkshake()
elif milkshake_code == MilkshakeName.VANILLA:
return VanillaMilkshake()
raise ValueError("Invalid Milkshake code")
# Usage
if __name__ == '__main__':
MilkshakeFactory.create_milkshake(MilkshakeName.BUTTERSCOTCH)
3. Builder Design Pattern
The Builder design pattern is used to construct complex objects step-by-step. Whenever we need to create complex objects with extensive configurations, Builder is the ideal pattern.
🎥 Tutorial: Builder Design Pattern
- Key Takeaways:
- Combats Telescoping Constructors: Gets rid of huge constructors taking massive lists of parameters.
- Separation of Concerns: Encapsulates construction logic away from final product representations.
- Fluent API Interface: Supports method chaining (e.g.
StringBuilderin Java) for clean, readable code.
3.1 Builder Example 1: Customer Profile Creation
Without Builder (Cluttered methods for every optional parameter combination):
class Customer:
def __init__(self):
self.f_name = None
self.m_name = None
self.l_name = None
def set_name_full(self, f_name, m_name, l_name):
self.f_name = f_name
self.m_name = m_name
self.l_name = l_name
def set_name_short(self, f_name, l_name):
self.f_name = f_name
self.l_name = l_name
def get_name(self):
print(f'{self.f_name} {self.m_name or ""} {self.l_name}')
With Fluent Builder:
class Customer:
def __init__(self):
self.builder = CustomerBuilder()
def first_name(self, first):
self.builder.parts.append(first)
return self
def middle_name(self, middle):
self.builder.parts.append(middle)
return self
def last_name(self, last):
self.builder.parts.append(last)
return self
def build(self):
print(' '.join(self.builder.parts))
class CustomerBuilder:
def __init__(self):
self.parts = []
# Usage
if __name__ == '__main__':
Customer().first_name('Amrit').middle_name('Kr').last_name('Prasad').build()
3.2 Builder Example 2: Complex Multi-Builder Person Profile
Using dedicated child builders (Information, Address, Job) operating on the same underlying object:
class Person:
def __init__(self):
self.name = None
self.city = None
self.company_name = None
def __str__(self) -> str:
return f'Name: {self.name}, Address: {self.city}, Employed at: {self.company_name}'
class PersonBuilder:
def __init__(self, person=None):
self.person = Person() if person is None else person
@property
def info(self):
return PersonInfoBuilder(self.person)
@property
def lives(self):
return PersonAddressBuilder(self.person)
@property
def works(self):
return PersonJobBuilder(self.person)
def build(self):
return self.person
class PersonInfoBuilder(PersonBuilder):
def name(self, name):
self.person.name = name
return self
class PersonJobBuilder(PersonBuilder):
def at(self, company_name):
self.person.company_name = company_name
return self
class PersonAddressBuilder(PersonBuilder):
def in_city(self, city):
self.person.city = city
return self
# Usage
if __name__ == '__main__':
person = PersonBuilder()\
.info.name('Amrit')\
.lives.in_city('London')\
.works.at('Fabrikam')\
.build()
print(person)
4. Singleton Design Pattern
The Singleton pattern ensures that a class has only one instance and provides a global access point to it. This pattern reduces memory usage by sharing a single connection or manager globally.
- Typical Use Cases:
- Shared Database Connections.
- Application Loggers.
- Shared Config Services.
🎥 Tutorial: Singleton Design Pattern
- Loading Approaches:
- Eager Loading: The instance is initialized as soon as the application launches.
- Lazy Loading: The instance is only initialized when a module explicitly requests it.
- Trade-Offs:
- Pros: Guaranteed single instance, structured access to a shared resource, and straightforward implementation.
- Cons: Harder to unit-test (introduces global state), and can introduce synchronization issues in multi-threaded contexts if not handled correctly.
4.1 Singleton via Static Methods (Basic)
class LoggerService:
logger = None
@staticmethod
def get_logger():
if LoggerService.logger is None:
LoggerService.logger = []
print('Initialized Logger Array')
return LoggerService.logger
@staticmethod
def log(message):
LoggerService.get_logger().append(message)
print(f"Log: {LoggerService.logger}")
# Usage
if __name__ == "__main__":
logger = LoggerService()
logger.log('message-1')
LoggerService.log('message-2')
4.2 Singleton using new (Allocator Pattern)
class Database:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Database, cls).__new__(cls, *args, **kwargs)
print('Loading database from file...')
return cls._instance
# Usage
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(f"Are instances identical? {d1 is d2}")
4.3 Singleton via Class Decorators
def singleton(class_):
instances = {}
def get_instance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return get_instance
@singleton
class Database:
def __init__(self):
print('Loading database...')
# Usage
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(f"Are decorator instances identical? {d1 is d2}")
4.4 Singleton via Metaclasses
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=Singleton):
def __init__(self):
print('Loading database via metaclass...')
# Usage
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(f"Are metaclass instances identical? {d1 is d2}")
5. Prototype Design Pattern
Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their concrete classes. Instead of constructing complicated objects from scratch, we reiterate and clone existing designs.
- Key Takeaway: An existing fully configured object acts as a Prototype. We make deep copies (clones) of this prototype and customize the clone’s properties.
5.1 Prototype via Deep Copy
import copy
class Address:
def __init__(self, street_address, city, country):
self.country = country
self.city = city
self.street_address = street_address
def __str__(self):
return f'{self.street_address}, {self.city}, {self.country}'
class Person:
def __init__(self, name, address):
self.name = name
self.address = address
def __str__(self):
return f'{self.name} lives at {self.address}'
if __name__ == '__main__':
john = Person("John", Address("123 London Road", "London", "UK"))
print(john)
# Cloning the prototype
jane = copy.deepcopy(john)
jane.name = "Jane"
jane.address.street_address = "124 xyz Road"
print(john)
print(jane)
5.2 Combining Prototype with a Factory
import copy
class Address:
def __init__(self, street_address, city, country):
self.country = country
self.city = city
self.street_address = street_address
def __str__(self):
return f'{self.street_address}, {self.city}, {self.country}'
class Person:
def __init__(self, name, address):
self.name = name
self.address = address
def __str__(self):
return f'{self.name} lives at {self.address}'
class PersonFactory:
prototype_person = Person("Prototype", Address("", "London", "UK"))
@staticmethod
def create_london_employee(name, street):
employee = copy.deepcopy(PersonFactory.prototype_person)
employee.name = name
employee.address.street_address = street
return employee
# Usage
if __name__ == '__main__':
john = PersonFactory.create_london_employee('John', '123 London Road')
ap = PersonFactory.create_london_employee('AP', '124 xyz Road')
print(john)
print(ap)
6. Abstract Factory Design Pattern
Abstract Factory is an abstraction layer over the Factory design pattern. It allows us to create families of related objects without specifying their concrete classes. It is essentially a factory of factories.
- Analogy: While a single Factory creates products (e.g. Tata Cars), an Abstract Industry contains multiple Factories (e.g. Tata Factory, Hyundai Factory) that manufacture products.
🎥 Tutorial: Abstract Factory Design Pattern
6.1 Abstract Factory Example 1: Vehicle Brands
class Car:
def drive(self):
pass
class HyundaiCar(Car):
def drive(self):
print('Driving a Hyundai Car')
class TataCar(Car):
def drive(self):
print('Driving a Tata Car')
class Bike:
def ride(self):
pass
class HyundaiBike(Bike):
def ride(self):
print('Riding a Hyundai Bike')
class TataBike(Bike):
def ride(self):
print('Riding a Tata Bike')
# Concrete Factories
class TataFactory:
@staticmethod
def get_car():
return TataCar()
@staticmethod
def get_bike():
return TataBike()
class HyundaiFactory:
@staticmethod
def get_car():
return HyundaiCar()
@staticmethod
def get_bike():
return HyundaiBike()
# The Abstract Factory (Factory of Factories)
class VehicleBrandFactory:
@staticmethod
def get_factory(brand):
if brand.lower() == 'tata':
return TataFactory()
elif brand.lower() == 'hyundai':
return HyundaiFactory()
raise ValueError(f"Unknown brand factory: {brand}")
# Usage
if __name__ == '__main__':
factory = VehicleBrandFactory.get_factory('hyundai')
car = factory.get_car()
car.drive()
6.2 Abstract Factory Example 2: Hot Drinks
from abc import ABC, abstractmethod
# Product Hierarchies
class HotDrink(ABC):
@abstractmethod
def consume(self):
pass
class Tea(HotDrink):
def consume(self):
print("This tea is nice but I'd prefer it with milk.")
class Coffee(HotDrink):
def consume(self):
print("This coffee is delicious.")
# Abstract Factory Interface
class HotDrinkFactory(ABC):
@abstractmethod
def prepare(self, amount):
pass
# Concrete Factories
class TeaFactory(HotDrinkFactory):
def prepare(self, amount):
print(f"Steeping tea bag, pouring {amount}ml of boiling water...")
return Tea()
class CoffeeFactory(HotDrinkFactory):
def prepare(self, amount):
print(f"Grinding coffee beans, pouring {amount}ml of boiling water...")
return Coffee()
# Client Driver
def make_drink(drink_type):
if drink_type.lower() == 'tea':
return TeaFactory().prepare(200)
elif drink_type.lower() == 'coffee':
return CoffeeFactory().prepare(50)
return None
if __name__ == '__main__':
drink = make_drink('tea')
if drink:
drink.consume()