DEV Community

Cover image for Design Principles of Software Applied: Practical Example in Python
AHMED HASAN AKHTAR OVIEDO
AHMED HASAN AKHTAR OVIEDO

Posted on

Design Principles of Software Applied: Practical Example in Python

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.


Top comments (0)