python
exercises
exercises.py🐍python
"""
Design Patterns - Exercises
Complete these exercises to practice implementing design patterns.
Run with: pytest 24_design_patterns/exercises.py -v
"""
from abc import ABC, abstractmethod
from typing import Any, Callable
import copy
# =============================================================================
# EXERCISE 1: Singleton Logger
# =============================================================================
class Logger:
"""
Implement a Singleton logger.
Requirements:
- Only one instance should ever exist
- Should have methods: debug(), info(), warning(), error()
- Should store all log messages in a list
- Should have a get_logs() method to retrieve all logs
Example:
logger1 = Logger()
logger2 = Logger()
assert logger1 is logger2
logger1.info("Hello")
logger2.warning("World")
assert len(logger1.get_logs()) == 2
"""
_instance = None
def __new__(cls):
# YOUR CODE HERE
pass
def __init__(self):
# YOUR CODE HERE - but be careful with Singleton!
pass
def debug(self, message: str):
# YOUR CODE HERE
pass
def info(self, message: str):
# YOUR CODE HERE
pass
def warning(self, message: str):
# YOUR CODE HERE
pass
def error(self, message: str):
# YOUR CODE HERE
pass
def get_logs(self) -> list[dict]:
# YOUR CODE HERE
pass
def clear(self):
# YOUR CODE HERE
pass
def test_singleton_logger():
"""Test the Singleton Logger implementation."""
# Reset singleton for testing
Logger._instance = None
logger1 = Logger()
logger2 = Logger()
# Should be same instance
assert logger1 is logger2, "Logger should be singleton"
# Clear any existing logs
logger1.clear()
# Log some messages
logger1.debug("Debug message")
logger2.info("Info message")
logger1.warning("Warning message")
logger2.error("Error message")
logs = logger1.get_logs()
assert len(logs) == 4, "Should have 4 log entries"
assert logs[0]["level"] == "DEBUG"
assert logs[1]["level"] == "INFO"
assert logs[2]["level"] == "WARNING"
assert logs[3]["level"] == "ERROR"
# =============================================================================
# EXERCISE 2: Shape Factory
# =============================================================================
class Shape(ABC):
"""Abstract base class for shapes."""
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
class Circle(Shape):
"""
Implement a Circle shape.
Formula:
- area = π * r²
- perimeter = 2 * π * r
"""
def __init__(self, radius: float):
# YOUR CODE HERE
pass
def area(self) -> float:
# YOUR CODE HERE
pass
def perimeter(self) -> float:
# YOUR CODE HERE
pass
class Rectangle(Shape):
"""
Implement a Rectangle shape.
Formula:
- area = width * height
- perimeter = 2 * (width + height)
"""
def __init__(self, width: float, height: float):
# YOUR CODE HERE
pass
def area(self) -> float:
# YOUR CODE HERE
pass
def perimeter(self) -> float:
# YOUR CODE HERE
pass
class Triangle(Shape):
"""
Implement a Triangle shape.
For simplicity, use a right triangle with base and height.
Formula:
- area = 0.5 * base * height
- perimeter = base + height + sqrt(base² + height²)
"""
def __init__(self, base: float, height: float):
# YOUR CODE HERE
pass
def area(self) -> float:
# YOUR CODE HERE
pass
def perimeter(self) -> float:
# YOUR CODE HERE
pass
class ShapeFactory:
"""
Implement a Shape Factory.
Requirements:
- create() method that takes shape type and dimensions
- Supported shapes: "circle", "rectangle", "triangle"
- Raise ValueError for unknown shapes
Example:
factory = ShapeFactory()
circle = factory.create("circle", radius=5)
rect = factory.create("rectangle", width=4, height=3)
"""
def create(self, shape_type: str, **kwargs) -> Shape:
# YOUR CODE HERE
pass
def test_shape_factory():
"""Test the Shape Factory implementation."""
import math
factory = ShapeFactory()
# Test circle
circle = factory.create("circle", radius=5)
assert isinstance(circle, Circle)
assert abs(circle.area() - 78.54) < 0.01
assert abs(circle.perimeter() - 31.42) < 0.01
# Test rectangle
rect = factory.create("rectangle", width=4, height=3)
assert isinstance(rect, Rectangle)
assert rect.area() == 12
assert rect.perimeter() == 14
# Test triangle
tri = factory.create("triangle", base=3, height=4)
assert isinstance(tri, Triangle)
assert tri.area() == 6
assert abs(tri.perimeter() - 12) < 0.01
# Test unknown shape
try:
factory.create("pentagon", side=5)
assert False, "Should raise ValueError"
except ValueError:
pass
# =============================================================================
# EXERCISE 3: Notification Builder
# =============================================================================
class Notification:
"""A notification with various properties."""
def __init__(self):
self.title: str = ""
self.message: str = ""
self.priority: str = "normal" # low, normal, high, urgent
self.recipients: list[str] = []
self.channels: list[str] = [] # email, sms, push
self.metadata: dict = {}
class NotificationBuilder:
"""
Implement a builder for notifications.
Requirements:
- Fluent interface (methods return self for chaining)
- Methods: set_title, set_message, set_priority,
add_recipient, add_channel, add_metadata
- build() returns the notification
Example:
notification = (NotificationBuilder()
.set_title("Alert")
.set_message("System is down")
.set_priority("urgent")
.add_recipient("admin@example.com")
.add_channel("email")
.add_channel("sms")
.build())
"""
def __init__(self):
# YOUR CODE HERE
pass
def set_title(self, title: str) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def set_message(self, message: str) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def set_priority(self, priority: str) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def add_recipient(self, recipient: str) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def add_channel(self, channel: str) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def add_metadata(self, key: str, value: Any) -> 'NotificationBuilder':
# YOUR CODE HERE
pass
def build(self) -> Notification:
# YOUR CODE HERE
pass
def test_notification_builder():
"""Test the Notification Builder implementation."""
notification = (NotificationBuilder()
.set_title("Server Alert")
.set_message("CPU usage is at 95%")
.set_priority("high")
.add_recipient("admin@example.com")
.add_recipient("ops@example.com")
.add_channel("email")
.add_channel("sms")
.add_metadata("server_id", "srv-001")
.add_metadata("timestamp", 1234567890)
.build())
assert notification.title == "Server Alert"
assert notification.message == "CPU usage is at 95%"
assert notification.priority == "high"
assert len(notification.recipients) == 2
assert "admin@example.com" in notification.recipients
assert len(notification.channels) == 2
assert notification.metadata["server_id"] == "srv-001"
# =============================================================================
# EXERCISE 4: Observer Pattern - Stock Price Alerts
# =============================================================================
class StockTicker:
"""
Implement a stock ticker with observer pattern.
Requirements:
- subscribe(callback) - add an observer
- unsubscribe(callback) - remove an observer
- update_price(symbol, price) - update stock price and notify observers
- Observers receive (symbol, old_price, new_price) when price changes
Example:
ticker = StockTicker()
def on_price_change(symbol, old, new):
print(f"{symbol}: ${old} -> ${new}")
ticker.subscribe(on_price_change)
ticker.update_price("AAPL", 150.00)
ticker.update_price("AAPL", 152.50) # Notifies observer
"""
def __init__(self):
# YOUR CODE HERE
pass
def subscribe(self, callback: Callable[[str, float, float], None]):
# YOUR CODE HERE
pass
def unsubscribe(self, callback: Callable[[str, float, float], None]):
# YOUR CODE HERE
pass
def update_price(self, symbol: str, price: float):
# YOUR CODE HERE
pass
def get_price(self, symbol: str) -> float | None:
# YOUR CODE HERE
pass
def test_stock_ticker():
"""Test the Stock Ticker implementation."""
ticker = StockTicker()
changes = []
def track_changes(symbol, old_price, new_price):
changes.append((symbol, old_price, new_price))
ticker.subscribe(track_changes)
# First update - no old price
ticker.update_price("AAPL", 150.00)
assert len(changes) == 1
assert changes[0][0] == "AAPL"
assert changes[0][2] == 150.00
# Second update - price changed
ticker.update_price("AAPL", 155.00)
assert len(changes) == 2
assert changes[1][1] == 150.00 # old price
assert changes[1][2] == 155.00 # new price
# Check current price
assert ticker.get_price("AAPL") == 155.00
# Unsubscribe and update
ticker.unsubscribe(track_changes)
ticker.update_price("AAPL", 160.00)
assert len(changes) == 2 # No new notifications
# =============================================================================
# EXERCISE 5: Strategy Pattern - Payment Processing
# =============================================================================
class PaymentResult:
"""Result of a payment attempt."""
def __init__(self, success: bool, transaction_id: str, message: str):
self.success = success
self.transaction_id = transaction_id
self.message = message
def process_credit_card(amount: float, details: dict) -> PaymentResult:
"""
Implement credit card payment strategy.
Requirements:
- Check that details contains 'card_number' and 'cvv'
- Return success if card_number starts with '4' (Visa simulation)
- Generate a transaction ID like "CC-{amount}-{last4digits}"
"""
# YOUR CODE HERE
pass
def process_paypal(amount: float, details: dict) -> PaymentResult:
"""
Implement PayPal payment strategy.
Requirements:
- Check that details contains 'email'
- Return success if email contains '@'
- Generate a transaction ID like "PP-{amount}-{email_prefix}"
"""
# YOUR CODE HERE
pass
def process_crypto(amount: float, details: dict) -> PaymentResult:
"""
Implement cryptocurrency payment strategy.
Requirements:
- Check that details contains 'wallet_address'
- Return success if wallet_address is at least 20 characters
- Generate a transaction ID like "CRYPTO-{amount}-{first8chars}"
"""
# YOUR CODE HERE
pass
class PaymentProcessor:
"""
Implement a payment processor using strategy pattern.
Requirements:
- Constructor takes a strategy function
- set_strategy() to change the strategy
- process(amount, details) to execute payment
Example:
processor = PaymentProcessor(process_credit_card)
result = processor.process(100.00, {"card_number": "4111...", "cvv": "123"})
"""
def __init__(self, strategy: Callable[[float, dict], PaymentResult]):
# YOUR CODE HERE
pass
def set_strategy(self, strategy: Callable[[float, dict], PaymentResult]):
# YOUR CODE HERE
pass
def process(self, amount: float, details: dict) -> PaymentResult:
# YOUR CODE HERE
pass
def test_payment_strategies():
"""Test the Payment Processing implementation."""
# Test credit card
processor = PaymentProcessor(process_credit_card)
result = processor.process(100.00, {
"card_number": "4111111111111111",
"cvv": "123"
})
assert result.success == True
assert "CC-" in result.transaction_id
# Invalid card (doesn't start with 4)
result = processor.process(100.00, {
"card_number": "5111111111111111",
"cvv": "123"
})
assert result.success == False
# Test PayPal
processor.set_strategy(process_paypal)
result = processor.process(50.00, {"email": "user@example.com"})
assert result.success == True
assert "PP-" in result.transaction_id
# Test Crypto
processor.set_strategy(process_crypto)
result = processor.process(200.00, {
"wallet_address": "0x1234567890abcdef1234567890abcdef12345678"
})
assert result.success == True
assert "CRYPTO-" in result.transaction_id
# =============================================================================
# EXERCISE 6: Decorator Pattern - Message Formatting
# =============================================================================
class Message(ABC):
"""Abstract message interface."""
@abstractmethod
def get_content(self) -> str:
pass
class PlainMessage(Message):
"""Basic plain text message."""
def __init__(self, text: str):
self.text = text
def get_content(self) -> str:
return self.text
class MessageDecorator(Message):
"""Base decorator for messages."""
def __init__(self, message: Message):
self._message = message
def get_content(self) -> str:
return self._message.get_content()
class UppercaseDecorator(MessageDecorator):
"""
Decorator that converts message to uppercase.
Example:
msg = PlainMessage("hello")
msg = UppercaseDecorator(msg)
assert msg.get_content() == "HELLO"
"""
def get_content(self) -> str:
# YOUR CODE HERE
pass
class TimestampDecorator(MessageDecorator):
"""
Decorator that prepends a timestamp.
Format: "[YYYY-MM-DD HH:MM:SS] message"
Example:
msg = PlainMessage("hello")
msg = TimestampDecorator(msg)
# Result: "[2024-01-15 10:30:00] hello"
"""
def get_content(self) -> str:
# YOUR CODE HERE
pass
class EncryptDecorator(MessageDecorator):
"""
Decorator that "encrypts" the message using simple ROT13.
ROT13 replaces each letter with the letter 13 positions after it.
A->N, B->O, ..., M->Z, N->A, ...
Example:
msg = PlainMessage("hello")
msg = EncryptDecorator(msg)
assert msg.get_content() == "uryyb"
"""
def get_content(self) -> str:
# YOUR CODE HERE
pass
def test_message_decorators():
"""Test the Message Decorator implementation."""
# Test uppercase
msg = PlainMessage("hello world")
msg = UppercaseDecorator(msg)
assert msg.get_content() == "HELLO WORLD"
# Test timestamp (just check format)
msg = PlainMessage("test")
msg = TimestampDecorator(msg)
content = msg.get_content()
assert content.startswith("[")
assert "]" in content
assert "test" in content
# Test encryption
msg = PlainMessage("hello")
msg = EncryptDecorator(msg)
assert msg.get_content() == "uryyb"
# Double encryption should return original
msg = EncryptDecorator(msg)
assert msg.get_content() == "hello"
# Chained decorators
msg = PlainMessage("secret")
msg = EncryptDecorator(msg)
msg = UppercaseDecorator(msg)
assert msg.get_content() == "FRPERG"
# =============================================================================
# EXERCISE 7: Command Pattern - Calculator with Undo
# =============================================================================
class Calculator:
"""Simple calculator that stores a value."""
def __init__(self, initial_value: float = 0):
self.value = initial_value
class CalculatorCommand(ABC):
"""Abstract command for calculator operations."""
@abstractmethod
def execute(self) -> float:
pass
@abstractmethod
def undo(self) -> float:
pass
class AddCommand(CalculatorCommand):
"""
Command to add a value.
Example:
calc = Calculator(10)
cmd = AddCommand(calc, 5)
cmd.execute() # calc.value = 15
cmd.undo() # calc.value = 10
"""
def __init__(self, calculator: Calculator, value: float):
# YOUR CODE HERE
pass
def execute(self) -> float:
# YOUR CODE HERE
pass
def undo(self) -> float:
# YOUR CODE HERE
pass
class MultiplyCommand(CalculatorCommand):
"""
Command to multiply by a value.
Example:
calc = Calculator(10)
cmd = MultiplyCommand(calc, 3)
cmd.execute() # calc.value = 30
cmd.undo() # calc.value = 10
"""
def __init__(self, calculator: Calculator, value: float):
# YOUR CODE HERE
pass
def execute(self) -> float:
# YOUR CODE HERE
pass
def undo(self) -> float:
# YOUR CODE HERE
pass
class CalculatorInvoker:
"""
Invoker that manages calculator commands.
Requirements:
- execute(command) - execute and store for undo
- undo() - undo the last command
- redo() - redo the last undone command
Example:
calc = Calculator(10)
invoker = CalculatorInvoker()
invoker.execute(AddCommand(calc, 5)) # 15
invoker.execute(MultiplyCommand(calc, 2)) # 30
invoker.undo() # 15
invoker.undo() # 10
invoker.redo() # 15
"""
def __init__(self):
# YOUR CODE HERE
pass
def execute(self, command: CalculatorCommand) -> float:
# YOUR CODE HERE
pass
def undo(self) -> float | None:
# YOUR CODE HERE
pass
def redo(self) -> float | None:
# YOUR CODE HERE
pass
def test_calculator_commands():
"""Test the Calculator Command implementation."""
calc = Calculator(10)
invoker = CalculatorInvoker()
# Add 5: 10 + 5 = 15
result = invoker.execute(AddCommand(calc, 5))
assert result == 15
assert calc.value == 15
# Multiply by 2: 15 * 2 = 30
result = invoker.execute(MultiplyCommand(calc, 2))
assert result == 30
assert calc.value == 30
# Add 10: 30 + 10 = 40
result = invoker.execute(AddCommand(calc, 10))
assert result == 40
# Undo: 40 - 10 = 30
result = invoker.undo()
assert result == 30
assert calc.value == 30
# Undo: 30 / 2 = 15
result = invoker.undo()
assert result == 15
# Redo: 15 * 2 = 30
result = invoker.redo()
assert result == 30
# Redo: 30 + 10 = 40
result = invoker.redo()
assert result == 40
# =============================================================================
# RUN TESTS
# =============================================================================
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])