AiTechWorlds
AiTechWorlds
McDonald's serves millions of burgers every day across thousands of locations worldwide. Each burger is consistent — same ingredients, same assembly order, same result. McDonald's achieves this not by hiring one expert burger-maker, but by building a system for creating burgers: the factory.
Separately, every McDonald's location has exactly one point-of-sale system, one connection to the payment processor, one database. You don't spin up a new database connection for every customer order. There is one, and everyone shares it.
These two ideas — a factory for consistent object creation, and a guarantee of exactly one instance — are among the most foundational design patterns in software.
In 1994, four software engineers (known as the Gang of Four) published Design Patterns: Elements of Reusable Object-Oriented Software. They cataloged 23 recurring solutions to common OOP design problems, organized into three categories:
| Category | Focus | Examples |
|---|---|---|
| Creational | How objects are created | Singleton, Factory, Builder |
| Structural | How objects are assembled | Adapter, Decorator, Facade |
| Behavioral | How objects communicate | Observer, Strategy, Command |
This lesson covers the three most important creational patterns.
The problem: Some resources must exist exactly once. A database connection pool, a configuration manager, a logger. Creating multiple instances would be wasteful or dangerous.
The solution: The Singleton pattern ensures a class has only one instance and provides a global access point to it.
import threading # For thread-safe Singleton
class DatabaseConnection:
_instance = None # Class variable: stores the one instance
_lock = threading.Lock() # Prevents race conditions in multi-threaded apps
def __new__(cls):
# __new__ is called BEFORE __init__ — controls object creation
if cls._instance is None:
with cls._lock: # Acquire lock
if cls._instance is None: # Double-check after lock
cls._instance = super().__new__(cls)
cls._instance.connected = False # Initialize state
return cls._instance # Always return the SAME instance
def connect(self, host: str, database: str):
if not self.connected:
print(f"Connecting to {database} on {host}...")
self.host = host
self.database = database
self.connected = True
else:
print("Already connected — reusing existing connection")
def query(self, sql: str) -> str:
if not self.connected:
raise RuntimeError("Not connected to database")
return f"Result of: {sql}"
Proving there is only one instance:
db1 = DatabaseConnection()
db2 = DatabaseConnection()
db1.connect("localhost", "production_db")
# Connecting to production_db on localhost...
db2.connect("remotehost", "other_db")
# Already connected — reusing existing connection
print(db1 is db2) # True — same object in memory
print(id(db1) == id(db2)) # True — identical memory address
print(db2.host) # localhost — db2 IS db1
Anti-pattern warning: Singleton introduces global state. Use it sparingly — for truly shared infrastructure (database connections, config, logging). Never use it to avoid thinking about dependency injection.
The problem: Your code needs to create objects, but the exact type should be determined at runtime — based on user input, configuration, or context.
The solution: A factory method encapsulates object creation, returning the right type based on the given parameters.
from abc import ABC, abstractmethod
# Abstract base for all notification types
class Notification(ABC):
@abstractmethod
def send(self, recipient: str, message: str):
pass
# Concrete notification implementations
class EmailNotification(Notification):
def send(self, recipient: str, message: str):
print(f"[EMAIL] To: {recipient} | Message: {message}")
class SMSNotification(Notification):
def send(self, recipient: str, message: str):
# Truncate long SMS messages to 160 characters
short_msg = message[:160]
print(f"[SMS] To: {recipient} | Message: {short_msg}")
class PushNotification(Notification):
def send(self, recipient: str, message: str):
print(f"[PUSH] Device: {recipient} | Alert: {message}")
# The Factory — creates the right notification type
class NotificationFactory:
# Registry maps channel names to their classes
_registry = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
@classmethod
def create(cls, channel: str) -> Notification:
# Look up the channel type in the registry
notification_class = cls._registry.get(channel.lower())
if notification_class is None:
available = ", ".join(cls._registry.keys())
raise ValueError(f"Unknown channel '{channel}'. Available: {available}")
return notification_class() # Create and return instance
@classmethod
def register(cls, channel: str, notification_class: type):
# Allow new channels to be added without modifying the factory
cls._registry[channel] = notification_class
Using the factory:
# The calling code doesn't import EmailNotification, SMSNotification, etc.
# It only knows about the factory and the abstract interface.
channels = ["email", "sms", "push"]
for channel in channels:
notifier = NotificationFactory.create(channel)
notifier.send("user@example.com", "Your order has shipped!")
Output:
[EMAIL] To: user@example.com | Message: Your order has shipped!
[SMS] To: user@example.com | Message: Your order has shipped!
[PUSH] Device: user@example.com | Alert: Your order has shipped!
Adding a new channel (say, Slack) requires zero changes to existing code — just register it: NotificationFactory.register("slack", SlackNotification).
The problem: Some objects are complex to construct — they have many optional parameters, and the construction order matters. Using a constructor with 12 arguments is confusing and error-prone.
The solution: The Builder pattern separates object construction into a series of clear, readable steps.
class SQLQuery:
"""Represents a finished SQL query."""
def __init__(self):
self._table = ""
self._columns = ["*"]
self._conditions = []
self._order_by = None
self._limit = None
def __str__(self) -> str:
# Assemble the SQL string from all the parts
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
where_clause = " AND ".join(self._conditions)
sql += f" WHERE {where_clause}"
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
class SQLQueryBuilder:
"""Builds an SQLQuery step by step."""
def __init__(self):
self._query = SQLQuery() # Start with a blank query
def select(self, *columns: str) -> "SQLQueryBuilder":
# Specify which columns to retrieve
self._query._columns = list(columns)
return self # Return self to enable method chaining
def from_table(self, table: str) -> "SQLQueryBuilder":
# Specify the source table
self._query._table = table
return self
def where(self, condition: str) -> "SQLQueryBuilder":
# Add a WHERE condition (can be called multiple times)
self._query._conditions.append(condition)
return self
def order_by(self, column: str) -> "SQLQueryBuilder":
# Specify sort order
self._query._order_by = column
return self
def limit(self, count: int) -> "SQLQueryBuilder":
# Limit the number of results
self._query._limit = count
return self
def build(self) -> SQLQuery:
# Validate and return the finished query
if not self._query._table:
raise ValueError("Query must specify a table (call .from_table())")
return self._query
Using the builder:
# Readable, step-by-step construction with method chaining
query = (
SQLQueryBuilder()
.select("id", "name", "email")
.from_table("users")
.where("active = true")
.where("age >= 18")
.order_by("name")
.limit(50)
.build()
)
print(query)
Output:
SELECT id, name, email FROM users WHERE active = true AND age >= 18 ORDER BY name LIMIT 50
Compare this to a constructor call: SQLQuery("users", ["id","name","email"], ["active = true","age >= 18"], "name", 50) — the builder version is far more readable and far less error-prone.
| Pattern | Solves | Key Mechanism | Real Use Case |
|---|---|---|---|
| Singleton | Need exactly one instance | Override __new__, cache in class variable | DB connection, config, logger |
| Factory Method | Choose type at runtime | Method returns different subclass instances | Notification channels, payment processors |
| Builder | Complex object construction | Chained methods, deferred .build() call | SQL builders, HTTP clients, test fixtures |
.build().McDonald's doesn't re-invent the burger each time. It has a proven system. Design patterns are those proven systems for software.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises