DEV Community

Bruno Enrique ANCCO SUAÑA
Bruno Enrique ANCCO SUAÑA

Posted on

Software Design Principles (with Real-World Python Example)

> Introduction

Software design is not only about writing code that works — it’s about creating systems that are maintainable, scalable, and easy to understand. As applications grow, poorly structured code quickly becomes difficult to modify or extend, leading to bugs and unnecessary complexity.

To prevent this, developers rely on design principles: guidelines that help us write cleaner, more adaptable code. Among these, the most widely known are the SOLID principles.

> The SOLID Principles

  1. Single Responsibility Principle (SRP)
    A class should have only one reason to change. This avoids classes that are overloaded with multiple responsibilities.

  2. Open/Closed Principle (OCP)
    Software entities should be open for extension but closed for modification. We should be able to add new features without rewriting existing code.

  3. Liskov Substitution Principle (LSP)
    Subtypes must be substitutable for their base types without altering the correctness of the program.

  4. Interface Segregation Principle (ISP)
    Clients should not be forced to depend on methods they don’t use. Interfaces should be small and focused.

  5. Dependency Inversion Principle (DIP)
    High-level modules should not depend on low-level modules. Both should depend on abstractions.

> Real-World Example: A Notification System

Let’s imagine an application that sends notifications via different channels: email, SMS, and push.

❌ Poor Design (Violates SRP and OCP)

class NotificationService:
    def send_notification(self, message: str, channel: str):
        if channel == "email":
            print(f"Sending EMAIL: {message}")
        elif channel == "sms":
            print(f"Sending SMS: {message}")
        elif channel == "push":
            print(f"Sending PUSH: {message}")
Enter fullscreen mode Exit fullscreen mode

Problems:

  • SRP Violation: One class handles multiple notification types.
  • OCP Violation: Adding a new channel requires editing this class.
  • Hard to Test: Logic for different responsibilities is mixed together.

✅ Improved Design (Applies SOLID in Python)

from abc import ABC, abstractmethod

# Abstraction (DIP, OCP)
class Notifier(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        pass

# Concrete implementations (SRP: one responsibility each)
class EmailNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f"Sending EMAIL: {message}")

class SMSNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f"Sending SMS: {message}")

class PushNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f"Sending PUSH: {message}")

# Service that depends on abstraction (DIP)
class NotificationService:
    def __init__(self, notifier: Notifier):
        self.notifier = notifier

    def notify(self, message: str) -> None:
        self.notifier.send(message)

# Example usage
if __name__ == "__main__":
    email_service = NotificationService(EmailNotifier())
    sms_service = NotificationService(SMSNotifier())
    push_service = NotificationService(PushNotifier())

    email_service.notify("Welcome to our platform!")
    sms_service.notify("Your OTP is 123456")
    push_service.notify("You have a new message")
Enter fullscreen mode Exit fullscreen mode

Why This Design is Better

  • SRP: Each class has a single job (email, SMS, or push).
  • OCP: Adding a new channel (e.g., SlackNotifier) requires no modification to existing code.
  • LSP: Any Notifier subclass can replace another without breaking the program.
  • DIP: NotificationService depends on the abstract Notifier, not concrete implementations.
  • Testability: Each notifier can be tested independently.

> Additional Design Concepts

While SOLID is a cornerstone, there are other important design principles:

  • DRY (Don’t Repeat Yourself): Avoid code duplication by extracting reusable components.
  • KISS (Keep It Simple, Stupid): Simpler solutions are easier to maintain and less error-prone.
  • YAGNI (You Aren’t Gonna Need It): Don’t add functionality until it’s actually required.
  • Composition Over Inheritance: Favor combining objects over deep inheritance hierarchies for flexibility.

Together, these principles help prevent code bloat, reduce bugs, and improve collaboration in large teams.

> GitHub Repository with Automation

A working implementation of this notification system (including tests and CI/CD) is available here:

Software_design_principles_python

The repository contains:

  • Source code (src/)
  • Unit tests (tests/)
  • A GitHub Actions workflow (.github/workflows/python-app.yml) for continuous testing

> Conclusion

Design principles are more than abstract theory: they shape how maintainable and extendable our systems are. By applying principles like SOLID, DRY, and KISS to something as simple as a notification system, we get code that is flexible, testable, and future-proof.

When applied consistently, these principles enable developers to write software that can evolve gracefully — even as requirements change or systems grow.

Top comments (0)