6. Creational Patterns
Creational pattern provides various object creation mechanisms, which increase flexibility and reuse of existing code.
FactoryMethod Design Pattern - ClassBuilderDesign PatternSingletonDesign PatternPrototypeDesign PatternAbstract FactoryDesign 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 couplingbetween 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
StringBuilderin 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 implementationof 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
Easyto implement- Guarantees
1 instance - Solves a well-defined problem
- Cons
- Used with parameters and
confused with Factory - Hard to write unit test
Thread safetyhas 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()