This article was inspired by a conversation with my mentee, a Software Engineering student in Canada. After explaining the basics of Object-Oriented Programming (OOP) to him, I asked him to read about SOLID principles.
As expected, he came back with questions. The discussion that followed made me realize the need to explain SOLID principles in simple terms. That’s what I aim to do here—to make this essential topic in software development easier to understand for everyone.
The SOLID principles, created by Robert C. Martin (aka Uncle Bob), are a set of best practices for designing software. Simply put, they guide you in building scalable and maintainable systems. While they aren’t the only way to design software, following these principles can help ensure your system grows smoothly as demand/requirements changes.
Let’s dive into SOLID, an acronym where each letter stands for a key principle:
S: Single Responsibility
O: Open-Closed
L: Liskov Substitution
I: Interface Segregation
D: Dependency Inversion
Single Responsibility Principle (SRP):
This principle states that a class should have one job and only one.
For example, in an ecommerce app, a class handling authentication shouldn’t also manage products or orders. Mixing responsibilities like this can lead to bugs and make your code harder to debug and maintain. Keeping each class focused on a single task ensures cleaner and more scalable code.
Open-Closed Principle (OCP):
This principle says a class should be open for extension but closed for modification.
In simple terms, you should be able to add new features to existing code without changing its original functionality.
Let's say you initially built a robot that can walk. Later, you want to add jumping ability. The wrong way would be to go back and change the original walking code. Instead, you should design your code, so new movements can be added without touching the existing walking functionality.
So Closed for modification means “Existing code doesn’t need to change”
Open to extension means “You can include new functionalities by extending the existing code”
Liskov Substitution Principle (LSP):
This principle means that a child class should be able to do everything its parent class can do.
In Object-Oriented Programming (OOP), when a new class (child) is created from an existing one (parent), the child class should be a proper replacement.
Think of it like substituting players in a football game. If one player is replaced, the substitute should at least know how to play football—not hockey or basketball. This ensures the system works as expected when the child class is used in place of the parent.
- Just like how a substitute player must understand the rules and basic skills of football.
- A subclass must maintain the expected behaviors of its parent class.
- The substitute might play differently (striker vs defender) but must fulfill the basic requirements of being a footballer.
- If you put in someone who can't play football at all, it breaks the game (violates LSP).
Interface Segregation Principle (ISP):
This principle says a class should only depend on the interfaces it actually uses.
In simple terms, it’s better to have many smaller, specific interfaces than one large, general-purpose one.
For example, if both robots and humans are workers in your system, they have different needs. Humans eat, sleep, and work, while robots only work. Instead of forcing both to use the same interface (with eat, sleep, and work methods), create separate interfaces for each.
Why does this matter?
- Avoids bugs (e.g., a robot trying to "eat").
- Makes the code easier to understand.
- Reduces unnecessary dependencies.
- Keeps the system flexible and easier to maintain.
The goal is to create small, focused interfaces tailored to each class's needs.
Dependency Inversion Principle (DIP) :
This principle states: “Depend on abstractions, not concretions.”
In simple terms:
High-level modules should not directly depend on low-level modules.
Both should depend on abstractions.
Example:
Imagine you're building a system to notify buyers when their order is processed.
Without DIP: Your OrderProcessor
directly depends on EmailSender
, making it hard to add other notification types like SMS.
With DIP: Create a NotificationSender
interface with an abstract method for sending notifications. Then, implement this interface for different types, like EmailSender
and SMSSender
. Now, OrderProcessor
only depends on the interface, making it easy to switch or add new notification types.
Benefits of this approach:
Decoupling: The OrderProcessor
is no longer tightly coupled to a specific notification method.
Flexibility: You can easily add new notification types by creating new classes that implement the NotificationSender
interface.
Scalability: Adding a new notification type (like WhatsApp or Push Notification) becomes trivial.
Testability: You can easily mock the notification sender for unit testing and if something breaks, you can easily debug it.
Summary
I hope this explanation was easy to understand! If you have any questions, feel free to drop them in the comments, and I’ll be happy to help.
Next, I plan to share code examples of these concepts to make them even clearer and more relatable. Stay tuned!
Top comments (0)