Design Principles of Software Applied: Practical Example in Python

Design Principles of Software Applied: Practical Example in Python

Publish Date: Sep 14
0 0

Design Principles of Software Applied: Practical Example in Python

Summary: In this article I explain key software design principles (SOLID —with emphasis on SRP and DIP—, DRY, KISS, YAGNI) and show a minimal, practical example in Python: a notification service (email + SMS) designed to be extensible, testable, and easy to understand.


Chosen principles

  • SOLID (SRP, OCP, LSP, ISP, DIP) — emphasis on SRP and DIP.
  • DRY (Don't Repeat Yourself).
  • KISS (Keep It Simple, Stupid).
  • YAGNI (You Aren't Gonna Need It).
  • Separation of concerns / Testability / Modularity.

Real problem

We need a component that sends notifications to users via multiple channels (e.g., email and SMS). Practical requirements:

  • Be able to add new channels (Push, Webhook) without changing core logic.
  • Make unit testing easy without real network calls.
  • Keep code clear and responsibilities separated.

Design (brief)

  • Define an abstraction Notifier that represents the contract for sending notifications.
  • Concrete implementations (EmailNotifier, SMSNotifier) implement the abstraction.
  • NotificationService coordinates notifiers via dependency injection (it does not know concrete implementations).
  • External clients (SMTP, SMS provider) are encapsulated and mocked in tests.

Example code (suggested files: notifiers.py, test_notifiers.py)

```python
# notifiers.py
from abc import ABC, abstractmethod
from typing import List, Dict

class Notifier(ABC):
    """Contract: any Notifier must implement send."""
    @abstractmethod
    def send(self, to: str, subject: str, body: str) -> bool:
        pass

class EmailNotifier(Notifier):
    """SRP: this class only knows how to send emails."""
    def __init__(self, smtp_client):
        # smtp_client encapsulates real sending logic
        self.smtp = smtp_client

    def send(self, to: str, subject: str, body: str) -> bool:
        return self.smtp.send_email(to, subject, body)

class SMSNotifier(Notifier):
    def __init__(self, sms_client):
        self.sms = sms_client

    def send(self, to: str, subject: str, body: str) -> bool:
        # Simplify: use subject as prefix in SMS
        text = f"{subject}: {body}"
        return self.sms.send_sms(to, text)

class NotificationService:
    """DIP: depends on the Notifier abstraction, not concrete classes."""
    def __init__(self, notifiers: List[Notifier]):
        self.notifiers = notifiers

    def notify_all(self, to: str, subject: str, body: str) -> Dict[str, bool]:
        results = {}
        for n in self.notifiers:
            key = n.__class__.__name__
            results[key] = n.send(to, subject, body)
        return results

# Mock clients for demo/local
class MockSMTPClient:
    def send_email(self, to, subject, body):
        print(f"[MockSMTP] Sending email to {to}: {subject} / {body}")
        return True

class MockSMSClient:
    def send_sms(self, to, text):
        print(f"[MockSMS] Sending SMS to {to}: {text}")
        return True

if __name__ == "__main__":
    email_notifier = EmailNotifier(MockSMTPClient())
    sms_notifier = SMSNotifier(MockSMSClient())
    svc = NotificationService([email_notifier, sms_notifier])
    result = svc.notify_all("user@example.com", "Welcome", "Hi, thanks for signing up.")
    print("Result:", result)
```
Enter fullscreen mode Exit fullscreen mode

Tests (example with pytesttest_notifiers.py)

```python
# test_notifiers.py
from notifiers import Notifier, NotificationService

class DummyNotifier(Notifier):
    def __init__(self):
        self.sent = False
    def send(self, to, subject, body):
        self.sent = True
        return True

def test_notification_service_sends_to_all():
    a = DummyNotifier()
    b = DummyNotifier()
    svc = NotificationService([a, b])
    res = svc.notify_all("u@x.com", "t", "b")
    assert res["DummyNotifier"] is True
    assert a.sent and b.sent
```
Enter fullscreen mode Exit fullscreen mode

How the principles apply here

  • SRP (Single Responsibility): Each class has a single responsibility: EmailNotifier only sends emails, SMSNotifier only sends SMS, NotificationService only orchestrates.
  • DIP (Dependency Inversion): NotificationService depends on Notifier (abstraction), not concrete implementations. This allows injecting mocks for testing.
  • OCP (Open/Closed): To add PushNotifier you do not modify NotificationService; create a new Notifier implementation and register it.
  • LSP & ISP: Implementations respect the send contract and do not force extra unnecessary methods.
  • DRY: Transport-specific logic is encapsulated, avoiding duplication.
  • KISS / YAGNI: Simple design that covers current requirements; no retries, batching, or added complexity until required.
  • Testability / Modularity: By injecting dependencies and using mock clients, tests are deterministic and fast.

How to run locally

  1. Create a virtual environment (recommended):

    python -m venv .venv
    source .venv/bin/activate   # Linux/macOS
    .venv\Scripts\activate      # Windows
    pip install pytest
    
  2. Run the demo:

    python notifiers.py
    
  3. Run tests:

    pytest -q
    

Conclusion

This minimal example demonstrates how to apply design principles to build a notification component that is extensible, testable, and maintainable. By prioritizing abstractions, separation of concerns, and simplicity, the code is prepared to grow (add channels, instrumentation, retries) without becoming fragile.


Comments 0 total

    Add comment