Gang of Four Design Patterns

23 Classic Patterns with Python Implementations

About These Patterns:

The 23 classic design patterns from "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, Helm, Johnson, and Vlissides (1994).

Categories:

Pattern Summary

Pattern Category Intent
SingletonCreationalEnsure a class has only one instance
Factory MethodCreationalDefine interface for creating objects
Abstract FactoryCreationalCreate families of related objects
BuilderCreationalConstruct complex objects step by step
PrototypeCreationalClone existing objects
AdapterStructuralMake incompatible interfaces work together
BridgeStructuralSeparate abstraction from implementation
CompositeStructuralCompose objects into tree structures
DecoratorStructuralAdd behavior to objects dynamically
FacadeStructuralSimplified interface to complex subsystem
FlyweightStructuralShare common state among many objects
ProxyStructuralProvide substitute for another object
Chain of ResponsibilityBehavioralPass request along chain of handlers
CommandBehavioralEncapsulate request as object
IteratorBehavioralAccess elements sequentially
MediatorBehavioralReduce coupling via centralized communication
MementoBehavioralSave and restore object state
ObserverBehavioralSubscription mechanism for notifications
StateBehavioralAlter behavior when state changes
StrategyBehavioralMake algorithms interchangeable
Template MethodBehavioralDefine skeleton, override steps
VisitorBehavioralSeparate algorithm from structure
InterpreterBehavioralDefine grammar and interpreter

CREATIONAL PATTERNS

Object Creation Mechanisms

1. Singleton Pattern

Intent: Ensure a class has only one instance and provide global access to it.

Use Cases: Trade-offs:
✅ Global access point, Lazy initialization
❌ Global state (testing difficulty), Thread safety concerns
import threading

class Singleton:
    """Singleton Pattern - Only one instance exists"""
    _instance = None
    _initialized = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        # Only initialize once
        if not Singleton._initialized:
            self.value = None
            Singleton._initialized = True

    def set_value(self, value):
        self.value = value

    def get_value(self):
        return self.value


# Thread-safe Singleton (better for production)
class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # Double-checked locking
                    cls._instance = super().__new__(cls)
        return cls._instance


# Usage
s1 = Singleton()
s1.set_value("Database Connection")
s2 = Singleton()
print(f"s1 value: {s1.get_value()}")  # Database Connection
print(f"s2 value: {s2.get_value()}")  # Database Connection
print(f"s1 is s2: {s1 is s2}")  # True - same instance

2. Factory Method Pattern

Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate.

Use Cases: Trade-offs:
✅ Open/Closed Principle, Single Responsibility
❌ More classes to maintain
from abc import ABC, abstractmethod

class Vehicle(ABC):
    """Abstract Product"""
    @abstractmethod
    def deliver(self):
        pass


class Truck(Vehicle):
    """Concrete Product"""
    def deliver(self):
        return "Deliver by land in a truck"


class Ship(Vehicle):
    """Concrete Product"""
    def deliver(self):
        return "Deliver by sea in a ship"


class Logistics(ABC):
    """Creator - Factory Method Pattern"""

    @abstractmethod
    def create_transport(self) -> Vehicle:
        """Factory method - subclasses override this"""
        pass

    def plan_delivery(self):
        transport = self.create_transport()
        return transport.deliver()


class RoadLogistics(Logistics):
    """Concrete Creator"""
    def create_transport(self) -> Vehicle:
        return Truck()


class SeaLogistics(Logistics):
    """Concrete Creator"""
    def create_transport(self) -> Vehicle:
        return Ship()


# Usage
road = RoadLogistics()
print(road.plan_delivery())  # Deliver by land in a truck
sea = SeaLogistics()
print(sea.plan_delivery())  # Deliver by sea in a ship

3. Abstract Factory Pattern

Intent: Create families of related objects without specifying their concrete classes.

Use Cases: Trade-offs:
✅ Ensures product compatibility, Isolates concrete classes
❌ Complexity increases with new product types
from abc import ABC, abstractmethod

class Button(ABC):
    """Abstract Product A"""
    @abstractmethod
    def paint(self):
        pass


class Checkbox(ABC):
    """Abstract Product B"""
    @abstractmethod
    def paint(self):
        pass


