Behavioral Patterns
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand behavioral design patterns
- - Implement the Observer pattern
- - Implement the Strategy pattern
- - Implement the Command pattern
- - Recognize when to use each pattern
- - Apply behavioral patterns in real-world scenarios
- - Combine patterns effectively
- - Choose appropriate patterns for problems
- - Debug pattern-related issues
Lesson 25.3: Behavioral Patterns
Learning Objectives
By the end of this lesson, you will be able to:
- Understand behavioral design patterns
- Implement the Observer pattern
- Implement the Strategy pattern
- Implement the Command pattern
- Recognize when to use each pattern
- Apply behavioral patterns in real-world scenarios
- Combine patterns effectively
- Choose appropriate patterns for problems
- Debug pattern-related issues
Introduction to Behavioral Patterns
Behavioral Patterns focus on communication between objects and how they operate together. They deal with algorithms and the assignment of responsibilities between objects.
Key Behavioral Patterns:
- Observer: Notify multiple objects about state changes
- Strategy: Define a family of algorithms and make them interchangeable
- Command: Encapsulate requests as objects
- Chain of Responsibility: Pass requests along a chain of handlers
- State: Allow object to alter behavior when internal state changes
- Template Method: Define algorithm skeleton in base class
- Iterator: Provide a way to access elements sequentially
- Mediator: Define how objects interact
- Memento: Capture and restore object state
- Visitor: Separate algorithm from object structure
Benefits:
- Flexibility: Easier to modify behavior
- Reusability: Behaviors can be reused
- Maintainability: Easier to maintain
- Decoupling: Reduces dependencies between objects
Observer Pattern
Concept
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Use Cases:
- Event handling systems
- Model-View-Controller (MVC) architecture
- Publish-Subscribe systems
- Real-time data updates
- Notification systems
Basic Observer
from abc import ABC, abstractmethod
# Subject (Observable)
class Subject(ABC):
def __init__(self):
self._observers = []
def attach(self, observer):
"""Attach an observer."""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer):
"""Detach an observer."""
self._observers.remove(observer)
def notify(self, event=None):
"""Notify all observers."""
for observer in self._observers:
observer.update(event)
# Observer
class Observer(ABC):
@abstractmethod
def update(self, event):
pass
# Concrete Subject
class NewsAgency(Subject):
def __init__(self):
super().__init__()
self._news = None
def set_news(self, news):
self._news = news
self.notify(self._news)
def get_news(self):
return self._news
# Concrete Observers
class NewsChannel(Observer):
def __init__(self, name):
self.name = name
self.news = None
def update(self, event):
self.news = event
print(f"{self.name} received news: {event}")
# Usage
agency = NewsAgency()
channel1 = NewsChannel("CNN")
channel2 = NewsChannel("BBC")
agency.attach(channel1)
agency.attach(channel2)
agency.set_news("Breaking: Python 4.0 released!")
# CNN received news: Breaking: Python 4.0 released!
# BBC received news: Breaking: Python 4.0 released!
Observer with Event Data
class Event:
"""Event data class."""
def __init__(self, event_type, data):
self.event_type = event_type
self.data = data
self.timestamp = None
import datetime
self.timestamp = datetime.datetime.now()
def __str__(self):
return f"{self.event_type}: {self.data} at {self.timestamp}"
class StockMarket(Subject):
def __init__(self):
super().__init__()
self._price = 100.0
def set_price(self, price):
old_price = self._price
self._price = price
event = Event("price_change", {
'old_price': old_price,
'new_price': price,
'change': price - old_price
})
self.notify(event)
def get_price(self):
return self._price
class StockDisplay(Observer):
def __init__(self, name):
self.name = name
def update(self, event):
if event.event_type == "price_change":
data = event.data
print(f"{self.name}: Stock price changed from "
f"${data['old_price']:.2f} to ${data['new_price']:.2f} "
f"(change: ${data['change']:.2f})")
class StockAlert(Observer):
def __init__(self, threshold=5.0):
self.threshold = threshold
def update(self, event):
if event.event_type == "price_change":
change = abs(event.data['change'])
if change >= self.threshold:
print(f"ALERT: Significant price change of ${change:.2f}!")
# Usage
market = StockMarket()
display = StockDisplay("Ticker")
alert = StockAlert(threshold=5.0)
market.attach(display)
market.attach(alert)
market.set_price(105.0) # Small change
market.set_price(112.0) # Large change - triggers alert
Practical Example: Weather Station
class WeatherData(Subject):
def __init__(self):
super().__init__()
self._temperature = None
self._humidity = None
self._pressure = None
def set_measurements(self, temperature, humidity, pressure):
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.notify()
def get_temperature(self):
return self._temperature
def get_humidity(self):
return self._humidity
def get_pressure(self):
return self._pressure
class CurrentConditionsDisplay(Observer):
def __init__(self, weather_data):
self.weather_data = weather_data
self.weather_data.attach(self)
self.temperature = None
self.humidity = None
def update(self, event=None):
self.temperature = self.weather_data.get_temperature()
self.humidity = self.weather_data.get_humidity()
self.display()
def display(self):
print(f"Current conditions: {self.temperature}°F "
f"and {self.humidity}% humidity")
class StatisticsDisplay(Observer):
def __init__(self, weather_data):
self.weather_data = weather_data
self.weather_data.attach(self)
self.temps = []
def update(self, event=None):
temp = self.weather_data.get_temperature()
self.temps.append(temp)
self.display()
def display(self):
if self.temps:
avg = sum(self.temps) / len(self.temps)
max_temp = max(self.temps)
min_temp = min(self.temps)
print(f"Avg/Max/Min temperature: "
f"{avg:.1f}/{max_temp:.1f}/{min_temp:.1f}°F")
class ForecastDisplay(Observer):
def __init__(self, weather_data):
self.weather_data = weather_data
self.weather_data.attach(self)
self.current_pressure = None
self.last_pressure = None
def update(self, event=None):
self.last_pressure = self.current_pressure
self.current_pressure = self.weather_data.get_pressure()
self.display()
def display(self):
if self.last_pressure is None:
print("Forecast: More of the same")
elif self.current_pressure > self.last_pressure:
print("Forecast: Improving weather on the way!")
elif self.current_pressure < self.last_pressure:
print("Forecast: Watch out for cooler, rainy weather")
else:
print("Forecast: More of the same")
# Usage
weather_data = WeatherData()
current_display = CurrentConditionsDisplay(weather_data)
statistics_display = StatisticsDisplay(weather_data)
forecast_display = ForecastDisplay(weather_data)
weather_data.set_measurements(80, 65, 30.4)
weather_data.set_measurements(82, 70, 29.2)
weather_data.set_measurements(78, 90, 29.2)
Strategy Pattern
Concept
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Use Cases:
- Multiple ways to perform a task
- Algorithm selection at runtime
- Replacing conditionals with polymorphism
- Payment processing
- Sorting algorithms
- Compression algorithms
Basic Strategy
from abc import ABC, abstractmethod
# Strategy interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass
# Concrete strategies
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount):
return f"Paid ${amount:.2f} 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:.2f} using PayPal account {self.email}"
class BankTransferPayment(PaymentStrategy):
def __init__(self, account_number):
self.account_number = account_number
def pay(self, amount):
return f"Paid ${amount:.2f} via Bank Transfer to account {self.account_number}"
# Context
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def set_strategy(self, strategy: PaymentStrategy):
"""Change strategy at runtime."""
self.strategy = strategy
def process_payment(self, amount):
return self.strategy.pay(amount)
# Usage
processor = PaymentProcessor(CreditCardPayment("1234567890123456", "123"))
print(processor.process_payment(100.00))
processor.set_strategy(PayPalPayment("user@example.com"))
print(processor.process_payment(50.00))
Strategy with Factory
class PaymentStrategyFactory:
"""Factory for creating payment strategies."""
@staticmethod
def create_strategy(payment_type, **kwargs):
strategies = {
'credit_card': CreditCardPayment,
'paypal': PayPalPayment,
'bank_transfer': BankTransferPayment
}
strategy_class = strategies.get(payment_type.lower())
if strategy_class:
return strategy_class(**kwargs)
else:
raise ValueError(f"Unknown payment type: {payment_type}")
# Usage
strategy = PaymentStrategyFactory.create_strategy(
'credit_card',
card_number='1234567890123456',
cvv='123'
)
processor = PaymentProcessor(strategy)
print(processor.process_payment(100.00))
Practical Example: Sorting Strategies
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
class BubbleSortStrategy(SortStrategy):
def sort(self, data):
"""Bubble sort algorithm."""
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 QuickSortStrategy(SortStrategy):
def sort(self, data):
"""Quick sort algorithm."""
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 MergeSortStrategy(SortStrategy):
def sort(self, data):
"""Merge sort algorithm."""
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, right):
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
class Sorter:
"""Context that uses sorting strategies."""
def __init__(self, strategy: SortStrategy = None):
self.strategy = strategy or QuickSortStrategy()
def set_strategy(self, strategy: SortStrategy):
"""Change sorting strategy."""
self.strategy = strategy
def sort(self, data):
"""Sort data using current strategy."""
return self.strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
print("Bubble Sort:", sorter.sort(data))
sorter.set_strategy(QuickSortStrategy())
print("Quick Sort:", sorter.sort(data))
sorter.set_strategy(MergeSortStrategy())
print("Merge Sort:", sorter.sort(data))
Command Pattern
Concept
The Command pattern encapsulates a request as an object, thereby allowing you to parameterize clients with different requests, queue requests, and support undo operations.
Use Cases:
- Undo/redo functionality
- Macro recording
- Queue operations
- Logging requests
- Transaction systems
- Remote procedure calls
Basic Command
from abc import ABC, abstractmethod
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Receiver
class Light:
def __init__(self, location):
self.location = location
self.is_on = False
def turn_on(self):
self.is_on = True
print(f"{self.location} light is ON")
def turn_off(self):
self.is_on = False
print(f"{self.location} light is OFF")
# Concrete commands
class LightOnCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_on()
def undo(self):
self.light.turn_off()
class LightOffCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_off()
def undo(self):
self.light.turn_on()
# Invoker
class RemoteControl:
def __init__(self):
self.command = None
self.history = []
def set_command(self, command: Command):
self.command = command
def press_button(self):
if self.command:
self.command.execute()
self.history.append(self.command)
def press_undo(self):
if self.history:
command = self.history.pop()
command.undo()
# Usage
living_room_light = Light("Living Room")
kitchen_light = Light("Kitchen")
living_room_on = LightOnCommand(living_room_light)
living_room_off = LightOffCommand(living_room_light)
remote = RemoteControl()
remote.set_command(living_room_on)
remote.press_button()
remote.set_command(living_room_off)
remote.press_button()
remote.press_undo() # Undo last command
Command with Parameters
class TextEditor:
"""Receiver for text editing commands."""
def __init__(self):
self.text = ""
self.cursor_position = 0
def insert_text(self, text, position=None):
if position is None:
position = self.cursor_position
self.text = self.text[:position] + text + self.text[position:]
self.cursor_position = position + len(text)
return position, text
def delete_text(self, start, length):
deleted = self.text[start:start + length]
self.text = self.text[:start] + self.text[start + length:]
if self.cursor_position > start:
self.cursor_position = max(start, self.cursor_position - length)
return start, deleted
def get_text(self):
return self.text
class InsertCommand(Command):
def __init__(self, editor, text, position=None):
self.editor = editor
self.text = text
self.position = position
self.executed_position = None
def execute(self):
self.executed_position, _ = self.editor.insert_text(self.text, self.position)
def undo(self):
if self.executed_position is not None:
self.editor.delete_text(self.executed_position, len(self.text))
class DeleteCommand(Command):
def __init__(self, editor, start, length):
self.editor = editor
self.start = start
self.length = length
self.deleted_text = None
def execute(self):
self.start, self.deleted_text = self.editor.delete_text(self.start, self.length)
def undo(self):
if self.deleted_text:
self.editor.insert_text(self.deleted_text, self.start)
class CommandHistory:
"""Manages command history for undo/redo."""
def __init__(self):
self.undo_stack = []
self.redo_stack = []
def execute_command(self, command: Command):
command.execute()
self.undo_stack.append(command)
self.redo_stack.clear() # Clear redo stack when new command executed
def undo(self):
if self.undo_stack:
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
def redo(self):
if self.redo_stack:
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
# Usage
editor = TextEditor()
history = CommandHistory()
history.execute_command(InsertCommand(editor, "Hello"))
history.execute_command(InsertCommand(editor, " World"))
print(editor.get_text()) # "Hello World"
history.undo()
print(editor.get_text()) # "Hello"
history.redo()
print(editor.get_text()) # "Hello World"
Practical Example: Calculator with Undo
class Calculator:
"""Receiver for calculator operations."""
def __init__(self):
self.value = 0
def add(self, value):
self.value += value
return self.value
def subtract(self, value):
self.value -= value
return self.value
def multiply(self, value):
self.value *= value
return self.value
def divide(self, value):
if value == 0:
raise ValueError("Cannot divide by zero")
self.value /= value
return self.value
def get_value(self):
return self.value
class AddCommand(Command):
def __init__(self, calculator, value):
self.calculator = calculator
self.value = value
def execute(self):
return self.calculator.add(self.value)
def undo(self):
return self.calculator.subtract(self.value)
class SubtractCommand(Command):
def __init__(self, calculator, value):
self.calculator = calculator
self.value = value
def execute(self):
return self.calculator.subtract(self.value)
def undo(self):
return self.calculator.add(self.value)
class MultiplyCommand(Command):
def __init__(self, calculator, value):
self.calculator = calculator
self.value = value
self.previous_value = None
def execute(self):
self.previous_value = self.calculator.get_value()
return self.calculator.multiply(self.value)
def undo(self):
if self.previous_value is not None:
self.calculator.value = self.previous_value
class DivideCommand(Command):
def __init__(self, calculator, value):
self.calculator = calculator
self.value = value
self.previous_value = None
def execute(self):
self.previous_value = self.calculator.get_value()
return self.calculator.divide(self.value)
def undo(self):
if self.previous_value is not None:
self.calculator.value = self.previous_value
# Usage
calc = Calculator()
history = CommandHistory()
history.execute_command(AddCommand(calc, 10))
print(f"Value: {calc.get_value()}") # 10
history.execute_command(MultiplyCommand(calc, 3))
print(f"Value: {calc.get_value()}") # 30
history.execute_command(SubtractCommand(calc, 5))
print(f"Value: {calc.get_value()}") # 25
history.undo()
print(f"Value: {calc.get_value()}") # 30
history.undo()
print(f"Value: {calc.get_value()}") # 10
history.redo()
print(f"Value: {calc.get_value()}") # 30
Pattern Comparison
When to Use Observer
# Use Observer when:
# - Changes to one object require changing others
# - You don't know how many objects need to be notified
# - You want to decouple objects
class EventSystem(Subject):
# Many observers can subscribe
# Changes automatically notify all
pass
When to Use Strategy
# Use Strategy when:
# - You have multiple ways to do something
# - You want to switch algorithms at runtime
# - You want to avoid conditional statements
class PaymentProcessor:
# Can switch between payment methods
# Each method is a strategy
pass
When to Use Command
# Use Command when:
# - You need undo/redo functionality
# - You want to queue operations
# - You want to log operations
class TextEditor:
# Commands can be undone
# Commands can be queued
# Commands can be logged
pass
Practical Examples
Example 1: Complete Observer Implementation
class EventBus(Subject):
"""Event bus for publish-subscribe pattern."""
def __init__(self):
super().__init__()
self._events = {}
def publish(self, event_type, data):
"""Publish an event."""
event = {
'type': event_type,
'data': data,
'timestamp': None
}
import datetime
event['timestamp'] = datetime.datetime.now()
self.notify(event)
def subscribe(self, event_type, observer):
"""Subscribe to specific event type."""
if event_type not in self._events:
self._events[event_type] = []
self._events[event_type].append(observer)
self.attach(observer)
def unsubscribe(self, event_type, observer):
"""Unsubscribe from event type."""
if event_type in self._events:
self._events[event_type].remove(observer)
self.detach(observer)
class EventHandler(Observer):
def __init__(self, name, event_types=None):
self.name = name
self.event_types = event_types or []
self.handled_events = []
def update(self, event):
if not self.event_types or event['type'] in self.event_types:
self.handle(event)
def handle(self, event):
self.handled_events.append(event)
print(f"{self.name} handled {event['type']}: {event['data']}")
# Usage
bus = EventBus()
handler1 = EventHandler("Handler1", ["user_created", "user_updated"])
handler2 = EventHandler("Handler2", ["order_placed"])
bus.subscribe("user_created", handler1)
bus.subscribe("user_updated", handler1)
bus.subscribe("order_placed", handler2)
bus.publish("user_created", {"user_id": 1, "name": "Alice"})
bus.publish("order_placed", {"order_id": 1, "amount": 100})
Example 2: Complete Strategy Implementation
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data):
pass
@abstractmethod
def decompress(self, data):
pass
class ZipCompression(CompressionStrategy):
def compress(self, data):
import zipfile
import io
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w') as zip_file:
zip_file.writestr('data', data)
return buffer.getvalue()
def decompress(self, data):
import zipfile
import io
with zipfile.ZipFile(io.BytesIO(data), 'r') as zip_file:
return zip_file.read('data').decode()
class GzipCompression(CompressionStrategy):
def compress(self, data):
import gzip
return gzip.compress(data.encode())
def decompress(self, data):
import gzip
return gzip.decompress(data).decode()
class FileCompressor:
def __init__(self, strategy: CompressionStrategy):
self.strategy = strategy
def set_strategy(self, strategy: CompressionStrategy):
self.strategy = strategy
def compress_file(self, filename, output_filename):
with open(filename, 'r') as f:
data = f.read()
compressed = self.strategy.compress(data)
with open(output_filename, 'wb') as f:
f.write(compressed)
def decompress_file(self, filename, output_filename):
with open(filename, 'rb') as f:
data = f.read()
decompressed = self.strategy.decompress(data)
with open(output_filename, 'w') as f:
f.write(decompressed)
# Usage
compressor = FileCompressor(ZipCompression())
# compressor.compress_file("data.txt", "data.zip")
compressor.set_strategy(GzipCompression())
# compressor.compress_file("data.txt", "data.gz")
Example 3: Complete Command Implementation
class FileOperation(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class CreateFileCommand(FileOperation):
def __init__(self, filename, content=""):
self.filename = filename
self.content = content
self.created = False
def execute(self):
import os
if not os.path.exists(self.filename):
with open(self.filename, 'w') as f:
f.write(self.content)
self.created = True
print(f"Created file: {self.filename}")
else:
print(f"File already exists: {self.filename}")
def undo(self):
import os
if self.created and os.path.exists(self.filename):
os.remove(self.filename)
print(f"Deleted file: {self.filename}")
self.created = False
class WriteFileCommand(FileOperation):
def __init__(self, filename, content):
self.filename = filename
self.content = content
self.old_content = None
def execute(self):
import os
if os.path.exists(self.filename):
with open(self.filename, 'r') as f:
self.old_content = f.read()
with open(self.filename, 'w') as f:
f.write(self.content)
print(f"Wrote to file: {self.filename}")
def undo(self):
if self.old_content is not None:
with open(self.filename, 'w') as f:
f.write(self.old_content)
print(f"Restored file: {self.filename}")
class FileManager:
"""Invoker for file operations."""
def __init__(self):
self.history = CommandHistory()
def execute(self, command: FileOperation):
self.history.execute_command(command)
def undo(self):
self.history.undo()
def redo(self):
self.history.redo()
# Usage
manager = FileManager()
manager.execute(CreateFileCommand("test.txt", "Hello"))
manager.execute(WriteFileCommand("test.txt", "Hello World"))
manager.undo() # Restore "Hello"
manager.undo() # Delete file
manager.redo() # Create file again
Common Mistakes and Pitfalls
Observer Issues
# WRONG: Observer holds reference to subject
class Observer:
def __init__(self, subject):
self.subject = subject # Strong reference!
# Subject can't be garbage collected
# CORRECT: Use weak references or detach properly
import weakref
class Observer:
def __init__(self, subject):
self.subject_ref = weakref.ref(subject)
Strategy Issues
# WRONG: Strategy with state
class Strategy:
def __init__(self):
self.state = {} # State in strategy!
def execute(self):
# Uses self.state - not thread-safe!
# CORRECT: Stateless strategies or pass state
class Strategy:
def execute(self, state):
# Use passed state
pass
Best Practices
1. Use Weak References for Observers
import weakref
class Subject:
def __init__(self):
self._observers = weakref.WeakSet()
2. Keep Strategies Stateless
# Strategies should be stateless
# Pass any needed data as parameters
3. Implement Undo Carefully
# Store enough state to undo
# Consider memory usage
# Handle edge cases
Practice Exercise
Exercise: Behavioral Patterns
Objective: Implement behavioral patterns in a practical scenario.
Requirements:
-
Create a system using behavioral patterns:
- Observer for event notifications
- Strategy for different algorithms
- Command for operations with undo
-
Scenario: Task management system
- Observer for task status changes
- Strategy for different task prioritization algorithms
- Command for task operations (add, update, delete) with undo
Example Solution:
"""
Task Management System with Behavioral Patterns
"""
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
# Observer Pattern: Task Status Notifications
class TaskStatus(Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class Task(Subject):
"""Task subject that notifies observers of status changes."""
def __init__(self, id, title, description=""):
super().__init__()
self.id = id
self.title = title
self.description = description
self.status = TaskStatus.PENDING
self.created_at = datetime.now()
self.updated_at = datetime.now()
def set_status(self, status):
old_status = self.status
self.status = status
self.updated_at = datetime.now()
self.notify({
'task_id': self.id,
'old_status': old_status,
'new_status': status,
'timestamp': self.updated_at
})
def __str__(self):
return f"Task {self.id}: {self.title} [{self.status.value}]"
class TaskObserver(Observer):
"""Observer for task status changes."""
def __init__(self, name):
self.name = name
self.notifications = []
def update(self, event):
self.notifications.append(event)
print(f"{self.name} notified: Task {event['task_id']} changed from "
f"{event['old_status'].value} to {event['new_status'].value}")
# Strategy Pattern: Task Prioritization
class PrioritizationStrategy(ABC):
@abstractmethod
def prioritize(self, tasks):
"""Sort tasks by priority."""
pass
class PriorityByStatusStrategy(PrioritizationStrategy):
"""Prioritize by status (in_progress > pending > completed > cancelled)."""
def prioritize(self, tasks):
status_order = {
TaskStatus.IN_PROGRESS: 1,
TaskStatus.PENDING: 2,
TaskStatus.COMPLETED: 3,
TaskStatus.CANCELLED: 4
}
return sorted(tasks, key=lambda t: status_order.get(t.status, 5))
class PriorityByDateStrategy(PrioritizationStrategy):
"""Prioritize by creation date (oldest first)."""
def prioritize(self, tasks):
return sorted(tasks, key=lambda t: t.created_at)
class PriorityByUpdateStrategy(PrioritizationStrategy):
"""Prioritize by last update (most recently updated first)."""
def prioritize(self, tasks):
return sorted(tasks, key=lambda t: t.updated_at, reverse=True)
class TaskManager:
"""Context that uses prioritization strategies."""
def __init__(self, strategy: PrioritizationStrategy = None):
self.tasks = []
self.strategy = strategy or PriorityByStatusStrategy()
def set_strategy(self, strategy: PrioritizationStrategy):
"""Change prioritization strategy."""
self.strategy = strategy
def add_task(self, task: Task):
"""Add a task."""
self.tasks.append(task)
def get_prioritized_tasks(self):
"""Get tasks sorted by current strategy."""
return self.strategy.prioritize(self.tasks)
# Command Pattern: Task Operations
class TaskCommand(Command):
"""Base command for task operations."""
def __init__(self, task_manager: TaskManager):
self.task_manager = task_manager
class AddTaskCommand(TaskCommand):
"""Command to add a task."""
def __init__(self, task_manager, task_id, title, description=""):
super().__init__(task_manager)
self.task = Task(task_id, title, description)
self.added = False
def execute(self):
self.task_manager.add_task(self.task)
self.added = True
print(f"Added: {self.task}")
return self.task
def undo(self):
if self.added and self.task in self.task_manager.tasks:
self.task_manager.tasks.remove(self.task)
print(f"Removed: {self.task}")
class UpdateTaskStatusCommand(TaskCommand):
"""Command to update task status."""
def __init__(self, task_manager, task_id, new_status):
super().__init__(task_manager)
self.task_id = task_id
self.new_status = new_status
self.old_status = None
self.task = None
def execute(self):
self.task = next((t for t in self.task_manager.tasks if t.id == self.task_id), None)
if self.task:
self.old_status = self.task.status
self.task.set_status(self.new_status)
print(f"Updated task {self.task_id} status to {self.new_status.value}")
return self.task
def undo(self):
if self.task and self.old_status:
self.task.set_status(self.old_status)
print(f"Reverted task {self.task_id} status to {self.old_status.value}")
class DeleteTaskCommand(TaskCommand):
"""Command to delete a task."""
def __init__(self, task_manager, task_id):
super().__init__(task_manager)
self.task_id = task_id
self.task = None
self.deleted = False
def execute(self):
self.task = next((t for t in self.task_manager.tasks if t.id == self.task_id), None)
if self.task:
self.task_manager.tasks.remove(self.task)
self.deleted = True
print(f"Deleted: {self.task}")
return self.task
def undo(self):
if self.deleted and self.task:
self.task_manager.tasks.append(self.task)
print(f"Restored: {self.task}")
class CommandHistory:
"""Manages command history."""
def __init__(self):
self.undo_stack = []
self.redo_stack = []
def execute(self, command: Command):
command.execute()
self.undo_stack.append(command)
self.redo_stack.clear()
def undo(self):
if self.undo_stack:
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
def redo(self):
if self.redo_stack:
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
# Complete System
def main():
# Create task manager with strategy
task_manager = TaskManager(PriorityByStatusStrategy())
history = CommandHistory()
# Create observers
logger = TaskObserver("Logger")
notifier = TaskObserver("Notifier")
# Add tasks using commands
history.execute(AddTaskCommand(task_manager, 1, "Design database", "Design schema"))
history.execute(AddTaskCommand(task_manager, 2, "Write tests", "Unit tests"))
history.execute(AddTaskCommand(task_manager, 3, "Deploy application", "Production deploy"))
# Attach observers to tasks
for task in task_manager.tasks:
task.attach(logger)
task.attach(notifier)
# Update task status (triggers observers)
history.execute(UpdateTaskStatusCommand(task_manager, 1, TaskStatus.IN_PROGRESS))
history.execute(UpdateTaskStatusCommand(task_manager, 2, TaskStatus.COMPLETED))
# Show prioritized tasks
print("\nTasks prioritized by status:")
for task in task_manager.get_prioritized_tasks():
print(f" {task}")
# Change strategy
task_manager.set_strategy(PriorityByDateStrategy())
print("\nTasks prioritized by date:")
for task in task_manager.get_prioritized_tasks():
print(f" {task}")
# Undo operations
print("\nUndoing last operation:")
history.undo()
print("\nUndoing another operation:")
history.undo()
print("\nRedoing:")
history.redo()
if __name__ == '__main__':
main()
Expected Output: A complete task management system demonstrating all three behavioral patterns.
Challenge (Optional):
- Add more observers (email, SMS)
- Add more strategies (priority by title, by description length)
- Add more commands (update title, update description)
- Add command batching (macro commands)
- Add persistent command history
Key Takeaways
- Behavioral Patterns - Focus on communication between objects
- Observer - Notify multiple objects about state changes
- Strategy - Define interchangeable algorithms
- Command - Encapsulate requests as objects
- Observer Benefits - Decoupling, flexibility, one-to-many dependency
- Strategy Benefits - Runtime algorithm selection, avoid conditionals
- Command Benefits - Undo/redo, queuing, logging, macro recording
- When to Use - Each pattern has specific use cases
- Pattern Combinations - Patterns can work together
- Best Practices - Weak references, stateless strategies, careful undo
- Common Mistakes - Strong references, stateful strategies
- Real-World Applications - Event systems, payment processing, text editors
- Flexibility - Patterns provide flexibility and extensibility
- Maintainability - Patterns improve code maintainability
- Learning - Study existing pattern implementations
Quiz: Behavioral Patterns
Test your understanding with these questions:
-
What does Observer pattern do?
- A) Encapsulates requests
- B) Notifies objects about changes
- C) Defines algorithms
- D) Creates objects
-
What does Strategy pattern do?
- A) Notifies objects
- B) Defines interchangeable algorithms
- C) Encapsulates requests
- D) Creates objects
-
What does Command pattern do?
- A) Notifies objects
- B) Defines algorithms
- C) Encapsulates requests as objects
- D) Creates objects
-
Observer relationship is:
- A) One-to-one
- B) One-to-many
- C) Many-to-one
- D) Many-to-many
-
Strategy allows:
- A) Runtime algorithm selection
- B) Compile-time selection
- C) Static selection
- D) No selection
-
Command enables:
- A) Undo/redo
- B) Queuing
- C) Logging
- D) All of the above
-
When to use Observer?
- A) Event handling
- B) MVC architecture
- C) Publish-subscribe
- D) All of the above
-
When to use Strategy?
- A) Multiple algorithms
- B) Runtime selection
- C) Avoid conditionals
- D) All of the above
-
When to use Command?
- A) Undo/redo needed
- B) Queue operations
- C) Log operations
- D) All of the above
-
Patterns can be:
- A) Combined
- B) Used separately
- C) Nested
- D) All of the above
Answers:
- B) Notifies objects about changes (Observer)
- B) Defines interchangeable algorithms (Strategy)
- C) Encapsulates requests as objects (Command)
- B) One-to-many (Observer relationship)
- A) Runtime algorithm selection (Strategy)
- D) All of the above (Command features)
- D) All of the above (Observer use cases)
- D) All of the above (Strategy use cases)
- D) All of the above (Command use cases)
- D) All of the above (pattern usage)
Next Steps
Excellent work! You've mastered behavioral patterns. You now understand:
- Observer pattern
- Strategy pattern
- Command pattern
- How to apply patterns in real scenarios
What's Next?
- Module 26: Software Architecture
- Learn project structure and organization
- Understand software architecture principles
- Apply architecture patterns
Additional Resources
- Design Patterns: refactoring.guru/design-patterns
- Python Design Patterns: python-patterns.guide/
- Observer Pattern: Event-driven programming
- Strategy Pattern: Algorithm selection
- Command Pattern: Undo/redo systems
Lesson completed! You're ready to move on to the next module.
Course Navigation
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
- Creational Patterns
- Structural Patterns
- Behavioral Patterns