Design Patterns in Modern Languages: Singleton, Factory, Observer, Strategy
Introduction
Design patterns provide reusable solutions to common software problems. This guide covers four essential patterns—Singleton for single instances, Factory for object creation, Observer for event notification, and Strategy for interchangeable algorithms—with implementations in C#, TypeScript, and Python.
Singleton Pattern
Purpose
Ensure a class has only one instance and provide global access point.
When to use:
- Database connections
- Configuration managers
- Logging services
- Thread pools
C# Implementation
// Thread-safe lazy initialization
public sealed class DatabaseConnection
{
private static readonly Lazy<DatabaseConnection> _instance =
new Lazy<DatabaseConnection>(() => new DatabaseConnection());
private DatabaseConnection()
{
// Private constructor prevents external instantiation
ConnectionString = LoadConnectionString();
}
public static DatabaseConnection Instance => _instance.Value;
public string ConnectionString { get; private set; }
public void ExecuteQuery(string sql)
{
Console.WriteLine($"Executing: {sql}");
}
}
// Usage
var db = DatabaseConnection.Instance;
db.ExecuteQuery("SELECT * FROM Users");
TypeScript Implementation
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {
// Private constructor
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
}
// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("Application started");
console.log(logger1 === logger2); // true - same instance
Python Implementation
class ConfigManager:
"""Thread-safe singleton with __new__."""
_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._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.config = self._load_config()
self._initialized = True
def _load_config(self):
return {"api_url": "https://api.example.com", "timeout": 30}
def get(self, key):
return self.config.get(key)
# Usage
config1 = ConfigManager()
config2 = ConfigManager()
print(config1 is config2) # True
Modern alternative - Dependency Injection:
// Instead of Singleton, use DI container
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(message);
}
}
// Register in DI container
container.register<ILogger>("ILogger", ConsoleLogger, { lifecycle: "singleton" });
// Inject dependency
class UserService {
constructor(private logger: ILogger) {}
createUser(name: string): void {
this.logger.log(`Creating user: ${name}`);
}
}
Factory Pattern
Purpose
Create objects without specifying exact class, delegating instantiation to subclasses.
Simple Factory (C#)
// Product interface
public interface INotification
{
void Send(string message);
}
// Concrete products
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Email: {message}");
}
}
public class SMSNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"SMS: {message}");
}
}
public class PushNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Push: {message}");
}
}
// Factory
public class NotificationFactory
{
public static INotification Create(string type)
{
return type.ToLower() switch
{
"email" => new EmailNotification(),
"sms" => new SMSNotification(),
"push" => new PushNotification(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}
}
// Usage
var notification = NotificationFactory.Create("email");
notification.Send("Hello, World!");
Factory Method (TypeScript)
// Product interface
interface IDocument {
open(): void;
save(): void;
}
// Concrete products
class PDFDocument implements IDocument {
open(): void {
console.log("Opening PDF document");
}
save(): void {
console.log("Saving PDF document");
}
}
class WordDocument implements IDocument {
open(): void {
console.log("Opening Word document");
}
save(): void {
console.log("Saving Word document");
}
}
// Creator abstract class
abstract class DocumentCreator {
abstract createDocument(): IDocument;
openDocument(): void {
const doc = this.createDocument();
doc.open();
}
}
// Concrete creators
class PDFCreator extends DocumentCreator {
createDocument(): IDocument {
return new PDFDocument();
}
}
class WordCreator extends DocumentCreator {
createDocument(): IDocument {
return new WordDocument();
}
}
// Usage
const pdfCreator = new PDFCreator();
pdfCreator.openDocument(); // Opens PDF
const wordCreator = new WordCreator();
wordCreator.openDocument(); // Opens Word
Abstract Factory (Python)
from abc import ABC, abstractmethod
# Abstract products
class Button(ABC):
@abstractmethod
def render(self): pass
class Checkbox(ABC):
@abstractmethod
def render(self): pass
# Concrete products - Windows
class WindowsButton(Button):
def render(self):
return "Rendering Windows button"
class WindowsCheckbox(Checkbox):
def render(self):
return "Rendering Windows checkbox"
# Concrete products - macOS
class MacButton(Button):
def render(self):
return "Rendering Mac button"
class MacCheckbox(Checkbox):
def render(self):
return "Rendering 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()
# Usage
def create_ui(factory: GUIFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())
print(checkbox.render())
import platform
factory = WindowsFactory() if platform.system() == "Windows" else MacFactory()
create_ui(factory)
Observer Pattern
Purpose
Define one-to-many dependency where state changes notify all dependents automatically.
C# with Events
// Subject
public class StockMarket
{
public event EventHandler<StockPriceChangedEventArgs> PriceChanged;
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
var oldPrice = _price;
_price = value;
OnPriceChanged(new StockPriceChangedEventArgs(oldPrice, value));
}
}
}
protected virtual void OnPriceChanged(StockPriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
public class StockPriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public StockPriceChangedEventArgs(decimal oldPrice, decimal newPrice)
{
OldPrice = oldPrice;
NewPrice = newPrice;
}
}
// Observers
public class StockDisplay
{
public void Subscribe(StockMarket market)
{
market.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object sender, StockPriceChangedEventArgs e)
{
Console.WriteLine($"Display: Price changed from {e.OldPrice} to {e.NewPrice}");
}
}
public class StockAlert
{
private decimal _threshold;
public StockAlert(decimal threshold)
{
_threshold = threshold;
}
public void Subscribe(StockMarket market)
{
market.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object sender, StockPriceChangedEventArgs e)
{
if (e.NewPrice > _threshold)
{
Console.WriteLine($"ALERT: Price exceeded {_threshold}!");
}
}
}
// Usage
var market = new StockMarket { Price = 100 };
var display = new StockDisplay();
var alert = new StockAlert(150);
display.Subscribe(market);
alert.Subscribe(market);
market.Price = 120; // Triggers display
market.Price = 160; // Triggers both display and alert
TypeScript with RxJS
import { Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
interface User {
id: number;
name: string;
status: 'online' | 'offline';
}
class UserService {
private userStatusSubject = new Subject<User>();
userStatus$ = this.userStatusSubject.asObservable();
updateUserStatus(user: User): void {
this.userStatusSubject.next(user);
}
}
// Observers
class NotificationService {
constructor(private userService: UserService) {
this.userService.userStatus$
.pipe(filter(user => user.status === 'online'))
.subscribe(user => {
console.log(`Notification: ${user.name} is now online`);
});
}
}
class ActivityLogger {
constructor(private userService: UserService) {
this.userService.userStatus$.subscribe(user => {
console.log(`Log: User ${user.id} status: ${user.status}`);
});
}
}
// Usage
const userService = new UserService();
const notifications = new NotificationService(userService);
const logger = new ActivityLogger(userService);
userService.updateUserStatus({ id: 1, name: 'Alice', status: 'online' });
// Output:
// Log: User 1 status: online
// Notification: Alice is now online
Python with Custom Observer
from abc import ABC, abstractmethod
from typing import List
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, subject: 'Subject') -> None:
pass
# Subject
class Subject:
def __init__(self):
self._observers: List[Observer] = []
self._state = None
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self)
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify()
# Concrete observers
class EmailNotifier(Observer):
def update(self, subject: Subject) -> None:
print(f"Email: State changed to {subject.state}")
class SMSNotifier(Observer):
def update(self, subject: Subject) -> None:
if subject.state == "critical":
print(f"SMS Alert: Critical state!")
class Logger(Observer):
def update(self, subject: Subject) -> None:
print(f"Log: State = {subject.state}")
# Usage
subject = Subject()
email = EmailNotifier()
sms = SMSNotifier()
logger = Logger()
subject.attach(email)
subject.attach(sms)
subject.attach(logger)
subject.state = "normal" # All observers notified
subject.state = "critical" # SMS sends alert
Strategy Pattern
Purpose
Define family of algorithms, encapsulate each, make them interchangeable.
C# Implementation
// Strategy interface
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
// Concrete strategies
public class CreditCardPayment : IPaymentStrategy
{
private string _cardNumber;
public CreditCardPayment(string cardNumber)
{
_cardNumber = cardNumber;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} with credit card {_cardNumber}");
}
}
public class PayPalPayment : IPaymentStrategy
{
private string _email;
public PayPalPayment(string email)
{
_email = email;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} via PayPal to {_email}");
}
}
public class BitcoinPayment : IPaymentStrategy
{
private string _walletAddress;
public BitcoinPayment(string walletAddress)
{
_walletAddress = walletAddress;
}
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} in Bitcoin to {_walletAddress}");
}
}
// Context
public class ShoppingCart
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void Checkout(decimal amount)
{
_paymentStrategy.Pay(amount);
}
}
// Usage
var cart = new ShoppingCart();
cart.SetPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.Checkout(99.99m);
cart.SetPaymentStrategy(new PayPalPayment("user@example.com"));
cart.Checkout(49.99m);
TypeScript with Functional Approach
// Strategy as function type
type CompressionStrategy = (data: string) => string;
// Concrete strategies as functions
const zipCompression: CompressionStrategy = (data) => {
return `ZIP compressed: ${data}`;
};
const gzipCompression: CompressionStrategy = (data) => {
return `GZIP compressed: ${data}`;
};
const lzmaCompression: CompressionStrategy = (data) => {
return `LZMA compressed: ${data}`;
};
// Context
class FileCompressor {
private strategy: CompressionStrategy;
constructor(strategy: CompressionStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: CompressionStrategy): void {
this.strategy = strategy;
}
compress(data: string): string {
return this.strategy(data);
}
}
// Usage
const compressor = new FileCompressor(zipCompression);
console.log(compressor.compress("data")); // ZIP compressed: data
compressor.setStrategy(gzipCompression);
console.log(compressor.compress("data")); // GZIP compressed: data
Python with Duck Typing
# Strategy classes (no interface needed in Python)
class QuickSort:
def sort(self, data: list) -> list:
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)
class BubbleSort:
def sort(self, data: list) -> list:
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
class MergeSort:
def sort(self, data: list) -> list:
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, right: list) -> list:
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
# Context
class Sorter:
def __init__(self, strategy):
self._strategy = strategy
def set_strategy(self, strategy):
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(QuickSort())
print(sorter.sort(data)) # Quick sorted
sorter.set_strategy(BubbleSort())
print(sorter.sort(data)) # Bubble sorted
Best Practices
- Singleton: Use dependency injection instead when possible
- Factory: Prefer factory methods for simple cases, abstract factory for families
- Observer: Clean up subscriptions to prevent memory leaks
- Strategy: Use functional approach in languages supporting first-class functions
Key Takeaways
- Singleton ensures single instance but hinders testability (prefer DI)
- Factory pattern abstracts object creation, improving flexibility
- Observer enables loose coupling between subject and observers
- Strategy allows runtime algorithm selection without conditional logic
- Design patterns solve recurring problems but shouldn't be overused
Next Steps
- Learn Decorator pattern for adding behavior dynamically
- Explore Command pattern for undo/redo functionality
- Study Repository pattern for data access abstraction
- Master CQRS pattern for separating read/write operations
Additional Resources
Patterns are solutions, not rules.