class WindowsButton(Button):
    def paint(self):
        return "Render Windows button"


class MacButton(Button):
    def paint(self):
        return "Render Mac button"


class WindowsCheckbox(Checkbox):
    def paint(self):
        return "Render Windows checkbox"


class MacCheckbox(Checkbox):
    def paint(self):
        return "Render Mac checkbox"


class GUIFactory(ABC):
    """Abstract Factory"""

    @abstractmethod
    def create_button(self) -> Button:
        pass

    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass


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()


def render_ui(factory: GUIFactory):
    button = factory.create_button()
    checkbox = factory.create_checkbox()
    print(f"  {button.paint()}")
    print(f"  {checkbox.paint()}")


# Usage
print("Windows UI:")
render_ui(WindowsFactory())
print("Mac UI:")
render_ui(MacFactory())

4. Builder Pattern

Intent: Construct complex objects step by step, allowing different representations.

Use Cases: Trade-offs:
✅ Readable construction, Reuse construction code, Step-by-step
❌ More code than simple constructor
class Pizza:
    """Complex Product"""
    def __init__(self):
        self.dough = None
        self.sauce = None
        self.toppings = []

    def __str__(self):
        return f"Pizza(dough={self.dough}, sauce={self.sauce}, toppings={self.toppings})"


class PizzaBuilder:
    """Builder Pattern"""

    def __init__(self):
        self.pizza = Pizza()

    def set_dough(self, dough):
        self.pizza.dough = dough
        return self  # Fluent interface

    def set_sauce(self, sauce):
        self.pizza.sauce = sauce
        return self

    def add_topping(self, topping):
        self.pizza.toppings.append(topping)
        return self

    def build(self):
        return self.pizza


# Director (optional) - encapsulates construction steps
class PizzaDirector:
    def __init__(self, builder: PizzaBuilder):
        self.builder = builder

    def make_margherita(self):
        return (self.builder
                .set_dough("thin")
                .set_sauce("tomato")
                .add_topping("mozzarella")
                .add_topping("basil")
                .build())

    def make_pepperoni(self):
        return (self.builder
                .set_dough("thick")
                .set_sauce("tomato")
                .add_topping("mozzarella")
                .add_topping("pepperoni")
                .build())


# Usage
# Direct building
custom_pizza = (PizzaBuilder()
                .set_dough("regular")
                .set_sauce("white")
                .add_topping("chicken")
                .add_topping("mushrooms")
                .build())
print(f"Custom: {custom_pizza}")

# Using Director
director = PizzaDirector(PizzaBuilder())
margherita = director.make_margherita()
print(f"Margherita: {margherita}")

5. Prototype Pattern

Intent: Create new objects by copying an existing object (prototype).

Use Cases: Trade-offs:
✅ Avoid repeated initialization, Dynamic object creation
❌ Cloning complex objects with circular refs is tricky
import copy
from abc import ABC, abstractmethod

class Prototype(ABC):
    """Prototype Pattern"""

    @abstractmethod
    def clone(self):
        pass


class Document(Prototype):
    def __init__(self, title, content, metadata=None):
        self.title = title
        self.content = content
        self.metadata = metadata or {}

    def clone(self):
        # Shallow copy
        return copy.copy(self)

    def deep_clone(self):
        # Deep copy (for nested objects)
        return copy.deepcopy(self)

    def __str__(self):
        return f"Document('{self.title}', content_len={len(self.content)}, metadata={self.metadata})"


# Usage
original = Document("Template", "Lorem ipsum...", {"author": "Admin", "tags": ["template"]})
print(f"Original: {original}")

# Clone and modify
clone = original.clone()
clone.title = "New Document"
clone.metadata["author"] = "User"  # Note: shallow copy shares metadata dict!
print(f"Clone: {clone}")
print(f"Original after clone modification: {original}")  # Metadata changed!

# Deep clone to avoid shared references
deep_clone = original.deep_clone()
deep_clone.title = "Deep Clone"
deep_clone.metadata["author"] = "Another User"
print(f"Deep Clone: {deep_clone}")
print(f"Original after deep clone: {original}")  # Metadata unchanged

STRUCTURAL PATTERNS

Object Composition and Relationships

