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
Notifierthat represents the contract for sending notifications. - Concrete implementations (
EmailNotifier,SMSNotifier) implement the abstraction. -
NotificationServicecoordinates 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)
```
Tests (example with pytest — test_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
```
How the principles apply here
-
SRP (Single Responsibility): Each class has a single responsibility:
EmailNotifieronly sends emails,SMSNotifieronly sends SMS,NotificationServiceonly orchestrates. -
DIP (Dependency Inversion):
NotificationServicedepends onNotifier(abstraction), not concrete implementations. This allows injecting mocks for testing. -
OCP (Open/Closed): To add
PushNotifieryou do not modifyNotificationService; create a newNotifierimplementation and register it. -
LSP & ISP: Implementations respect the
sendcontract 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
-
Create a virtual environment (recommended):
python -m venv .venv source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows pip install pytest -
Run the demo:
python notifiers.py -
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.

