Design Patterns in Modern Languages: Singleton, Factory, Observer, Strategy

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

  1. Singleton: Use dependency injection instead when possible
  2. Factory: Prefer factory methods for simple cases, abstract factory for families
  3. Observer: Clean up subscriptions to prevent memory leaks
  4. 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.