DEV Community

Tiru
Tiru

Posted on

#07 - Writing Elegant Code: OOP Best Practices in Python

Welcome to Day 7—the culmination of our Object-Oriented Programming masterclass! Up to this point, you've learned the mechanics: classes, inheritance, magic methods, decorators, and object relationships.

Today is about software architecture: how to combine these tools into modular, testable, production-ready systems that remain easy to maintain as requirements evolve.


1. Core Architectural Principles 🧱

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CORE OOP DESIGN PRINCIPLES                        │
├──────────────────────────┬──────────────────────────────────────────────────┤
│ Principle                │ Core Idea                                        │
├──────────────────────────┼──────────────────────────────────────────────────┤
│ Composition over         │ Prefer combining simple objects ("has-a") over   │
│ Inheritance              │ creating rigid, deep hierarchy trees ("is-a").   │
├──────────────────────────┼──────────────────────────────────────────────────┤
│ Loose Coupling           │ Reduce direct dependencies between modules using │
│                          │ interfaces (ABCs) and Dependency Injection.       │
├──────────────────────────┼──────────────────────────────────────────────────┤
│ High Cohesion            │ Keep classes small and focused on a single,      │
│                          │ well-defined responsibility (SRP).               │
├──────────────────────────┼──────────────────────────────────────────────────┤
│ Object Modeling          │ Map real-world domain concepts directly to       │
│                          │ encapsulated classes with strong invariants.      │
└──────────────────────────┴──────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

2. Principle Deep Dives 💡

1. Composition over Inheritance 🧩

Inheritance binds child classes tightly to parent implementation details (the fragile base class problem). Composition connects independent components via attributes, making systems flexible and easy to modify at runtime.

  INHERITANCE (Rigid & Fragile)              COMPOSITION (Flexible & Swappable)

      ┌──────────────────┐                         ┌──────────────────┐
      │   BaseNotifier   │                         │ InventoryManager │
      └────────┬─────────┘                         └────────┬─────────┘
               │                                            │  has-a
      ┌────────┴─────────┐                                  ▼
      │  EmailNotifier   │                         ┌──────────────────┐
      └──────────────────┘                         │ Notifier Protocol│
                                                   └────────┬─────────┘
                                                            │
                                            ┌───────────────┴───────────────┐
                                            ▼                               ▼
                                  ┌──────────────────┐            ┌──────────────────┐
                                  │  EmailNotifier   │            │   SlackNotifier  │
                                  └──────────────────┘            └──────────────────┘

Enter fullscreen mode Exit fullscreen mode

2. High Cohesion vs. Low Cohesion 🎯

  • Low Cohesion (Bad): A single class manages database storage, HTML rendering, validation, and email alerts.
  • High Cohesion (Good): Every class manages one thing. A UserRepository handles data, a UserValidator handles rules, and a Notifier handles communication.
# ❌ LOW COHESION (Do not do this)
class UserSystem:
    def save_to_db(self): ...
    def validate_email(self): ...
    def send_welcome_email(self): ...
    def render_dashboard_html(self): ...

# ✅ HIGH COHESION (Separation of Concerns)
class UserRepository:
    def save(self, user): ...

class EmailValidator:
    def validate(self, email): ...

class NotificationService:
    def send_welcome(self, user): ...

Enter fullscreen mode Exit fullscreen mode

3. Loose Coupling via Dependency Injection 🔌

Instead of instantiating dependencies inside a class, pass (inject) them into __init__. This allows swapping implementations (e.g., swapping a real database for a test mock) without touching internal logic.

# ❌ TIGHTLY COUPLED
class OrderProcessor:
    def __init__(self):
        # Hardcoded dependency! Cannot easily switch or mock during testing.
        self.notifier = EmailNotifier()

# ✅ LOOSELY COUPLED
class OrderProcessor:
    def __init__(self, notifier: NotificationService):
        # Injected dependency! Accepts ANY object implementing NotificationService
        self.notifier = notifier

Enter fullscreen mode Exit fullscreen mode

3. Top OOP Pitfalls to Avoid ⚠️

┌─────────────────────────────────────────────────────────────────────────────┐
│                            COMMON OOP PITFALLS                              │
├─────────────────────┬───────────────────────────────────────────────────────┤
│ Pitfall             │ Consequence & Remedy                                  │
├─────────────────────┼───────────────────────────────────────────────────────┤
│ The "God Object"    │ One massive class holds all state and methods.        │
│                     │ ➔ Split into smaller, single-responsibility classes.  │
├─────────────────────┼───────────────────────────────────────────────────────┤
│ Deep Inheritance    │ 4+ levels of subclassing makes code untraceable.      │
│                     │ ➔ Flatten hierarchies; replace with Composition.      │
├─────────────────────┼───────────────────────────────────────────────────────┤
│ Primitive Obsession │ Using raw dicts/strings everywhere instead of objects.│
│                     │ ➔ Encapsulate related fields into Dataclasses/Objects.│
├─────────────────────┼───────────────────────────────────────────────────────┤
│ Hidden Side-Effects │ Methods modify global state or unrelated attributes.  │
│                     │ ➔ Keep methods pure or explicitly bound to instance.  │
└─────────────────────┴───────────────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

