Lab 12: Design Patterns

Objective

Implement the most useful Gang-of-Four design patterns in idiomatic Python: Singleton, Factory, Observer, Strategy, Command, Decorator, and Composite.

Time

30 minutes

Prerequisites

  • Lab 01 (Advanced OOP), Lab 02 (Decorators)

Tools

  • Docker image: zchencow/innozverse-python:latest


Lab Instructions

Step 1: Creational Patterns — Singleton & Factory

docker run --rm zchencow/innozverse-python:latest python3 -c "
from __future__ import annotations
from typing import ClassVar
import threading

# --- Singleton (thread-safe) ---
class Config:
    _instance: ClassVar[Config | None] = None
    _lock: ClassVar[threading.Lock] = threading.Lock()

    def __new__(cls) -> Config:
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._settings = {
                        'db_url': 'sqlite:///store.db',
                        'api_key': 'secret',
                        'debug': False,
                    }
        return cls._instance

    def get(self, key: str, default=None):
        return self._settings.get(key, default)

    def set(self, key: str, value) -> None:
        self._settings[key] = value

c1, c2 = Config(), Config()
c1.set('debug', True)
print('Same instance:', c1 is c2)
print('c2.debug:', c2.get('debug'))  # True — same object

# --- Factory Method ---
from abc import ABC, abstractmethod
from dataclasses import dataclass

class Exporter(ABC):
    @abstractmethod
    def export(self, data: list[dict]) -> str: ...

class JsonExporter(Exporter):
    def export(self, data):
        import json
        return json.dumps(data, indent=2)

class CsvExporter(Exporter):
    def export(self, data):
        if not data: return ''
        headers = ','.join(data[0].keys())
        rows = [','.join(str(v) for v in row.values()) for row in data]
        return '\n'.join([headers] + rows)

class TsvExporter(Exporter):
    def export(self, data):
        if not data: return ''
        return '\n'.join('\t'.join(str(v) for v in row.values()) for row in data)

def exporter_factory(fmt: str) -> Exporter:
    exporters = {'json': JsonExporter, 'csv': CsvExporter, 'tsv': TsvExporter}
    cls = exporters.get(fmt)
    if cls is None:
        raise ValueError(f'Unknown format: {fmt!r}. Choose from {list(exporters)}')
    return cls()

products = [
    {'id': 1, 'name': 'Surface Pro', 'price': 864.0},
    {'id': 2, 'name': 'Surface Pen', 'price': 49.99},
]

for fmt in ['json', 'csv', 'tsv']:
    exp = exporter_factory(fmt)
    result = exp.export(products)
    print(f'--- {fmt.upper()} ---')
    print(result[:80])
"

💡 Singleton ensures exactly one instance exists (useful for config, DB connections). The double-checked locking pattern (if _instance is None inside with _lock) prevents race conditions in multithreaded code without locking on every access. Factory Method decouples creation logic from usage — add a new format without changing caller code.

📸 Verified Output:


Step 2: Behavioral Patterns — Observer & Strategy

📸 Verified Output:


Steps 3–8: Command, Builder, Composite, Adapter, Template Method, Capstone

📸 Verified Output:


Summary

Pattern
Category
Python idiom

Singleton

Creational

__new__ + class var

Factory

Creational

dict dispatch + ABC

Observer

Behavioral

EventBus with subscribe/publish

Strategy

Behavioral

Protocol + runtime swap

Command

Behavioral

ABC + history list for undo

Builder

Creational

Fluent interface (method chaining)

Composite

Structural

Recursive ABC price property

Further Reading

Last updated