AiTechWorlds
AiTechWorlds
In October 1994, four software engineers published a book that would become the most cited text in object-oriented software engineering. The authors — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — were collectively known as the Gang of Four (GoF). Their book: Design Patterns: Elements of Reusable Object-Oriented Software.
The premise was simple but powerful: certain design problems recur across different software systems. Rather than reinventing solutions each time, why not catalogue them, name them, and describe them in a way that any experienced programmer can understand?
They identified 23 patterns, organised into three families:
This lesson covers creational patterns — the five GoF patterns that control how objects come into existence.
Naive object creation — my_object = MyClass() scattered everywhere — creates tight coupling. If the class name changes, if the constructor signature changes, or if you want to substitute a different class (for testing, for configuration), you must find and change every call site.
Creational patterns decouple the request for an object from the creation of that object. They give you flexibility to:
Problem: Some resources should exist only once — a database connection pool, a configuration manager, a logger. Creating multiple instances wastes resources or introduces inconsistency.
Solution: Ensure a class has exactly one instance, and provide a global access point to it.
class DatabasePool:
_instance = None # Class-level variable — shared across all calls
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connections = []
print("Pool created — only once!")
return cls._instance # Always returns the same object
def get_connection(self):
return f"Connection from pool of {len(self.connections)} connections"
pool1 = DatabasePool()
pool2 = DatabasePool()
print(pool1 is pool2) # True — same object
Real uses: Django's database connection pool, Python's logging.getLogger(), AWS SDK client instances, configuration readers.
Caution: Singleton is the most criticised GoF pattern. It creates hidden global state, makes testing difficult (you cannot easily inject a mock), and violates the Single Responsibility Principle. Use it only when a single instance is a genuine architectural requirement, not just convenient.
Problem: You know you need an object, but which type of object depends on runtime context. Hardcoding if platform == "windows": ... else: ... throughout your codebase is unmanageable.
Solution: Define an interface for creating an object, but let subclasses decide which class to instantiate. The factory method lets a class defer instantiation to subclasses.
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def deliver(self, message: str): ...
class EmailNotifier(Notifier):
def deliver(self, message):
print(f"Email: {message}")
class SMSNotifier(Notifier):
def deliver(self, message):
print(f"SMS: {message}")
class NotificationService(ABC):
@abstractmethod
def create_notifier(self) -> Notifier: ... # Factory Method
def notify(self, message):
notifier = self.create_notifier() # Subclass decides the type
notifier.deliver(message)
class EmailService(NotificationService):
def create_notifier(self):
return EmailNotifier()
class SMSService(NotificationService):
def create_notifier(self):
return SMSNotifier()
service = EmailService()
service.notify("Your order has shipped!") # Email: Your order has shipped!
Real uses: Django's ORM database backends (each database type is a factory), Python's logging module handlers, game engine entity spawners.
Problem: You need to create families of related objects that must work together. Creating a Windows button alongside a Mac dialog would be inconsistent.
Solution: Define an abstract factory interface with creation methods for each product. Each concrete factory creates a consistent family.
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self): ...
class Dialog(ABC):
@abstractmethod
def show(self): ...
class WindowsButton(Button):
def render(self): print("[ Windows Button ]")
class MacButton(Button):
def render(self): print("( Mac Button )")
class WindowsDialog(Dialog):
def show(self): print("[== Windows Dialog ==]")
class MacDialog(Dialog):
def show(self): print("(~~ Mac Dialog ~~)")
class UIFactory(ABC): # Abstract Factory
@abstractmethod
def create_button(self) -> Button: ...
@abstractmethod
def create_dialog(self) -> Dialog: ...
class WindowsFactory(UIFactory): # Concrete Factory for Windows
def create_button(self): return WindowsButton()
def create_dialog(self): return WindowsDialog()
class MacFactory(UIFactory): # Concrete Factory for Mac
def create_button(self): return MacButton()
def create_dialog(self): return MacDialog()
def build_ui(factory: UIFactory):
button = factory.create_button() # Always gets a consistent family
dialog = factory.create_dialog()
button.render()
dialog.show()
build_ui(MacFactory())
# ( Mac Button )
# (~~ Mac Dialog ~~)
Real uses: Cross-platform UI toolkits, database abstraction layers (create connection + cursor from one factory), cloud provider SDKs.
Problem: Constructing a complex object requires many steps, many optional parameters, and the construction process should be independent of the parts. A constructor with 12 parameters is both hard to call and hard to read.
Solution: Separate the construction of a complex object from its representation. Use a builder that constructs the object step by step, supporting a fluent interface.
class QueryBuilder:
def __init__(self):
self._table = ""
self._columns = ["*"]
self._conditions = []
self._limit_val = None
def select(self, *columns):
self._columns = list(columns)
return self # Return self for method chaining
def from_table(self, table):
self._table = table
return self
def where(self, condition):
self._conditions.append(condition)
return self
def limit(self, n):
self._limit_val = n
return self
def build(self):
cols = ", ".join(self._columns)
query = f"SELECT {cols} FROM {self._table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._limit_val:
query += f" LIMIT {self._limit_val}"
return query
query = (QueryBuilder()
.select("id", "name", "email")
.from_table("users")
.where("active = true")
.where("age > 18")
.limit(100)
.build())
print(query)
# SELECT id, name, email FROM users WHERE active = true AND age > 18 LIMIT 100
Real uses: SQL query builders (SQLAlchemy, Django ORM), HTTP request builders (requests.Request), Java's StringBuilder, Kotlin's buildString, test data factories.
The fluent interface (each method returns self) makes complex construction readable as a sentence.
Problem: Creating an object from scratch is expensive — it requires querying a database, parsing configuration, or doing heavy computation. You need many similar objects.
Solution: Create new objects by cloning (copying) an existing instance — the prototype.
import copy
class GameCharacter:
def __init__(self, name, health, weapons):
self.name = name
self.health = health
self.weapons = weapons # A list — mutable
def clone(self):
return copy.deepcopy(self) # Deep copy: new object, new nested objects
def __repr__(self):
return f"Character({self.name}, HP={self.health}, weapons={self.weapons})"
# Create the expensive prototype once
template_soldier = GameCharacter("Soldier", 100, ["rifle", "grenade"])
# Spawn 1000 enemies cheaply by cloning
enemy1 = template_soldier.clone()
enemy1.name = "Enemy-001"
enemy1.health = 80
enemy2 = template_soldier.clone()
enemy2.name = "Enemy-002"
enemy2.weapons.append("knife") # Only enemy2 gets the knife (deep copy)
print(template_soldier) # Character(Soldier, HP=100, weapons=['rifle', 'grenade'])
print(enemy2) # Character(Enemy-002, HP=100, weapons=['rifle', 'grenade', 'knife'])
Real uses: Game object spawning, document template systems (clone a base Word document), JavaScript's prototype chain, Excel cell style cloning.
| Anti-Pattern | Description | Problem |
|---|---|---|
| Singleton Abuse | Using Singleton for convenience, not necessity | Hidden global state, untestable code |
| God Object | One class that creates and manages everything | Violates SRP, becomes a bottleneck |
| Telescoping Constructor | Constructor with 10+ parameters | Unreadable call sites, easy to misorder arguments — use Builder instead |
| Pattern | Problem It Solves | Real Example | When to Use | Caution |
|---|---|---|---|---|
| Singleton | Only one instance should exist | DB connection pool, Logger | Truly unique shared resource | Avoid as global state shortcut |
| Factory Method | Subclass decides which class to create | Django DB backends, logging handlers | Type depends on runtime context | Can over-complicate simple cases |
| Abstract Factory | Create families of compatible objects | Cross-platform UI, cloud SDK | Consistent product families | More classes = more complexity |
| Builder | Step-by-step construction of complex objects | SQL builders, HTTP request builders | Many optional params; fluent API | Overkill for simple objects |
| Prototype | Clone instead of construct | Game object spawning, doc templates | Object creation is expensive | Shallow vs deep copy bugs |
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises