6. Creational Patterns
Creational pattern provides various object creation mechanisms, which increase flexibility and reuse of existing code
.
Factory
Method Design Pattern - ClassBuilder
Design PatternSingleton
Design PatternPrototype
Design PatternAbstract Factory
Design Pattern
6.1 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 in the factory design pattern is responsible for creating objects based on the request from the client.
.
- Take Away
Avoid tight coupling
between creator and the concrete implementation- Can be extended to Abstract Factory patter
- Factory only exposes a common shared type - perfect for the Open-Close Principle
Example - 1
# Interface
class Vehicle:
def create_vehicle(self):
pass
class Car(Vehicle):
def create_vehicle(self):
print('Car')
class Bike(Vehicle):
def create_vehicle(self):
print("Bike")
# Adding more - Truck
class Truck(Vehicle):
def create_vehicle(self):
print("Truck")
# For this client has to make changes in client code (if-else statement)
# This is wrong way to add more feature - its very tightly-coupled
# For Client Usage
if __name__ == '__main__':
vehicle_type = 'car'
if vehicle_type == 'car':
car = Car()
car.create_vehicle()
elif vehicle_type == 'bike':
bike = Bike()
bike.create_vehicle()
Implementation is hidden
and client don’t have to make any changes for adding new feature
# Interface
class Vehicle:
def create_vehicle(self):
pass
class Car(Vehicle):
def create_vehicle(self):
print('Car')
class Bike(Vehicle):
def create_vehicle(self):
print("Bike")
class VehicleFactory:
@staticmethod
def get_vehicle(vehicle_type):
if vehicle_type == 'car':
car = Car()
car.create_vehicle()
elif vehicle_type == 'bike':
bike = Bike()
bike.create_vehicle()
# For Client Usage
if __name__ == '__main__':
VehicleFactory.get_vehicle('bike')
Example - 2
from enum import Enum
class MilkshakeName(Enum):
OreoMilkshake = 0
ButterscotchMilkshake = 1
VanillaMilkshake = 2
class Milkshake:
name = None
pass
class ButterscotchMilkshake(Milkshake):
def __init__(self):
self.name = "Butterscotch Milkshake"
print(self.name)
class VanillaMilkshake(Milkshake):
def __init__(self):
self.name = 'Vanilla Milkshake'
print(self.name)
class OreoMilkshake(Milkshake):
def __init__(self):
self.name = "Oreo Milkshake"
print(self.name)
# For Client Usage
# if __name__ == '__main__':
# milkshake_code = 0
# if milkshake_code == 0:
# OreoMilkshake()
# elif milkshake_code == 1:
# ButterscotchMilkshake()
# elif milkshake_code == 2:
# VanillaMilkshake()
class MilkshakeFactory:
def __init__(self, milkshake_code):
# eval(milkshake.name)()
if milkshake_code == 0:
OreoMilkshake()
elif milkshake_code == 1:
ButterscotchMilkshake()
elif milkshake_code == 2:
VanillaMilkshake()
# For Client Usage
if __name__ == '__main__':
MilkshakeFactory(1)
6.2 Builder Design Pattern
Builder design pattern is a creational design pattern that allows us to construct an object step-by-step.
Whenever we need to create complex object with has lots of configuartion
in it then we use builder design pattern.
It uses a builder class that contains the construction steps to create an object.
.
- Take Away
- Perfect to get rid of
big constructors
, which take a huge amount of parameter - Encapsulate code for construction and representation
- The builder needs “access” to the internal representation and creation
StringBuilder
in Java is a famous example for the pattern
- Perfect to get rid of
Example - 1
Without builder pattern
, we may need to create various function to set the variables
class Customer:
f_name = None
m_name = None
l_name = None
def set_name1(self, f_name, m_name, l_name):
self.f_name = f_name
self.m_name = m_name
self.l_name = l_name
def set_name2(self, f_name, l_name):
self.f_name = f_name
self.l_name = l_name
def get_name(self):
print('{0} {1} {2}'.format(self.f_name, self.m_name, self.l_name ))
cust1 = Customer()
cust1.set_name1('Amrit', 'Kr', 'Prasad')
cust1.get_name()
cust2 = Customer()
cust2.set_name2('Amrit', 'Prasad')
cust2.get_name()
Simple implementation
of Builder Design pattern
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):
part_list = self.builder.parts
print(' '.join(part_list))
class CustomerBuilder:
def __init__(self):
self.parts = []
# Client
if __name__ == '__main__':
Customer().first_name('Amrit').middle_name('Kr').last_name('Prasad').build()
Example - 2
class Person:
def __init__(self):
self.name = None
# self.street_address = None
# self.postcode = None
self.city = None
self.company_name = None
# self.position = None
# self.annual_income = 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):
if person is None:
self.person = Person()
else:
self.person = 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
if __name__ == '__main__':
person1 = PersonBuilder().lives.in_city('London').works.at('Fabrikam').build()
print(person1)
person1 = PersonBuilder().info.name('Amrit').lives.in_city(
'London').works.at('Fabrikam').build()
print(person1)
6.3 Singleton Design Pattern
Singleton design pattern is a creational design pattern that makes sure that a class has only one instance
and is globally accessible by all other classes.
Singleton design pattern reduces memory usage
by sharing a single instance across the application.
- Usage
- Single DB connection
- Single Logger Service
- Eager Loading
- The instance is already initialized as soon as the application is up
- Lazy Loading
- The instance is initialized only when any App module calls for it.
- Pros
- Neat way to handle access to shared global resource
Easy
to implement- Guarantees
1 instance
- Solves a well-defined problem
- Cons
- Used with parameters and
confused with Factory
- Hard to write unit test
Thread safety
has to be insured else can be dangerous
- Used with parameters and
By using @staticmethod
Simple but not proper Singleton design - By using @staticmethod
class LoggerService:
logger = None
@staticmethod
def get_logger():
if LoggerService.logger is None:
LoggerService.logger = []
print('New')
@staticmethod
def log(message):
LoggerService.logger.append(message)
print(LoggerService.logger)
if __name__ == "__main__":
logger = LoggerService()
logger.get_logger()
logger.log('message-1')
LoggerService.get_logger()
LoggerService.log('message-2')
Singleton Allocator
__init__
is called on each db creation
class Database:
_instance = None
def __init__(self):
print('Loading database from file')
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Database, cls).__new__(cls, *args, **kwargs)
return cls._instance
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(d1 == d2)
Singleton Decorator
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')
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(d1 == d2)
Singleton Metaclass
Alternative to Singleton Decorator
class Singleton(type):
""" Metaclass that creates a Singleton base type when called. """
_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')
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(d1 == d2
6.4 Prototype Design Pattern
Prototype is a creational design pattern that lets you copy existing objects
without making your code dependent on their classes.
Complicated objects (e.g. Cars) aren’t designed from scratch they reiterate existing designs
.
An existing (partially or fully constructed) design is a Prototype. We make a copy (clone) the prototype and customize it.
# Prototype Design Pattern
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)
jane = copy.deepcopy(john)
jane.name = "Jane"
jane.address.street_address = "124 xyz Road"
print(john)
print(jane)
# Prototype and Factory Design Pattern
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:
person = None
def __init__(self, name, street):
if not PersonFactory.person:
PersonFactory.person = Person(name, Address(street, "London", "UK"))
print(PersonFactory.person)
else:
person2 = copy.deepcopy(PersonFactory.person)
person2.name = name
person2.address.street_address = street
print(person2)
if __name__ == '__main__':
PersonFactory('John', '123 London Road')
PersonFactory('AP', '124 xyz Road')
6.5 Abstract Factory Design Pattern
Abstract Factory Design Pattern, as the name suggests is an abstraction over Factory design pattern
.
It is one of the creational design patterns.
As a factory pattern allows us to create a generic factory of one or more than one type of object,
extending the same behavior abstract factory design pattern allows us to create a factory of factories
,
one level above the abstraction in the factory design pattern.
Consider a real-life analogy, just like a factory can create products or objects, similarly, an industry can create multiple factories.
So industry
can be understood as the abstract factory pattern
, and a single factory
can be understood as the factory pattern
.
Example - 1
# Interface
class Vehicle:
def create_vehicle(self):
pass
class Car(Vehicle):
def create_vehicle(self):
print("Car")
class Bike(Vehicle):
def create_vehicle(self):
print("Bike")
class HyundaiCar(Car):
def create_vehicle(self):
print('Hyundai Car')
class TataCar(Car):
def create_vehicle(self):
print('Tata Car')
class TataFactory:
@staticmethod
def get_vehicle(vehicle_type):
if vehicle_type == 'car':
car = TataCar()
car.create_vehicle()
elif vehicle_type == 'bike':
print('Tata Bike')
class HyundaiFactory:
@staticmethod
def get_vehicle(vehicle_type):
if vehicle_type == 'car':
car = HyundaiCar()
car.create_vehicle()
elif vehicle_type == 'bike':
print('Hyundai Bike')
class AbstractVehicleFactory:
@staticmethod
def get_vehicle(vehicle):
if 'hyundai' in vehicle:
HyundaiFactory.get_vehicle(vehicle.split("_")[1])
elif 'tata' in vehicle:
TataFactory.get_vehicle(vehicle.split("_")[1])
# For Client Usage
if __name__ == '__main__':
AbstractVehicleFactory.get_vehicle('hyundai_car')
AbstractVehicleFactory.get_vehicle('hyundai_bike')
AbstractVehicleFactory.get_vehicle('tata_car')
Example - 2
from abc import ABC
class HotDrink(ABC):
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')
# Industry
class HotDrinkFactory(ABC):
def prepare(self, amount):
pass
class TeaFactory(HotDrinkFactory):
def prepare(self, amount):
print(f'Put in tea bag, boil water, pour {amount}ml, enjoy!')
return Tea()
class CoffeeFactory(HotDrinkFactory):
def prepare(self, amount):
print(f'Grind some beans, boil water, pour {amount}ml, enjoy!')
return Coffee()
def make_drink(type):
if type == 'tea':
return TeaFactory().prepare(200)
elif type == 'coffee':
return CoffeeFactory().prepare(50)
else:
return None
if __name__ == '__main__':
entry = input('What kind of drink would you like?')
drink = make_drink(entry)
drink.consume()