Part 3: Behavioral Design Patterns
2 min read
Lesson slides
1 / 13
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 again3.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