Back to Curriculum

Object-Oriented Programming and Design Patterns

📚 Lesson 5 of 20 ⏱️ 70 min

Object-Oriented Programming and Design Patterns

70 min

Python's OOP implementation is based on the principle that everything is an object. Classes are blueprints for creating objects, and objects are instances of classes with their own state and behavior.

Python supports multiple inheritance, method resolution order (MRO), and special methods (dunder methods) that enable powerful object-oriented patterns. Understanding these concepts is crucial for advanced Python development.

Design patterns provide proven solutions to common programming problems. Python's dynamic nature makes it particularly well-suited for implementing patterns like Singleton, Factory, Observer, and Strategy.

Modern Python development emphasizes composition over inheritance, and the use of abstract base classes and protocols for better code design and maintainability.

Python's special methods (__init__, __str__, __repr__, __eq__, etc.) allow you to define how objects behave with built-in operations, making your classes feel native to Python.

The Method Resolution Order (MRO) determines how Python searches for methods in inheritance hierarchies, especially important when dealing with multiple inheritance scenarios.

Key Concepts

  • Classes are blueprints for creating objects with state and behavior.
  • Inheritance allows classes to inherit attributes and methods from parent classes.
  • Encapsulation hides internal implementation details from external code.
  • Polymorphism allows different classes to be used interchangeably.
  • Special methods (dunder methods) customize object behavior.

Learning Objectives

Master

  • Creating classes and instantiating objects
  • Understanding inheritance and method resolution order
  • Implementing special methods for custom behavior
  • Applying design patterns in Python

Develop

  • Object-oriented design thinking
  • Understanding when to use composition vs inheritance
  • Recognizing and applying design patterns

Tips

  • Prefer composition over inheritance for more flexible designs.
  • Use dataclasses for simple data containers (Python 3.7+).
  • Implement __repr__ for better debugging and __str__ for user-friendly output.
  • Use abstract base classes (ABC) to define interfaces.

Common Pitfalls

  • Overusing inheritance when composition would be better.
  • Not understanding MRO, causing unexpected method resolution.
  • Forgetting to call super() in child class constructors.
  • Creating overly complex inheritance hierarchies.

Summary

  • OOP in Python enables code organization and reusability.
  • Inheritance allows code reuse but composition is often preferred.
  • Special methods customize how objects interact with Python operations.
  • Design patterns provide proven solutions to common problems.

Exercise

Implement a comprehensive OOP demonstration that showcases inheritance, polymorphism, design patterns, and best practices for object-oriented design.

from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
import json

class PaymentMethod(Enum):
    """Enumeration for different payment methods."""
    CREDIT_CARD = "credit_card"
    DEBIT_CARD = "debit_card"
    BANK_TRANSFER = "bank_transfer"
    CASH = "cash"

class PaymentProcessor(ABC):
    """Abstract base class for payment processors."""
    
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        """Process a payment and return success status."""
        pass
    
    @abstractmethod
    def get_processing_fee(self) -> float:
        """Get the processing fee for this payment method."""
        pass

class CreditCardProcessor(PaymentProcessor):
    """Concrete implementation for credit card payments."""
    
    def __init__(self, card_number: str, expiry_date: str):
        self.card_number = card_number
        self.expiry_date = expiry_date
        self._processing_fee = 0.025  # 2.5%
    
    def process_payment(self, amount: float) -> bool:
        """Process credit card payment with validation."""
        if amount <= 0:
            return False
        
        if not self._validate_card():
            return False
        
        # Simulate payment processing
        total_amount = amount + (amount * self._processing_fee)
        print(f"Processing credit card payment: ${total_amount:.2f}")
        return True
    
    def get_processing_fee(self) -> float:
        return self._processing_fee
    
    def _validate_card(self) -> bool:
        """Validate credit card details."""
        # Check card number length and that it contains only digits
        if len(self.card_number) != 16 or not self.card_number.isdigit():
            return False
        
        # Check expiry date format (MM/YY or MM/YYYY)
        if not self.expiry_date or '/' not in self.expiry_date:
            return False
        
        try:
            month, year = self.expiry_date.split('/')
            month = int(month)
            year = int(year)
            
            # Validate month (1-12)
            if month < 1 or month > 12:
                return False
                
            # Validate year (assuming 2-digit years are 20xx)
            if len(str(year)) == 2:
                year = 2000 + year
                
            return True
        except (ValueError, TypeError):
            return False

class BankTransferProcessor(PaymentProcessor):
    """Concrete implementation for bank transfer payments."""
    
    def __init__(self, account_number: str, routing_number: str):
        if not account_number or not routing_number:
            raise ValueError("Account number and routing number cannot be empty")
        if not account_number.isdigit() or not routing_number.isdigit():
            raise ValueError("Account number and routing number must contain only digits")
        self.account_number = account_number
        self.routing_number = routing_number
        self._processing_fee = 0.01  # 1%
    
    def process_payment(self, amount: float) -> bool:
        """Process bank transfer payment."""
        if amount <= 0:
            return False
        
        total_amount = amount + (amount * self._processing_fee)
        print(f"Processing bank transfer: ${total_amount:.2f}")
        return True
    
    def get_processing_fee(self) -> float:
        return self._processing_fee