6. Adapter Pattern

Intent: Convert the interface of a class into another interface clients expect.

Use Cases: Trade-offs:
✅ Reuse existing code, Single Responsibility Principle
❌ Code complexity increases
class EuropeanSocket:
    """Existing class with incompatible interface"""
    def voltage(self):
        return 230

    def live(self):
        return 1

    def neutral(self):
        return -1


class USASocket(ABC):
    """Target interface"""
    @abstractmethod
    def voltage(self):
        pass

    @abstractmethod
    def live(self):
        pass

    @abstractmethod
    def neutral(self):
        pass


class Adapter(USASocket):
    """Adapter Pattern"""

    def __init__(self, socket: EuropeanSocket):
        self.socket = socket

    def voltage(self):
        return 110  # Convert 230V to 110V

    def live(self):
        return self.socket.live()

    def neutral(self):
        return self.socket.neutral()


# Usage
eu_socket = EuropeanSocket()
adapter = Adapter(eu_socket)
print(f"EU voltage: {eu_socket.voltage()}V")  # 230V
print(f"Adapted voltage: {adapter.voltage()}V")  # 110V

7. Bridge Pattern

Intent: Separate abstraction from implementation so they can vary independently.

Use Cases: Trade-offs:
✅ Decouple abstraction from implementation, Add new abstractions/implementations independently
❌ Increased complexity
class Device(ABC):
    """Implementation interface"""
    @abstractmethod
    def is_enabled(self):
        pass

    @abstractmethod
    def enable(self):
        pass

    @abstractmethod
    def disable(self):
        pass

    @abstractmethod
    def get_volume(self):
        pass

    @abstractmethod
    def set_volume(self, percent):
        pass


class TV(Device):
    def __init__(self):
        self._on = False
        self._volume = 50

    def is_enabled(self):
        return self._on

    def enable(self):
        self._on = True

    def disable(self):
        self._on = False

    def get_volume(self):
        return self._volume

    def set_volume(self, percent):
        self._volume = percent


class RemoteControl:
    """Abstraction - Bridge Pattern"""

    def __init__(self, device: Device):
        self.device = device

    def toggle_power(self):
        if self.device.is_enabled():
            self.device.disable()
        else:
            self.device.enable()

    def volume_up(self):
        self.device.set_volume(self.device.get_volume() + 10)

    def volume_down(self):
        self.device.set_volume(self.device.get_volume() - 10)


class AdvancedRemote(RemoteControl):
    """Extended abstraction"""
    def mute(self):
        self.device.set_volume(0)


# Usage
tv = TV()
remote = RemoteControl(tv)
remote.toggle_power()
remote.volume_up()
print(f"TV: On={tv.is_enabled()}, Volume={tv.get_volume()}")

8. Composite Pattern

Intent: Compose objects into tree structures to represent part-whole hierarchies.

Use Cases: Trade-offs:
✅ Uniform interface for tree structures, Easy to add new component types
❌ Can make design overly general
class Graphic(ABC):
    """Component"""
    @abstractmethod
    def draw(self):
        pass


class Circle(Graphic):
    """Leaf"""
    def __init__(self, name):
        self.name = name

    def draw(self):
        return f"Circle({self.name})"


class Square(Graphic):
    """Leaf"""
    def __init__(self, name):
        self.name = name

    def draw(self):
        return f"Square({self.name})"


class CompositeGraphic(Graphic):
    """Composite"""

    def __init__(self, name):
        self.name = name
        self.children = []

    def add(self, graphic: Graphic):
        self.children.append(graphic)

    def remove(self, graphic: Graphic):
        self.children.remove(graphic)

    def draw(self):
        results = [f"Group({self.name}): ["]
        for child in self.children:
            results.append(f"  {child.draw()}")
        results.append("]")
        return "\n".join(results)


# Usage - Build tree structure
all_graphics = CompositeGraphic("All")
group1 = CompositeGraphic("Group1")
group1.add(Circle("c1"))
group1.add(Square("s1"))
group2 = CompositeGraphic("Group2")
group2.add(Circle("c2"))
all_graphics.add(group1)
all_graphics.add(group2)
all_graphics.add(Circle("c3"))

print(all_graphics.draw())

