Logo

Part 1: Creational Design Patterns

3 min read
Lesson slides
1 / 14

Part 1: Creational Design Patterns

Singleton, Factory, and Builder

Purpose: Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

1.1 Singleton Pattern

Intuition: Ensures a class has only one instance and provides global access to it.

classDiagram
    class Singleton {
        -instance: Singleton
        +getInstance(): Singleton
        +operation()
    }
    
    note for Singleton "Only one instance exists"

✅ When to Use:

  • Database connection pools (expensive to create multiple)
  • Logging services (centralized logging)
  • Configuration managers (single source of truth)
  • Cache implementations (shared memory space)
  • Device drivers (hardware access control)

Code Example:

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance.connection = None
        return cls._instance
    
    def connect(self):
        if self.connection is None:
            self.connection = "Connected to Database"
            print("Database connection established")
        return self.connection
 
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True - Same instance

1.2 Factory Pattern

Intuition: Creates objects without specifying their exact class. Like a car factory that can produce different car models based on specifications.

classDiagram
    class AnimalFactory {
        +createAnimal(type: String): Animal
    }
    
    class Animal {
        <<interface>>
        +makeSound()
    }
    
    class Dog {
        +makeSound()
    }
    
    class Cat {
        +makeSound()
    }
    
    AnimalFactory --> Animal
    Dog ..|> Animal
    Cat ..|> Animal

✅ When to Use:

  • When you don't know which exact class to instantiate until runtime
  • When creation logic is complex and centralized
  • When you want to decouple object creation from usage
  • When you have a family of related products
  • When you need to enforce creation constraints

Code Example:

from abc import ABC, abstractmethod
 
class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass
 
class Dog(Animal):
    def make_sound(self):
        return "Woof!"
 
class Cat(Animal):
    def make_sound(self):
        return "Meow!"
 
class AnimalFactory:
    @staticmethod
    def create_animal(animal_type):
        if animal_type.lower() == "dog":
            return Dog()
        elif animal_type.lower() == "cat":
            return Cat()
        else:
            raise ValueError(f"Unknown animal type: {animal_type}")
 
# Usage
factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
print(dog.make_sound())  # Woof!
print(cat.make_sound())  # Meow!
  • Realworld Example : The Factory Pattern is often used in GUI frameworks where different button types (e.g., WindowsButton, MacButton) can be created based on the operating system.

1.3 Builder Pattern

Intuition: Constructs complex objects step by step. Like building a house where you can choose different options for rooms, materials, etc.

classDiagram
    class Pizza {
        +size: String
        +crust: String
        +toppings: List
    }
    
    class PizzaBuilder {
        -pizza: Pizza
        +setSize(size: String): PizzaBuilder
        +setCrust(crust: String): PizzaBuilder
        +addTopping(topping: String): PizzaBuilder
        +build(): Pizza
    }
    
    class Director {
        +makeMargherita(): Pizza
        +makePepperoni(): Pizza
    }
    
    Director --> PizzaBuilder
    PizzaBuilder --> Pizza

✅ When to Use:

  • Creating objects with many optional parameters (>4-5 parameters)
  • When constructor would have too many parameters
  • When you want to create different representations of the same object
  • When the construction process is complex and multi-step
  • When you need immutable objects with many fields

Code Example:

class Pizza:
    def __init__(self):
        self.size = None
        self.crust = None
        self.toppings = []
    
    def __str__(self):
        return f"{self.size} {self.crust} pizza with {', '.join(self.toppings)}"
 
class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()
    
    def set_size(self, size):
        self.pizza.size = size
        return self
    
    def set_crust(self, crust):
        self.pizza.crust = crust
        return self
    
    def add_topping(self, topping):
        self.pizza.toppings.append(topping)
        return self
    
    def build(self):
        return self.pizza
 
# Usage
pizza = (PizzaBuilder()
         .set_size("Large")
         .set_crust("Thin")
         .add_topping("Pepperoni")
         .add_topping("Mushrooms")
         .build())
 
print(pizza)  # Large Thin pizza with Pepperoni, Mushrooms