Software design principles are fundamental guidelines that help developers create code that is reliable, maintainable, and scalable. By following these principles, teams can reduce technical debt, ensure better collaboration, and deliver high-quality software.
Why Do Design Principles Matter?
Poorly designed software often leads to hard-to-maintain code, bugs, and frustration among developers. Design principles provide a foundation for writing code that is easy to understand, modify, and extend.
Let’s explore some essential software design principles, and see how they can be applied in a real Python project.
Key Principles
1. Single Responsibility Principle (SRP)
Definition:
A class should have only one reason to change, meaning it should have only one job or responsibility.
Why?
If a class does too much, changes in one part can unintentionally affect another, leading to bugs and confusion.
2. Open/Closed Principle (OCP)
Definition:
Software entities (classes, modules, functions) should be open for extension but closed for modification.
Why?
You should be able to add new features by adding new code, not by changing existing, stable code.
3. Liskov Substitution Principle (LSP)
Definition:
Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Why?
This ensures that inheritance is used properly and that subclasses enhance, not break, the behavior of the base class.
4. Interface Segregation Principle (ISP)
Definition:
Clients should not be forced to depend on interfaces they do not use.
Why?
It’s better to have several small, specific interfaces than one big, general-purpose interface.
5. Dependency Inversion Principle (DIP)
Definition:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Why?
This reduces coupling between different parts of your application.
Example: Applying SRP and OCP in Python
Let’s build a simple notification system that sends messages via email or SMS. We'll apply the Single Responsibility Principle (SRP) and Open/Closed Principle (OCP).
Initial (Bad) Design
class Notifier:
def send(self, message, type):
if type == "email":
# email sending logic
print(f"Sending EMAIL: {message}")
elif type == "sms":
# sms sending logic
print(f"Sending SMS: {message}")
Problems:
- The
Notifier
class has multiple responsibilities. - Every time a new notification type is added, we have to modify the class (
OCP
violation).
Refactored (Good) Design
Let’s use SRP by separating each notification type, and OCP by allowing new types via extension.
from abc import ABC, abstractmethod
class NotificationSender(ABC):
@abstractmethod
def send(self, message):
pass
class EmailSender(NotificationSender):
def send(self, message):
print(f"Sending EMAIL: {message}")
# Actual email logic here
class SMSSender(NotificationSender):
def send(self, message):
print(f"Sending SMS: {message}")
# Actual SMS logic here
def notify(sender: NotificationSender, message: str):
sender.send(message)
# Usage example
email_sender = EmailSender()
sms_sender = SMSSender()
notify(email_sender, "Hello via Email!")
notify(sms_sender, "Hello via SMS!")
Benefits:
- Each sender class has a single responsibility (
SRP
). - New senders (e.g.,
PushSender
) can be added without modifying existing code (OCP
).
You can find the complete code and exercises in the companion repository:
SebastianFuentesAvalos/article1_software-design-principles-python
Conclusion
Applying software design principles like SRP and OCP leads to cleaner, more maintainable, and scalable code. In Python, using abstract base classes and separation of concerns makes it easy to follow these guidelines.
If you want to see more principles in action, you can extend this example—try adding a PushSender
for push notifications!
References:
Written by Sebastián Fuentes Avalos
Top comments (4)
Made with soul and blood 🩸
Oh yeah , My nigga friend 🗣️😸
good article
Really so good.