9. Decorator Pattern

Intent: Attach additional responsibilities to an object dynamically.

Use Cases: Trade-offs:
✅ More flexible than inheritance, Composable decorators
❌ Many small objects, Order of decorators matters
class Coffee(ABC):
    """Component"""
    @abstractmethod
    def cost(self):
        pass

    @abstractmethod
    def description(self):
        pass


class SimpleCoffee(Coffee):
    """Concrete Component"""
    def cost(self):
        return 2.0

    def description(self):
        return "Simple coffee"


class CoffeeDecorator(Coffee):
    """Base Decorator"""

    def __init__(self, coffee: Coffee):
        self._coffee = coffee

    @abstractmethod
    def cost(self):
        pass

    @abstractmethod
    def description(self):
        pass


class Milk(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.5

    def description(self):
        return self._coffee.description() + ", milk"


class Sugar(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.2

    def description(self):
        return self._coffee.description() + ", sugar"


class Whip(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.7

    def description(self):
        return self._coffee.description() + ", whip"


# Usage - Build decorated object
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost()}")

coffee_with_milk = Milk(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost()}")

coffee_deluxe = Whip(Sugar(Milk(SimpleCoffee())))
print(f"{coffee_deluxe.description()}: ${coffee_deluxe.cost()}")

10. Facade Pattern

Intent: Provide a simplified interface to a complex subsystem.

Use Cases: Trade-offs:
✅ Simplifies interface, Decouples client from subsystem
❌ Can become a "god object"
class CPU:
    """Complex subsystem class"""
    def freeze(self):
        return "CPU frozen"

    def jump(self, position):
        return f"CPU jumping to {position}"

    def execute(self):
        return "CPU executing"


class Memory:
    """Complex subsystem class"""
    def load(self, position, data):
        return f"Memory loaded {data} at {position}"


class HardDrive:
    """Complex subsystem class"""
    def read(self, lba, size):
        return f"HardDrive read {size} bytes from {lba}"


class ComputerFacade:
    """Facade Pattern"""

    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()

    def start(self):
        """Simple interface to complex boot sequence"""
        results = []
        results.append(self.cpu.freeze())
        results.append(self.memory.load("0x00", "boot data"))
        results.append(self.hard_drive.read("0", 1024))
        results.append(self.cpu.jump("0x00"))
        results.append(self.cpu.execute())
        return " → ".join(results)


# Usage
computer = ComputerFacade()
print(computer.start())

11. Flyweight Pattern

Intent: Use sharing to support large numbers of fine-grained objects efficiently.

Use Cases: Trade-offs:
✅ Massive memory savings, Performance improvement
❌ Complexity in separating intrinsic/extrinsic state
class TreeType:
    """Flyweight - shared immutable state"""

    def __init__(self, name, color, texture):
        self.name = name
        self.color = color
        self.texture = texture

    def draw(self, x, y):
        return f"Draw {self.color} {self.name} at ({x},{y})"


class TreeFactory:
    """Flyweight Factory - manages shared objects"""
    _tree_types = {}

    @staticmethod
    def get_tree_type(name, color, texture):
        key = (name, color, texture)
        if key not in TreeFactory._tree_types:
            TreeFactory._tree_types[key] = TreeType(name, color, texture)
        return TreeFactory._tree_types[key]

    @staticmethod
    def get_count():
        return len(TreeFactory._tree_types)


class Tree:
    """Context object - extrinsic state"""
    def __init__(self, x, y, tree_type: TreeType):
        self.x = x
        self.y = y
        self.type = tree_type

    def draw(self):
        return self.type.draw(self.x, self.y)


# Usage - Create forest with many trees
forest = []
for i in range(1000):
    # Only a few unique tree types (shared)
    tree_type = TreeFactory.get_tree_type(
        "Oak" if i % 2 == 0 else "Pine",
        "Green",
        "bark.jpg"
    )
    forest.append(Tree(i, i * 2, tree_type))

print(f"Created {len(forest)} trees")
print(f"Unique TreeType objects: {TreeFactory.get_count()}")
print(f"Memory saved: {len(forest) - TreeFactory.get_count()} object reuses")

12. Proxy Pattern

Intent: Provide a surrogate or placeholder for another object to control access to it.

Types: Trade-offs:
✅ Control over object access, Lazy initialization
❌ Response time delay on first access
class Image(ABC):
    """Subject"""
    @abstractmethod
    def display(self):
        pass


class RealImage(Image):
    """Real Subject - expensive to create"""
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()

    def _load_from_disk(self):
        print(f"[RealImage] Loading {self.filename} from disk (expensive!)")

    def display(self):
        return f"Displaying {self.filename}"


class ImageProxy(Image):
    """Proxy Pattern - Virtual Proxy (lazy initialization)"""

    def __init__(self, filename):
        self.filename = filename
        self._real_image = None

    def display(self):
        if self._real_image is None:
            # Lazy initialization
            self._real_image = RealImage(self.filename)
        return self._real_image.display()


# Usage
print("Creating proxies (no loading yet):")
image1 = ImageProxy("photo1.jpg")
image2 = ImageProxy("photo2.jpg")

print("\nDisplaying image1 (loads now):")
print(image1.display())

print("\nDisplaying image1 again (already loaded):")
print(image1.display())

print("\nDisplaying image2 (loads now):")
print(image2.display())

BEHAVIORAL PATTERNS

Communication Between Objects

13. Chain of Responsibility Pattern

Intent: Pass request along a chain of handlers until one handles it.

Use Cases: Trade-offs:
✅ Decouples sender from receiver, Dynamic chain modification
❌ Request might go unhandled, Performance (traversing chain)
class Handler(ABC):
    """Handler interface"""
    def __init__(self):
        self._next = None

    def set_next(self, handler):
        self._next = handler
        return handler

    @abstractmethod
    def handle(self, request):
        pass


class AuthHandler(Handler):
    def handle(self, request):
        if "auth_token" not in request:
            return "AuthHandler: Rejected - no auth token"
        print("AuthHandler: Passed")
        return self._next.handle(request) if self._next else "OK"


class ValidationHandler(Handler):
    def handle(self, request):
        if "data" not in request or not request["data"]:
            return "ValidationHandler: Rejected - no data"
        print("ValidationHandler: Passed")
        return self._next.handle(request) if self._next else "OK"


class LoggingHandler(Handler):
    def handle(self, request):
        print(f"LoggingHandler: Logging request {request.get('id', 'unknown')}")
        return self._next.handle(request) if self._next else "OK"


# Usage - Build chain
auth = AuthHandler()
validation = ValidationHandler()
logging = LoggingHandler()
auth.set_next(validation).set_next(logging)

# Test requests
print("Request 1 (valid):")
result = auth.handle({"id": 1, "auth_token": "abc", "data": "payload"})
print(f"Result: {result}\n")

print("Request 2 (no auth):")
result = auth.handle({"id": 2, "data": "payload"})
print(f"Result: {result}")

14. Command Pattern

Intent: Encapsulate a request as an object, allowing parameterization and queuing.

Use Cases: Trade-offs:
✅ Decouples invoker from receiver, Easy to add new commands, Undo/redo support
❌ More classes
class Command(ABC):
    """Command interface"""
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass


class Light:
    """Receiver"""
    def __init__(self):
        self.is_on = False

    def turn_on(self):
        self.is_on = True
        return "Light is ON"

    def turn_off(self):
        self.is_on = False
        return "Light is OFF"


class LightOnCommand(Command):
    """Concrete Command"""

    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        return self.light.turn_on()

    def undo(self):
        return self.light.turn_off()


class LightOffCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        return self.light.turn_off()

    def undo(self):
        return self.light.turn_on()


class RemoteControl:
    """Invoker"""
    def __init__(self):
        self.history = []

    def submit(self, command: Command):
        result = command.execute()
        self.history.append(command)
        return result

    def undo(self):
        if self.history:
            command = self.history.pop()
            return command.undo()
        return "Nothing to undo"


# Usage
light = Light()
remote = RemoteControl()

print(remote.submit(LightOnCommand(light)))
print(f"Light state: {light.is_on}")
print(remote.submit(LightOffCommand(light)))
print(f"Light state: {light.is_on}")
print(remote.undo())
print(f"Light state after undo: {light.is_on}")

15. Iterator Pattern

Intent: Provide a way to access elements of a collection sequentially without exposing the underlying representation.

Use Cases: Note: Python has built-in iterator protocol (__iter__, __next__)
class Book:
    def __init__(self, title):
        self.title = title


class BookCollection:
    """Iterator Pattern"""

    def __init__(self):
        self._books = []

    def add_book(self, book: Book):
        self._books.append(book)

    def __iter__(self):
        """Python's iterator protocol"""
        return iter(self._books)

    # Could also implement custom iterator
    def reverse_iterator(self):
        return reversed(self._books)


# Usage
collection = BookCollection()
collection.add_book(Book("Design Patterns"))
collection.add_book(Book("Clean Code"))
collection.add_book(Book("Refactoring"))

print("Forward iteration:")
for book in collection:
    print(f"  {book.title}")

print("Reverse iteration:")
for book in collection.reverse_iterator():
    print(f"  {book.title}")

16. Mediator Pattern

Intent: Define an object that encapsulates how a set of objects interact.

Use Cases: Trade-offs:
✅ Reduces coupling between colleagues, Centralizes control
❌ Mediator can become complex (god object)
class Mediator(ABC):
    """Mediator interface"""
    @abstractmethod
    def notify(self, sender, event):
        pass


class ChatRoomMediator(Mediator):
    """Concrete Mediator"""

    def __init__(self):
        self.users = []

    def register_user(self, user):
        self.users.append(user)

    def notify(self, sender, message):
        for user in self.users:
            if user != sender:
                user.receive(f"{sender.name}: {message}")


class User:
    """Colleague"""
    def __init__(self, name, mediator: ChatRoomMediator):
        self.name = name
        self.mediator = mediator
        self.mediator.register_user(self)

    def send(self, message):
        print(f"{self.name} sends: {message}")
        self.mediator.notify(self, message)

    def receive(self, message):
        print(f"{self.name} receives: {message}")


# Usage
chat_room = ChatRoomMediator()
alice = User("Alice", chat_room)
bob = User("Bob", chat_room)
charlie = User("Charlie", chat_room)

alice.send("Hello everyone!")

17. Memento Pattern

Intent: Capture and externalize an object's internal state without violating encapsulation.

Use Cases: Trade-offs:
✅ Doesn't violate encapsulation, Simplifies originator
❌ Memory intensive (many snapshots)
class EditorMemento:
    """Memento - stores state"""
    def __init__(self, content, cursor):
        self._content = content
        self._cursor = cursor

    def get_content(self):
        return self._content

    def get_cursor(self):
        return self._cursor


class Editor:
    """Originator"""

    def __init__(self):
        self._content = ""
        self._cursor = 0

    def type(self, text):
        self._content += text
        self._cursor = len(self._content)

    def save(self):
        """Create memento"""
        return EditorMemento(self._content, self._cursor)

    def restore(self, memento: EditorMemento):
        """Restore from memento"""
        self._content = memento.get_content()
        self._cursor = memento.get_cursor()

    def get_content(self):
        return self._content


class History:
    """Caretaker - manages mementos"""
    def __init__(self):
        self._mementos = []

    def push(self, memento):
        self._mementos.append(memento)

    def pop(self):
        if self._mementos:
            return self._mementos.pop()
        return None


# Usage
editor = Editor()
history = History()

editor.type("Hello ")
history.push(editor.save())
print(f"Content: '{editor.get_content()}'")

editor.type("World")
history.push(editor.save())
print(f"Content: '{editor.get_content()}'")

editor.type("!!!")
print(f"Content: '{editor.get_content()}'")

# Undo
memento = history.pop()
if memento:
    editor.restore(memento)
    print(f"After undo: '{editor.get_content()}'")

18. Observer Pattern

Intent: Define a one-to-many dependency so that when one object changes state, all dependents are notified.

Use Cases: Trade-offs:
✅ Loose coupling, Dynamic relationships
❌ Unexpected updates, Memory leaks (forgotten unsubscribe)
class Subject:
    """Subject (Observable)"""

    def __init__(self):
        self._observers = []
        self._state = None

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    def set_state(self, state):
        self._state = state
        self.notify()

    def get_state(self):
        return self._state


class Observer(ABC):
    """Observer interface"""
    @abstractmethod
    def update(self, subject: Subject):
        pass


class ConcreteObserverA(Observer):
    def update(self, subject: Subject):
        print(f"ObserverA: Reacted to state change: {subject.get_state()}")


class ConcreteObserverB(Observer):
    def update(self, subject: Subject):
        print(f"ObserverB: Reacted to state change: {subject.get_state()}")


# Usage
subject = Subject()
observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

subject.attach(observer_a)
subject.attach(observer_b)

print("Setting state to 123:")
subject.set_state(123)

print("\nDetaching ObserverA")
subject.detach(observer_a)

print("Setting state to 456:")
subject.set_state(456)

19. State Pattern

Intent: Allow an object to alter its behavior when its internal state changes.

Use Cases: Trade-offs:
✅ Organizes state-specific code, Makes state transitions explicit
❌ Many classes for simple state machines
class State(ABC):
    """State interface"""
    @abstractmethod
    def handle(self, context):
        pass


class Context:
    """Context"""

    def __init__(self, state: State):
        self._state = state

    def set_state(self, state: State):
        print(f"Context: Transitioning to {state.__class__.__name__}")
        self._state = state

    def request(self):
        self._state.handle(self)


class StartState(State):
    def handle(self, context: Context):
        print("StartState: Handling request")
        context.set_state(RunningState())


class RunningState(State):
    def handle(self, context: Context):
        print("RunningState: Handling request")
        context.set_state(StopState())


class StopState(State):
    def handle(self, context: Context):
        print("StopState: Handling request (no transition)")


# Usage
context = Context(StartState())
context.request()
context.request()
context.request()

20. Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.

Use Cases: Trade-offs:
✅ Easy to switch algorithms, Isolates algorithm implementation
❌ Clients must know strategies, More objects
class PaymentStrategy(ABC):
    """Strategy interface"""
    @abstractmethod
    def pay(self, amount):
        pass


class CreditCardStrategy(PaymentStrategy):
    def __init__(self, card_number):
        self.card_number = card_number

    def pay(self, amount):
        return f"Paid ${amount} using Credit Card {self.card_number}"


class PayPalStrategy(PaymentStrategy):
    def __init__(self, email):
        self.email = email

    def pay(self, amount):
        return f"Paid ${amount} using PayPal {self.email}"


class BitcoinStrategy(PaymentStrategy):
    def __init__(self, wallet):
        self.wallet = wallet

    def pay(self, amount):
        return f"Paid ${amount} using Bitcoin {self.wallet}"


class ShoppingCart:
    """Context"""

    def __init__(self, strategy: PaymentStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: PaymentStrategy):
        self._strategy = strategy

    def checkout(self, amount):
        return self._strategy.pay(amount)


# Usage
cart = ShoppingCart(CreditCardStrategy("1234-5678"))
print(cart.checkout(100))

cart.set_strategy(PayPalStrategy("user@example.com"))
print(cart.checkout(50))

cart.set_strategy(BitcoinStrategy("1A2B3C..."))
print(cart.checkout(200))

21. Template Method Pattern

Intent: Define the skeleton of an algorithm, letting subclasses override specific steps.

Use Cases: Trade-offs:
✅ Reuses common code, Controls extension points
❌ Liskov Substitution Principle violations possible
class DataMiner(ABC):
    """Template Method Pattern"""

    def mine(self, path):
        """Template method - defines skeleton"""
        data = self.open_file(path)
        raw_data = self.extract_data(data)
        analyzed = self.analyze_data(raw_data)
        self.send_report(analyzed)
        self.close_file(data)

    @abstractmethod
    def open_file(self, path):
        pass

    @abstractmethod
    def extract_data(self, file):
        pass

    def analyze_data(self, data):
        """Hook - can be overridden"""
        return f"Analyzed: {data}"

    @abstractmethod
    def send_report(self, analysis):
        pass

    def close_file(self, file):
        """Hook"""
        return "File closed"


class CSVDataMiner(DataMiner):
    def open_file(self, path):
        return f"CSV file opened: {path}"

    def extract_data(self, file):
        return "CSV data extracted"

    def send_report(self, analysis):
        print(f"CSV Report: {analysis}")


class JSONDataMiner(DataMiner):
    def open_file(self, path):
        return f"JSON file opened: {path}"

    def extract_data(self, file):
        return "JSON data extracted"

    def analyze_data(self, data):
        # Custom analysis
        return f"JSON-specific analysis: {data}"

    def send_report(self, analysis):
        print(f"JSON Report: {analysis}")


# Usage
print("CSV Mining:")
csv_miner = CSVDataMiner()
csv_miner.mine("data.csv")

print("\nJSON Mining:")
json_miner = JSONDataMiner()
json_miner.mine("data.json")

22. Visitor Pattern

Intent: Separate an algorithm from the object structure it operates on.

Use Cases: Trade-offs:
✅ Easy to add new operations, Gathers related operations
❌ Hard to add new element classes, Breaks encapsulation
class Shape(ABC):
    """Element"""
    @abstractmethod
    def accept(self, visitor):
        pass


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def accept(self, visitor):
        visitor.visit_circle(self)


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def accept(self, visitor):
        visitor.visit_rectangle(self)


class ShapeVisitor(ABC):
    """Visitor"""

    @abstractmethod
    def visit_circle(self, circle: Circle):
        pass

    @abstractmethod
    def visit_rectangle(self, rectangle: Rectangle):
        pass


class AreaCalculator(ShapeVisitor):
    def visit_circle(self, circle: Circle):
        area = 3.14 * circle.radius ** 2
        print(f"Circle area: {area:.2f}")

    def visit_rectangle(self, rectangle: Rectangle):
        area = rectangle.width * rectangle.height
        print(f"Rectangle area: {area:.2f}")


class PerimeterCalculator(ShapeVisitor):
    def visit_circle(self, circle: Circle):
        perimeter = 2 * 3.14 * circle.radius
        print(f"Circle perimeter: {perimeter:.2f}")

    def visit_rectangle(self, rectangle: Rectangle):
        perimeter = 2 * (rectangle.width + rectangle.height)
        print(f"Rectangle perimeter: {perimeter:.2f}")


# Usage
shapes = [Circle(5), Rectangle(4, 6)]

print("Calculating areas:")
area_calc = AreaCalculator()
for shape in shapes:
    shape.accept(area_calc)

print("\nCalculating perimeters:")
perim_calc = PerimeterCalculator()
for shape in shapes:
    shape.accept(perim_calc)

23. Interpreter Pattern

Intent: Define a representation for a grammar along with an interpreter.

Use Cases: Trade-offs:
✅ Easy to change/extend grammar, Easy to implement
❌ Complex grammars hard to maintain

Note: Rarely used; parsers/compilers better for complex languages
class Expression(ABC):
    """Interpreter Pattern"""

    @abstractmethod
    def interpret(self, context):
        pass


class Number(Expression):
    def __init__(self, value):
        self.value = value

    def interpret(self, context):
        return self.value


class Add(Expression):
    def __init__(self, left: Expression, right: Expression):
        self.left = left
        self.right = right

    def interpret(self, context):
        return self.left.interpret(context) + self.right.interpret(context)


class Subtract(Expression):
    def __init__(self, left: Expression, right: Expression):
        self.left = left
        self.right = right

    def interpret(self, context):
        return self.left.interpret(context) - self.right.interpret(context)


# Usage - Represent: (5 + 3) - 2
expression = Subtract(
    Add(Number(5), Number(3)),
    Number(2)
)
result = expression.interpret({})
print(f"(5 + 3) - 2 = {result}")

Interview Tips & Pattern Relationships

Most Common Patterns in Interviews:

Be ready to code 2-3 patterns from scratch!

Pattern Relationships:
When Asked "Design a System" in Interviews:
  1. Identify responsibilities → Single Responsibility Principle
  2. Look for varying behaviors → Strategy Pattern
  3. Need to extend functionality → Decorator or Chain of Responsibility
  4. Object creation complex → Factory or Builder
  5. Need to decouple components → Mediator or Observer
  6. Performance/memory concerns → Flyweight or Proxy
  7. Undo/redo functionality → Command or Memento
  8. Tree structures → Composite
  9. Legacy integration → Adapter or Facade
Key Interview Insights: