Creational and Structural Design Patterns
Implement Singleton, Factory, Builder, Adapter, Decorator (structural), and Proxy patterns in Python
Creational and Structural Design Patterns
Singleton Pattern
Ensure a class has exactly one instance.
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
self.connection = None
def connect(self, url):
self.connection = f"Connected to {url}"
# Both variables point to the same instance
db1 = Database()
db2 = Database()
print(db1 is db2) # TrueThread-Safe Singleton
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
pass # init runs every time — guard with flag if neededBe cautious with Singleton in multi-threaded contexts. Always use a lock for the first creation, and guard __init__ from re-initialisation.
Factory Pattern
Abstract object creation behind a factory interface.
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount):
pass
class StripeGateway(PaymentGateway):
def charge(self, amount):
return f"Stripe charged ${amount}"
class PayPalGateway(PaymentGateway):
def charge(self, amount):
return f"PayPal charged ${amount}"
class PaymentFactory:
GATEWAYS = {
"stripe": StripeGateway,
"paypal": PayPalGateway,
}
@staticmethod
def create(gateway_type):
cls = PaymentFactory.GATEWAYS.get(gateway_type)
if not cls:
raise ValueError(f"Unknown gateway: {gateway_type}")
return cls()
gateway = PaymentFactory.create("stripe")
print(gateway.charge(100))Builder Pattern
Build complex objects step by step.
class QueryBuilder:
def __init__(self):
self._select = []
self._from_ = ""
self._where = []
self._order_by = []
self._limit = None
def select(self, *columns):
self._select.extend(columns)
return self
def from_(self, table):
self._from_ = table
return self
def where(self, condition):
self._where.append(condition)
return self
def order_by(self, column, direction="ASC"):
self._order_by.append(f"{column} {direction}")
return self
def limit(self, n):
self._limit = n
return self
def build(self):
parts = ["SELECT"]
parts.append(", ".join(self._select) if self._select else "*")
parts.append(f"FROM {self._from_}")
if self._where:
parts.append("WHERE " + " AND ".join(self._where))
if self._order_by:
parts.append("ORDER BY " + ", ".join(self._order_by))
if self._limit is not None:
parts.append(f"LIMIT {self._limit}")
return " ".join(parts)
query = (QueryBuilder()
.select("id", "name", "email")
.from_("users")
.where("age > 18")
.where("status = 'active'")
.order_by("name")
.limit(10)
.build())
print(query)
# SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name ASC LIMIT 10Builder is ideal for constructing objects with many optional parameters, SQL queries, HTTP requests, and configuration objects.
Adapter Pattern
Convert one interface to another that clients expect.
class USPlug:
def voltage(self):
return 120
class EuropeanPlug:
def voltage(self):
return 230
class USCharger:
def charge(self, plug):
return f"Charging at {plug.voltage()}V (US)"
class EuropeanToUSAdapter:
def __init__(self, euro_plug):
self._euro = euro_plug
def voltage(self):
return self._euro.voltage()
charger = USCharger()
euro_plug = EuropeanPlug()
adapter = EuropeanToUSAdapter(euro_plug)
print(charger.charge(adapter)) # Works!Decorator Pattern (Structural)
Add behaviour to objects dynamically without subclassing.
from functools import wraps
class Beverage:
def cost(self):
return 5
def description(self):
return "Beverage"
class MilkDecorator:
def __init__(self, beverage):
self._beverage = beverage
def cost(self):
return self._beverage.cost() + 2
def description(self):
return self._beverage.description() + ", Milk"
class SugarDecorator:
def __init__(self, beverage):
self._beverage = beverage
def cost(self):
return self._beverage.cost() + 1
def description(self):
return self._beverage.description() + ", Sugar"
coffee = Beverage()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(f"{coffee.description()} = ${coffee.cost()}")
# Beverage, Milk, Sugar = $8The structural Decorator pattern differs from Python's function decorators, but the idea is the same: wrap an object/function to add behaviour.
Proxy Pattern
Control access to an object via a surrogate.
import time
from datetime import datetime
class SensitiveData:
def read(self):
return "TOP SECRET DATA"
class AccessProxy:
def __init__(self, target):
self._target = target
self._allowed_users = {"admin", "supervisor"}
def read(self, user):
if user not in self._allowed_users:
raise PermissionError(f"{user} is not authorised")
return self._target.read()
class LoggingProxy:
def __init__(self, target):
self._target = target
def read(self, *args, **kwargs):
print(f"[{datetime.now()}] Access attempt")
return self._target.read(*args, **kwargs)
data = SensitiveData()
proxy = LoggingProxy(AccessProxy(data))
print(proxy.read("admin")) # Logs access, checks auth, returns dataLazy Proxy
class LazyImage:
def __init__(self, path):
self.path = path
self._image = None
def _load(self):
if self._image is None:
print(f"Loading {self.path} from disk...")
self._image = f"<image:{self.path}>"
return self._image
def display(self):
return self._load()
img = LazyImage("photo.jpg")
# Image not loaded yet
print(img.display()) # Loads now
print(img.display()) # Uses cached versionPractice Questions
- Implement a
Loggersingleton that writes to a file. Ensure thread safety. - Build a
ShapeFactorythat createsCircle,Square, andTriangleobjects. Add a new shape without modifying the factory. - Implement a Builder for constructing HTML elements (e.g.,
div,p,awith attributes and children). - Create an adapter that converts a modern JSON API response to a legacy XML-based interface.
- What is the difference between the structural Decorator and Python's function decorators?
- Implement a caching proxy that stores results of an expensive computation and returns cached results for repeated calls.
- When would you choose Builder over a constructor with many parameters?
- Build a notification system using Factory:
EmailNotifier,SMSNotifier,PushNotifier. - Implement a virtual proxy that lazily loads a large database query result only when accessed.
- Combine the Decorator and Adapter patterns: wrap a legacy SMS service with an adapter, then add logging via decorator.