Design Patterns: Singleton, Factory, Observer, Strategy in Python
Implementing GoF design patterns in Pythonic ways
Interview Question
"Implement the Singleton, Factory, Observer, and Strategy patterns in Python. How do you make them thread-safe? When would you use each pattern?"
Difficulty: Medium | Frequently asked at Google, Meta, Amazon
Theoretical Foundation
What are Design Patterns?
Design patterns are reusable solutions to common software design problems. They're not code but templates for solving specific issues.
# Design patterns categorized:
# 1. Creational: How objects are created (Singleton, Factory)
# 2. Structural: How objects are composed (Adapter, Decorator)
# 3. Behavioral: How objects communicate (Observer, Strategy)
# Python advantage: Many patterns are simpler due to dynamic typing
# and first-class functions
ℹ️
Key Concept: Design patterns are guidelines, not rules. Use them when they solve a real problem.
Singleton Pattern
Problem
Ensure a class has only one instance and provide global access to it.
Implementation
import threading
from typing import Optional
# Method 1: Using __new__
class Singleton:
_instance: Optional['Singleton'] = 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, value=None):
if not hasattr(self, '_initialized'):
self.value = value
self._initialized = True
# Method 2: Using metaclass
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.connected = False
def connect(self):
self.connected = True
print(f"Connected to {self.connection_string}")
# Method 3: Using decorator
def singleton(cls):
instances = {}
lock = threading.Lock()
def get_instance(*args, **kwargs):
if cls not in instances:
with lock:
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Logger:
def __init__(self):
self.logs = []
def log(self, message):
self.logs.append(message)
print(f"LOG: {message}")
# Usage
db1 = Database("postgresql://localhost/mydb")
db2 = Database("postgresql://localhost/mydb") # Same instance
print(f"Same instance: {db1 is db2}")
db1.connect()
print(f"DB2 connected: {db2.connected}")
Output:
Same instance: True
Connected to postgresql://localhost/mydb
DB2 connected: True
Thread-Safe Singleton
import threading
import time
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):
if not hasattr(self, 'initialized'):
self.initialized = True
self.data = {}
print("Singleton initialized")
def create_singleton(thread_id):
"""Create singleton from multiple threads."""
singleton = ThreadSafeSingleton()
print(f"Thread {thread_id}: {id(singleton)}")
# Test thread safety
threads = []
for i in range(5):
t = threading.Thread(target=create_singleton, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
Output:
Singleton initialized
Thread 0: 140234866577152
Thread 1: 140234866577152
Thread 2: 140234866577152
Thread 3: 140234866577152
Thread 4: 140234866577152
⚠️
Pythonic Alternative: Consider using module-level variables instead of Singleton. Python modules are naturally singletons.
Factory Pattern
Problem
Create objects without specifying their exact class.
Implementation
from abc import ABC, abstractmethod
from typing import Dict, Type
# Product interface
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
pass
@abstractmethod
def move(self) -> str:
pass
# Concrete products
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
def move(self) -> str:
return "Runs on four legs"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
def move(self) -> str:
return "Slinks gracefully"
class Bird(Animal):
def speak(self) -> str:
return "Tweet!"
def move(self) -> str:
return "Flies in the sky"
# Simple Factory
class AnimalFactory:
_animals: Dict[str, Type[Animal]] = {
"dog": Dog,
"cat": Cat,
"bird": Bird,
}
@classmethod
def create(cls, animal_type: str) -> Animal:
animal_class = cls._animals.get(animal_type.lower())
if not animal_class:
raise ValueError(f"Unknown animal type: {animal_type}")
return animal_class()
@classmethod
def register(cls, animal_type: str, animal_class: Type[Animal]):
cls._animals[animal_type.lower()] = animal_class
# Usage
animals = ["dog", "cat", "bird"]
for animal_type in animals:
animal = AnimalFactory.create(animal_type)
print(f"{animal_type.title()}: {animal.speak()} {animal.move()}")
Output:
Dog: Woof! Runs on four legs
Cat: Meow! Slinks gracefully
Bird: Tweet! Flies in the sky
Abstract Factory
from abc import ABC, abstractmethod
# Abstract products
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str:
pass
# Concrete products
class WindowsButton(Button):
def render(self) -> str:
return "Windows Button"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "Windows Checkbox"
class MacButton(Button):
def render(self) -> str:
return "Mac Button"
class MacCheckbox(Checkbox):
def render(self) -> str:
return "Mac Checkbox"
# Abstract factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete factories
class WindowsFactory(GUIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self) -> Button:
return MacButton()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
# Client code
class Application:
def __init__(self, factory: GUIFactory):
self.factory = factory
self.button = None
self.checkbox = None
def create_ui(self):
self.button = self.factory.create_button()
self.checkbox = self.factory.create_checkbox()
def render(self):
print(f"Button: {self.button.render()}")
print(f"Checkbox: {self.checkbox.render()}")
# Usage
import platform
if platform.system() == "Windows":
factory = WindowsFactory()
else:
factory = MacFactory()
app = Application(factory)
app.create_ui()
app.render()
💡
Interview Tip: Factory patterns are useful when you need to create objects based on configuration or runtime conditions.
Observer Pattern
Problem
Define a one-to-many dependency between objects so that when one object changes state, all dependents are notified.
Implementation
from typing import List, Callable, Any
from dataclasses import dataclass, field
from datetime import datetime
# Event system
@dataclass
class Event:
name: str
data: Any
timestamp: datetime = field(default_factory=datetime.now)
class EventEmitter:
"""Simple event emitter for Observer pattern."""
def __init__(self):
self._listeners: dict[str, List[Callable]] = {}
def on(self, event_name: str, callback: Callable):
"""Register event listener."""
if event_name not in self._listeners:
self._listeners[event_name] = []
self._listeners[event_name].append(callback)
def off(self, event_name: str, callback: Callable):
"""Remove event listener."""
if event_name in self._listeners:
self._listeners[event_name].remove(callback)
def emit(self, event_name: str, data: Any = None):
"""Emit event to all listeners."""
event = Event(name=event_name, data=data)
if event_name in self._listeners:
for callback in self._listeners[event_name]:
callback(event)
# Concrete subjects
class User(EventEmitter):
def __init__(self, name: str):
super().__init__()
self.name = name
self._email = ""
@property
def email(self):
return self._email
@email.setter
def email(self, value: str):
old_email = self._email
self._email = value
self.emit("email_changed", {
"user": self.name,
"old_email": old_email,
"new_email": value
})
# Observers
def log_email_change(event: Event):
print(f"[LOG] Email changed: {event.data}")
def send_notification(event: Event):
print(f"[NOTIFY] Sending notification to {event.data['new_email']}")
def update_database(event: Event):
print(f"[DB] Updating database for {event.data['user']}")
# Usage
user = User("Alice")
# Register observers
user.on("email_changed", log_email_change)
user.on("email_changed", send_notification)
user.on("email_changed", update_database)
# Change email (triggers all observers)
user.email = "alice@example.com"
Output:
[LOG] Email changed: {'user': 'Alice', 'old_email': '', 'new_email': 'alice@example.com'}
[NOTIFY] Sending notification to alice@example.com
[DB] Updating database for Alice
Advanced Observer with Weak References
import weakref
from typing import List, Any, Callable
class WeakObserver:
"""Observer that uses weak references to avoid memory leaks."""
def __init__(self):
self._listeners: dict[str, List[weakref.WeakMethod]] = {}
def on(self, event_name: str, callback: Callable):
"""Register weak event listener."""
if event_name not in self._listeners:
self._listeners[event_name] = []
# Use WeakMethod for bound methods
try:
weak_ref = weakref.WeakMethod(callback)
except TypeError:
# For regular functions
weak_ref = weakref.ref(callback)
self._listeners[event_name].append(weak_ref)
def emit(self, event_name: str, data: Any = None):
"""Emit event to all live listeners."""
if event_name not in self._listeners:
return
# Clean up dead references
self._listeners[event_name] = [
ref for ref in self._listeners[event_name]
if ref() is not None
]
for weak_ref in self._listeners[event_name]:
callback = weak_ref()
if callback is not None:
callback(event_name, data)
# Usage
class User:
def __init__(self, name):
self.name = name
self.emitter = WeakObserver()
def set_email(self, email):
self.emitter.emit("email_changed", {"user": self.name, "email": email})
def handler(event_name, data):
print(f"Handled {event_name}: {data}")
user = User("Bob")
user.emitter.on("email_changed", handler)
user.set_email("bob@example.com") # Works
del handler # Handler can be garbage collected
user.set_email("bob2@example.com") # No error
ℹ️
Memory Safety: Weak references prevent memory leaks when observers are deleted.
Strategy Pattern
Problem
Define a family of algorithms, encapsulate each one, and make them interchangeable.
Implementation
from abc import ABC, abstractmethod
from typing import List
from dataclasses import dataclass
# Strategy interface
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: List[int]) -> List[int]:
pass
@abstractmethod
def name(self) -> str:
pass
# Concrete strategies
class BubbleSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
arr = data.copy()
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
def name(self) -> str:
return "Bubble Sort"
class QuickSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
def name(self) -> str:
return "Quick Sort"
class MergeSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: List[int], right: List[int]) -> List[int]:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def name(self) -> str:
return "Merge Sort"
# Context
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
@property
def strategy(self) -> SortStrategy:
return self._strategy
@strategy.setter
def strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: List[int]) -> List[int]:
print(f"Sorting with {self._strategy.name()}")
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSort())
print(f"Bubble: {sorter.sort(data)}")
sorter.strategy = QuickSort()
print(f"Quick: {sorter.sort(data)}")
sorter.strategy = MergeSort()
print(f"Merge: {sorter.sort(data)}")
Output:
Sorting with Bubble Sort
Bubble: [11, 12, 22, 25, 34, 64, 90]
Sorting with Quick Sort
Quick: [11, 12, 22, 25, 34, 64, 90]
Sorting with Merge Sort
Merge: [11, 12, 22, 25, 34, 64, 90]
Strategy with Functions
from typing import Callable, List
from dataclasses import dataclass
# Strategies as functions
def discount_10_percent(price: float) -> float:
return price * 0.9
def discount_20_percent(price: float) -> float:
return price * 0.8
def free_shipping(price: float) -> float:
return price - 5.99 # Assuming $5.99 shipping
# Context using functions
class ShoppingCart:
def __init__(self):
self.items: List[dict] = []
self.discount_strategy: Callable[[float], float] = None
def add_item(self, name: str, price: float):
self.items.append({"name": name, "price": price})
def set_discount(self, strategy: Callable[[float], float]):
self.discount_strategy = strategy
def total(self) -> float:
total = sum(item["price"] for item in self.items)
if self.discount_strategy:
total = self.discount_strategy(total)
return total
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
print(f"No discount: ${cart.total():.2f}")
cart.set_discount(discount_10_percent)
print(f"10% off: ${cart.total():.2f}")
cart.set_discount(discount_20_percent)
print(f"20% off: ${cart.total():.2f}")
Output:
No discount: $1029.98
10% off: $926.98
20% off: $823.98
💡
Interview Tip: Strategy pattern is great for algorithms that need to be swapped at runtime (sorting, pricing, validation).
Other Important Patterns
Decorator Pattern
from abc import ABC, abstractmethod
# Component interface
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete component
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.00
def description(self) -> str:
return "Simple coffee"
# Decorator base
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
# Concrete decorators
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.50
def description(self) -> str:
return f"{self._coffee.description()}, milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.25
def description(self) -> str:
return f"{self._coffee.description()}, sugar"
# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")
coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")
coffee_with_milk_sugar = SugarDecorator(coffee_with_milk)
print(f"{coffee_with_milk_sugar.description()}: ${coffee_with_milk_sugar.cost():.2f}")
Output:
Simple coffee: $2.00
Simple coffee, milk: $2.50
Simple coffee, milk, sugar: $2.75
Command Pattern
from abc import ABC, abstractmethod
from typing import List
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Concrete commands
class TextEditor:
def __init__(self):
self.text = ""
def insert(self, text: str):
self.text += text
def delete(self, length: int):
self.text = self.text[:-length] if length else self.text
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str):
self.editor = editor
self.text = text
def execute(self):
self.editor.insert(self.text)
def undo(self):
self.editor.delete(len(self.text))
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, length: int):
self.editor = editor
self.length = length
self.deleted_text = ""
def execute(self):
self.deleted_text = self.editor.text[-self.length:]
self.editor.delete(self.length)
def undo(self):
self.editor.insert(self.deleted_text)
# Invoker
class CommandManager:
def __init__(self):
self.history: List[Command] = []
self.undone: List[Command] = []
def execute(self, command: Command):
command.execute()
self.history.append(command)
self.undone.clear()
def undo(self):
if self.history:
command = self.history.pop()
command.undo()
self.undone.append(command)
def redo(self):
if self.undone:
command = self.undone.pop()
command.execute()
self.history.append(command)
# Usage
editor = TextEditor()
manager = CommandManager()
# Execute commands
manager.execute(InsertCommand(editor, "Hello"))
manager.execute(InsertCommand(editor, " World"))
print(f"After insert: '{editor.text}'")
manager.execute(DeleteCommand(editor, 5))
print(f"After delete: '{editor.text}'")
# Undo
manager.undo()
print(f"After undo: '{editor.text}'")
manager.undo()
print(f"After undo: '{editor.text}'")
Output:
After insert: 'Hello World'
After delete: 'Hello'
After undo: 'Hello World'
After undo: 'Hello'
Complexity Analysis
Pattern Overhead
| Pattern | Time Complexity | Space Complexity | Use Case |
|---|---|---|---|
| Singleton | O(1) access | O(1) | Global state |
| Factory | O(1) creation | O(n) classes | Object creation |
| Observer | O(n) notification | O(n) listeners | Event systems |
| Strategy | O(1) swap | O(1) | Algorithm selection |
When to Use
# Singleton: Global state, resource management
# - Database connections
# - Configuration
# - Logging
# Factory: Object creation based on conditions
# - Plugin systems
# - Document parsers
# - UI components
# Observer: Event-driven systems
# - GUI events
# - Message queues
# - React systems
# Strategy: Algorithm selection
# - Sorting algorithms
# - Payment methods
# - Compression algorithms
Interview Tips
Common Follow-up Questions
-
"When would you NOT use these patterns?"
- Over-engineering simple solutions
- When language features already solve the problem
- When patterns add unnecessary complexity
-
"How do you make Singleton thread-safe?"
- Double-checked locking
- Module-level variables (Pythonic)
- threading.Lock
-
"What's the difference between Factory and Abstract Factory?"
- Factory: Creates one product type
- Abstract Factory: Creates families of related products
Code Review Tips
# BAD: Singleton everywhere
class BadSingleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# GOOD: Use module-level when possible
# config.py
_config = {"debug": False, "db_url": "..."}
def get_config():
return _config
# BAD: Observer with memory leaks
class BadObserver:
def __init__(self):
self.listeners = [] # Strong references
def add_listener(self, listener):
self.listeners.append(listener) # Never garbage collected!
# GOOD: Observer with weak references
import weakref
class GoodObserver:
def __init__(self):
self.listeners = [] # Weak references
def add_listener(self, listener):
self.listeners.append(weakref.ref(listener))
⚠️
Common Mistake: Overusing design patterns. They're tools, not goals. Use them only when they solve a real problem.
Summary
| Pattern | Category | Purpose | Python Implementation |
|---|---|---|---|
| Singleton | Creational | Single instance | __new__, metaclass, module |
| Factory | Creational | Object creation | Functions, classes |
| Observer | Behavioral | Event notification | Callbacks, weakref |
| Strategy | Behavioral | Algorithm selection | Functions, classes |
| Decorator | Structural | Add behavior | Python decorators |
| Command | Behavioral | Encapsulate actions | Classes with execute/undo |
Best Practices
- Use Pythonic alternatives when possible (module variables for Singleton)
- Keep patterns simple - don't over-engineer
- Document pattern usage clearly
- Test patterns independently
- Consider performance implications
ℹ️
Key Takeaway: Design patterns solve common problems. Use them judiciously and prefer Pythonic solutions when available.
Practice Problems
- Singleton Logger: Implement a thread-safe singleton logger
- Payment Factory: Create a factory for different payment methods
- Event System: Build an observer pattern for a chat application
- Compression Strategy: Implement different compression algorithms
- Undo/Redo: Build a command pattern with undo/redo functionality
Further Reading
- Gang of Four Book: "Design Patterns: Elements of Reusable Object-Oriented Software"
- Python Patterns: "Python Cookbook" by David Beazley
- Refactoring Guru: https://refactoring.guru/design-patterns
- Python Docs:
abcmodule,weakrefmodule
Remember: Design patterns are tools, not goals. Use them to solve real problems, not to show off.