4. Mini Project: Enterprise Inventory Management System 🏬

Let's put all 7 days of concepts together into a complete, modular Inventory Management System.

System Architecture

  1. Item (Dataclass): Domain model representing inventory items.
  2. DiscountStrategy (ABC / Composition): Pluggable pricing algorithms.
  3. NotificationService (ABC / Loose Coupling): Pluggable alerting interface.
  4. InventoryRepository (High Cohesion): Dedicated storage interface and memory repository.
  5. InventoryManager (Orchestrator): Business logic layer utilizing Dependency Injection.

Step 1: Initialize Your Workspace

uv init oop_day7 && cd oop_day7
touch inventory_system.py

Enter fullscreen mode Exit fullscreen mode

Step 2: Implement inventory_system.py

# inventory_system.py
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import time


# ==========================================
# 1. DOMAIN MODEL (Encapsulation & Dataclass)
# ==========================================
@dataclass
class InventoryItem:
    """Encapsulates item state with clear fields and validation."""
    item_id: str
    name: str
    unit_price: float
    quantity: int
    reorder_threshold: int = 5

    def is_low_stock(self) -> bool:
        """Domain logic method enforcing state invariants."""
        return self.quantity <= self.reorder_threshold

    def adjust_stock(self, amount: int) -> None:
        """Safely mutates stock levels with boundary checks."""
        if self.quantity + amount < 0:
            raise ValueError(f"Insufficient stock for '{self.name}'. Requested: {abs(amount)}, Available: {self.quantity}")
        self.quantity += amount


# ==========================================
# 2. PLUGGABLE STRATEGIES (Composition over Inheritance)
# ==========================================
class PricingStrategy(ABC):
    """Abstract strategy interface for dynamic price calculations."""
    @abstractmethod
    def calculate_price(self, item: InventoryItem) -> float:
        pass


class StandardPricing(PricingStrategy):
    def calculate_price(self, item: InventoryItem) -> float:
        return item.unit_price


class ClearancePricing(PricingStrategy):
    """Applies a 30% clearance discount."""
    def calculate_price(self, item: InventoryItem) -> float:
        return round(item.unit_price * 0.70, 2)


# ==========================================
# 3. NOTIFICATION INTERFACE (Loose Coupling)
# ==========================================
class NotificationService(ABC):
    """Abstract interface defining the alert contract."""
    @abstractmethod
    def send_alert(self, message: str) -> None:
        pass


class ConsoleNotifier(NotificationService):
    """Concrete implementation printing alerts to console."""
    def send_alert(self, message: str) -> None:
        print(f"📢 [CONSOLE NOTIFICATION] {message}")


class EmailNotifier(NotificationService):
    """Concrete implementation simulating email dispatch."""
    def __init__(self, admin_email: str):
        self.admin_email = admin_email

    def send_alert(self, message: str) -> None:
        print(f"📧 [EMAIL SENT TO {self.admin_email}] {message}")


# ==========================================
# 4. STORAGE REPOSITORY (High Cohesion)
# ==========================================
class InventoryRepository:
    """Dedicated class handling storage and query operations."""
    def __init__(self):
        self._items: Dict[str, InventoryItem] = {}

    def save(self, item: InventoryItem) -> None:
        self._items[item.item_id] = item

    def get(self, item_id: str) -> Optional[InventoryItem]:
        return self._items.get(item_id)

    def get_all(self) -> List[InventoryItem]:
        return list(self._items.values())


# ==========================================
# 5. ORCHESTRATOR SERVICE (Dependency Injection)
# ==========================================
class InventoryManager:
    """Core manager using Dependency Injection for loose coupling."""

    def __init__(
        self,
        repository: InventoryRepository,
        notifier: NotificationService,
        pricing_strategy: PricingStrategy
    ):
        # Dependencies injected via constructor
        self.repo = repository
        self.notifier = notifier
        self.pricing_strategy = pricing_strategy

    def add_item(self, item: InventoryItem) -> None:
        self.repo.save(item)
        print(f"✅ Added '{item.name}' to inventory.")

    def process_sale(self, item_id: str, quantity_sold: int) -> float:
        item = self.repo.get(item_id)
        if not item:
            raise KeyError(f"Item ID '{item_id}' not found.")

        # Deduct stock safely
        item.adjust_stock(-quantity_sold)

        # Calculate price using composed pricing strategy
        unit_price = self.pricing_strategy.calculate_price(item)
        total_price = unit_price * quantity_sold

        print(f"🛒 Sold {quantity_sold}x '{item.name}' @ ${unit_price:,.2f} each (Total: ${total_price:,.2f})")

        # Trigger notification if stock drops below threshold
        if item.is_low_stock():
            self.notifier.send_alert(
                f"LOW STOCK WARNING: '{item.name}' (ID: {item.item_id}) has only {item.quantity} unit(s) remaining!"
            )

        return total_price

    def display_inventory_report(self) -> None:
        print("\n==========================================================")
        print("               CURRENT INVENTORY REPORT                   ")
        print("==========================================================")
        for item in self.repo.get_all():
            effective_price = self.pricing_strategy.calculate_price(item)
            status = "⚠️ LOW STOCK" if item.is_low_stock() else "OK"
            print(f" [{item.item_id}] {item.name:<18} | Stock: {item.quantity:>3} | Unit Price: ${effective_price:>7.2f} | Status: {status}")
        print("==========================================================\n")


