6. Creational Patterns

Creational pattern provides various object creation mechanisms, which increase flexibility and reuse of existing code.

  • Factory Method Design Pattern - Class
  • Builder Design Pattern
  • Singleton Design Pattern
  • Prototype Design Pattern
  • Abstract 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

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

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()