python

exercises

exercises.py🐍
"""
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"])
Exercises - Python Tutorial | DeepML