Logo

Part 3: Behavioral Design Patterns

2 min read
Lesson slides
1 / 13

Part 3: Behavioral Design Patterns

Observer, Strategy, and Command

Purpose: Focus on communication between objects and the assignment of responsibilities.

3.1 Observer Pattern

Intuition: Defines a one-to-many dependency so that when one object changes state, all dependents are notified. Like a newsletter subscription system.

classDiagram
    class Subject {
        -observers: List~Observer~
        +attach(observer: Observer)
        +detach(observer: Observer)
        +notify()
    }
    
    class Observer {
        <<interface>>
        +update(data)
    }
    
    class ConcreteSubject {
        -state
        +getState()
        +setState(state)
    }
    
    class ConcreteObserverA {
        +update(data)
    }
    
    class ConcreteObserverB {
        +update(data)
    }
    
    Subject <|-- ConcreteSubject
    Observer <|.. ConcreteObserverA
    Observer <|.. ConcreteObserverB
    Subject --> Observer

✅ When to Use:

  • Event handling systems (GUI, user interactions)
  • Model-View architectures (MVC, MVP, MVVM)
  • When changes to one object require updating multiple objects
  • Implementing distributed event handling
  • Real-time data updates (stock prices, news feeds)

Code Example:

from abc import ABC, abstractmethod
 
class Observer(ABC):
    @abstractmethod
    def update(self, message):
        pass
 
class Subject:
    def __init__(self):
        self._observers = []
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def detach(self, observer):
        self._observers.remove(observer)
    
    def notify(self, message):
        for observer in self._observers:
            observer.update(message)
 
class WeatherStation(Subject):
    def __init__(self):
        super().__init__()
        self._temperature = 0
    
    @property
    def temperature(self):
        return self._temperature
    
    @temperature.setter
    def temperature(self, temp):
        self._temperature = temp
        self.notify(f"Temperature changed to {temp}°C")
 
class Display(Observer):
    def __init__(self, name):
        self.name = name
    
    def update(self, message):
        print(f"{self.name} received: {message}")
 
# Usage
weather_station = WeatherStation()
phone_display = Display("Phone")
computer_display = Display("Computer")
 
weather_station.attach(phone_display)
weather_station.attach(computer_display)
 
weather_station.temperature = 25  # Both displays get notified
weather_station.temperature = 30  # Both displays get notified again

3.2 Strategy Pattern

Intuition: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Like choosing different routes to reach the same destination.

classDiagram
    class Context {
        -strategy: Strategy
        +setStrategy(strategy: Strategy)
        +executeStrategy()
    }
    
    class Strategy {
        <<interface>>
        +execute()
    }
    
    class ConcreteStrategyA {
        +execute()
    }
    
    class ConcreteStrategyB {
        +execute()
    }
    
    class ConcreteStrategyC {
        +execute()
    }
    
    Context --> Strategy
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB
    Strategy <|.. ConcreteStrategyC

✅ When to Use:

  • Multiple ways to perform the same task
  • Avoiding large conditional statements for algorithm selection
  • Runtime algorithm switching based on conditions
  • When algorithms have different performance characteristics
  • Plugin architectures where behavior can be swapped

Code Example:

from abc import ABC, abstractmethod
 
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass
 
class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def pay(self, amount):
        return f"Paid ${amount} using Credit Card ending in {self.card_number[-4:]}"
 
class PayPalPayment(PaymentStrategy):
    def __init__(self, email):
        self.email = email
    
    def pay(self, amount):
        return f"Paid ${amount} using PayPal account {self.email}"
 
class CryptoPayment(PaymentStrategy):
    def __init__(self, wallet_address):
        self.wallet_address = wallet_address
    
    def pay(self, amount):
        return f"Paid ${amount} using Crypto wallet {self.wallet_address[:8]}..."
 
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None
    
    def add_item(self, item, price):
        self.items.append((item, price))
    
    def set_payment_strategy(self, strategy):
        self.payment_strategy = strategy
    
    def checkout(self):
        total = sum(price for item, price in self.items)
        if self.payment_strategy:
            return self.payment_strategy.pay(total)
        return "No payment method selected"
 
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 1000)
cart.add_item("Mouse", 25)
 
# Try different payment strategies
credit_card = CreditCardPayment("1234567890123456")
paypal = PayPalPayment("[email protected]")
 
cart.set_payment_strategy(credit_card)
print(cart.checkout())  # Paid $1025 using Credit Card ending in 3456
 
cart.set_payment_strategy(paypal)
print(cart.checkout())  # Paid $1025 using PayPal account [email protected]

3.3 Command Pattern

Intuition: Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, and support undo. Like a remote control with different buttons.

classDiagram
    class Invoker {
        -command: Command
        +setCommand(command: Command)
        +executeCommand()
    }
    
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    
    class ConcreteCommand {
        -receiver: Receiver
        -state
        +execute()
        +undo()
    }
    
    class Receiver {
        +action()
    }
    
    class Client {
        +main()
    }
    
    Invoker --> Command
    Command <|.. ConcreteCommand
    ConcreteCommand --> Receiver
    Client --> ConcreteCommand
    Client --> Receiver
    Client --> Invoker

✅ When to Use:

  • Undo/Redo functionality in applications
  • Queuing operations for later execution
  • Logging and auditing operations
  • Macro recording and playback
  • Transactional behavior and rollback

Code Example:

from abc import ABC, abstractmethod
 
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass
 
class Light:
    def __init__(self, location):
        self.location = location
        self.is_on = False
    
    def turn_on(self):
        self.is_on = True
        return f"{self.location} light is ON"
    
    def turn_off(self):
        self.is_on = False
        return f"{self.location} light is OFF"
 
class LightOnCommand(Command):
    def __init__(self, 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):
        self.light = light
    
    def execute(self):
        return self.light.turn_off()
    
    def undo(self):
        return self.light.turn_on()
 
class RemoteControl:
    def __init__(self):
        self.commands = {}
        self.last_command = None
    
    def set_command(self, slot, command):
        self.commands[slot] = command
    
    def press_button(self, slot):
        if slot in self.commands:
            result = self.commands[slot].execute()
            self.last_command = self.commands[slot]
            return result
        return "No command set for this slot"
    
    def press_undo(self):
        if self.last_command:
            return self.last_command.undo()
        return "No command to undo"
 
# Usage
living_room_light = Light("Living Room")
bedroom_light = Light("Bedroom")
 
living_room_on = LightOnCommand(living_room_light)
living_room_off = LightOffCommand(living_room_light)
bedroom_on = LightOnCommand(bedroom_light)
 
remote = RemoteControl()
remote.set_command(1, living_room_on)
remote.set_command(2, living_room_off)
remote.set_command(3, bedroom_on)
 
print(remote.press_button(1))  # Living Room light is ON
print(remote.press_button(3))  # Bedroom light is ON
print(remote.press_undo())     # Bedroom light is OFF