@dataclass
class OrderItem:
    """Data class for order items."""
    product_id: str
    name: str
    price: float
    quantity: int
    
    def __post_init__(self):
        """Validate order item data after initialization."""
        if self.price < 0:
            raise ValueError("Price cannot be negative")
        if self.quantity < 1:
            raise ValueError("Quantity must be at least 1")
        if not self.product_id or not self.name:
            raise ValueError("Product ID and name cannot be empty")
    
    @property
    def total_price(self) -> float:
        """Calculate total price for this item."""
        return self.price * self.quantity

class Order:
    """Represents a customer order with multiple items."""
    
    def __init__(self, order_id: str, customer_name: str):
        if not order_id or not customer_name:
            raise ValueError("Order ID and customer name cannot be empty")
        self.order_id = order_id
        self.customer_name = customer_name
        self.items: List[OrderItem] = []
        self._status = "pending"
    
    def add_item(self, item: OrderItem) -> None:
        """Add an item to the order."""
        self.items.append(item)
    
    def remove_item(self, product_id: str) -> bool:
        """Remove an item from the order."""
        for i, item in enumerate(self.items):
            if item.product_id == product_id:
                del self.items[i]
                return True
        return False
    
    @property
    def total_amount(self) -> float:
        """Calculate total order amount."""
        return sum(item.total_price for item in self.items)
    
    @property
    def status(self) -> str:
        return self._status
    
    def set_status(self, status: str) -> None:
        """Set order status with validation."""
        valid_statuses = ["pending", "processing", "shipped", "delivered", "cancelled"]
        if status in valid_statuses:
            self._status = status
        else:
            raise ValueError(f"Invalid status: {status}")

class OrderProcessor:
    """Facade pattern implementation for order processing."""
    
    def __init__(self):
        self.payment_processors: Dict[PaymentMethod, PaymentProcessor] = {}
    
    def register_payment_processor(self, method: PaymentMethod, processor: PaymentProcessor) -> None:
        """Register a payment processor for a specific method."""
        self.payment_processors[method] = processor
    
    def process_order(self, order: Order, payment_method: PaymentMethod) -> bool:
        """Process an order with the specified payment method."""
        if not order.items:
            raise ValueError("Cannot process order with no items")
        
        if payment_method not in self.payment_processors:
            raise ValueError(f"No processor registered for {payment_method.value}")
        
        processor = self.payment_processors[payment_method]
        
        # Process payment
        if processor.process_payment(order.total_amount):
            order.set_status("processing")
            return True
        else:
            order.set_status("cancelled")
            return False
    
    def get_order_summary(self, order: Order) -> Dict[str, Any]:
        """Generate order summary."""
        return {
            "order_id": order.order_id,
            "customer_name": order.customer_name,
            "total_amount": order.total_amount,
            "item_count": len(order.items),
            "status": order.status,
            "items": [
                {
                    "product_id": item.product_id,
                    "name": item.name,
                    "quantity": item.quantity,
                    "total_price": item.total_price
                }
                for item in order.items
            ]
        }

# Singleton pattern implementation
class Logger:
    """Singleton logger for order processing."""
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.logs = []
        return cls._instance
    
    def log(self, message: str) -> None:
        """Add a log message."""
        if not message:
            raise ValueError("Log message cannot be empty")
        self.logs.append(message)
        print(f"[LOG] {message}")
    
    def get_logs(self) -> List[str]:
        """Get all logs."""
        return self.logs.copy()

def main():
    """Demonstrate OOP concepts and design patterns."""
    try:
        logger = Logger()
        
        # Create order
        order = Order("ORD-001", "John Doe")
        
        # Add items to order
        items = [
            OrderItem("PROD-001", "Laptop", 999.99, 1),
            OrderItem("PROD-002", "Mouse", 29.99, 2),
            OrderItem("PROD-003", "Keyboard", 89.99, 1)
        ]
        
        for item in items:
            order.add_item(item)
        
        # Create payment processors
        credit_processor = CreditCardProcessor("1234567890123456", "12/25")
        bank_processor = BankTransferProcessor("987654321", "123456789")
        
        # Create order processor
        processor = OrderProcessor()
        processor.register_payment_processor(PaymentMethod.CREDIT_CARD, credit_processor)
        processor.register_payment_processor(PaymentMethod.BANK_TRANSFER, bank_processor)
        
        # Process order with credit card
        logger.log("Processing order with credit card...")
        success = processor.process_order(order, PaymentMethod.CREDIT_CARD)
        
        if success:
            logger.log("Order processed successfully!")
            
            # Generate and display order summary
            summary = processor.get_order_summary(order)
            print("\n=== Order Summary ===")
            print(json.dumps(summary, indent=2))
        else:
            logger.log("Order processing failed!")
        
        # Demonstrate polymorphism
        print("\n=== Payment Processor Comparison ===")
        processors = [credit_processor, bank_processor]
        for i, proc in enumerate(processors):
            fee = proc.get_processing_fee()
            print(f"Processor {i+1} fee: {fee:.1%}")
            
    except Exception as e:
        print(f"Error in main function: {e}")

if __name__ == "__main__":
    main()

Exercise Tips

  • Start with simple classes before implementing complex inheritance.
  • Use @dataclass decorator for simple data classes.
  • Implement __repr__ and __str__ methods for better object representation.
  • Practice composition by creating classes that use other classes.

Code Editor

Output