# ==========================================
# TEST EXECUTION SUITE
# ==========================================
if __name__ == "__main__":
    print("--- 1. Initializing System Components ---")

    # Instantiate standalone components
    storage_repo = InventoryRepository()
    email_notifier = EmailNotifier("inventory_admin@company.com")
    clearance_pricing = ClearancePricing()

    # Inject dependencies into Orchestrator
    manager = InventoryManager(
        repository=storage_repo,
        notifier=email_notifier,
        pricing_strategy=clearance_pricing
    )

    print("\n--- 2. Populating Initial Inventory ---")
    item1 = InventoryItem(item_id="SKU-101", name="Wireless Mouse", unit_price=29.99, quantity=12, reorder_threshold=4)
    item2 = InventoryItem(item_id="SKU-102", name="Mechanical Keyboard", unit_price=89.99, quantity=6, reorder_threshold=3)

    manager.add_item(item1)
    manager.add_item(item2)

    manager.display_inventory_report()

    print("--- 3. Executing Sales Transactions ---")
    # Sale 1: Normal reduction
    manager.process_sale("SKU-101", quantity_sold=5)

    # Sale 2: Triggers low stock alert via EmailNotifier
    manager.process_sale("SKU-101", quantity_sold=4)

    manager.display_inventory_report()

    print("--- 4. Dynamically Swapping Components at Runtime ---")
    # Easily swap pricing strategy or notifier without modifying InventoryManager code!
    manager.pricing_strategy = StandardPricing()
    manager.notifier = ConsoleNotifier()

    # Sale 3: Triggers console alert with standard pricing
    manager.process_sale("SKU-102", quantity_sold=4)

    manager.display_inventory_report()

Enter fullscreen mode Exit fullscreen mode

Step 3: Run & Verify Execution

uv run inventory_system.py

Enter fullscreen mode Exit fullscreen mode

Output Summary

--- 1. Initializing System Components ---

--- 2. Populating Initial Inventory ---
✅ Added 'Wireless Mouse' to inventory.
✅ Added 'Mechanical Keyboard' to inventory.

==========================================================
               CURRENT INVENTORY REPORT                   
==========================================================
 [SKU-101] Wireless Mouse     | Stock:  12 | Unit Price: $  20.99 | Status: OK
 [SKU-102] Mechanical Keyboard| Stock:   6 | Unit Price: $  62.99 | Status: OK
==========================================================

--- 3. Executing Sales Transactions ---
🛒 Sold 5x 'Wireless Mouse' @ $20.99 each (Total: $104.95)
🛒 Sold 4x 'Wireless Mouse' @ $20.99 each (Total: $83.96)
📧 [EMAIL SENT TO inventory_admin@company.com] LOW STOCK WARNING: 'Wireless Mouse' (ID: SKU-101) has only 3 unit(s) remaining!

==========================================================
               CURRENT INVENTORY REPORT                   
==========================================================
 [SKU-101] Wireless Mouse     | Stock:   3 | Unit Price: $  20.99 | Status: ⚠️ LOW STOCK
 [SKU-102] Mechanical Keyboard| Stock:   6 | Unit Price: $  62.99 | Status: OK
==========================================================

--- 4. Dynamically Swapping Components at Runtime ---
🛒 Sold 4x 'Mechanical Keyboard' @ $89.99 each (Total: $359.96)
📢 [CONSOLE NOTIFICATION] LOW STOCK WARNING: 'Mechanical Keyboard' (ID: SKU-102) has only 2 unit(s) remaining!

==========================================================
               CURRENT INVENTORY REPORT                   
==========================================================
 [SKU-101] Wireless Mouse     | Stock:   3 | Unit Price: $  29.99 | Status: ⚠️ LOW STOCK
 [SKU-102] Mechanical Keyboard| Stock:   2 | Unit Price: $  89.99 | Status: ⚠️ LOW STOCK
==========================================================

Enter fullscreen mode Exit fullscreen mode

🎉 Congratulations on Completing the 7-Day OOP Series!

Top comments (0)