Understanding SOLID Principles: A Beginner's Guide to Clean Code
EP01 | System Design | LLD Series | #01
As a developer, writing code that works is just one part of the job. Writing code that’s easy to understand, maintain, and extend over time is what really makes a great developer. That’s where the SOLID principles come in picture.
If you’re new to the SOLID principles, don’t worry. In this guide, I’ll walk you through what each principle means and show you simple examples that you can apply in your code.
1. Single Responsibility Principle (SRP)
Each class should have only one responsibility.
Imagine you have a class that’s doing too many things. Every time you change one part, something else breaks. The Single Responsibility Principle (SRP) tells us that each class should only do one thing.
Example:
Let’s say you have an InvoiceManager class that does everything related to invoices.
This class has too many responsibilities! It’s creating invoices, printing them, saving them to the database, and sending emails. If you ever need to change how you send emails, you’ll have to change the InvoiceManager class and you might accidentally end up breaking the other parts.
Instead, you can split this into smaller classes based on SRP so that each class handle only one job as mentioned below:
Now each class has only one responsibility, making the code easier to manage.
2. Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
This means you should be able to add new features without making modification in the existing code. This avoids breaking things that already work.
Example:
Let’s say you have a PaymentProcessor class that processes different types of payments:
But what if you want to add support for a new payment method like Google Pay? You’d have to go back and change the class, which can lead to errors.
Instead, you can design the code so it’s easy to extend with new payment types, without changing the old code. We can achieve this by creating an interface which will process the payments and for different payment methods we can have different classes.
Now, if you want to add Google Pay, you just create a new class that implements PaymentMethod—no need to change the existing code!
3. Liskov Substitution Principle (LSP)
We should be able to substitute the base class objects with its subclasses objects without breaking the application.
This principle means that if you have a class that inherits from another class, it should behave in a way that doesn’t break the program when used in place of the parent class.
Example:
Let’s look at a Bird class. All birds can move, but not all can fly:
Here, Penguin extends Bird, but it can’t fly. If you try to use a Penguin where you expect a Bird, the code will break.
To fix this, we should design the Bird class to allow different types of movement and for other birds who can fly we can create a new class which extends Bird class:
Now, Penguin can be used wherever a Bird is expected without causing application failure.
Subscribe to receive new articles every week.
4. Interface Segregation Principle (ISP)
Don’t force classes to implement methods they don’t need.
This principle says that instead of creating one big interface that has many methods, it’s better to create smaller, specific interfaces. That way, a class only has to implement the methods it needs.
Example:
Let’s say we have a Worker interface that looks like this:
A Developer might only need to work, but not attend meetings. If we force Developer to implement attendMeeting(), we’re violating ISP.
Instead, we can segregate the interface with two different interfaces i.e. Workable and Attendable. Now other classes can implement one of them or both the interfaces as needed :
Now, Developer only implements the Workable interface and doesn’t need to worry about meetings.
5. Dependency Inversion Principle (DIP)
High-level modules should depend on abstractions, not low-level details.
This means that the core logic of your program should not depend directly on lower-level details like how data is stored. Instead, both the core logic and lower-level parts should rely on abstractions (like interfaces).
Example:
Let’s say you have a BusinessLogic class that saves data to a database:
The problem here is that BusinessLogic directly depends on the Database class. If you want to change how data is saved (e.g., to a file instead of a database), you’d need to change the code.
To apply DIP, we can use an interface to represent the data storage and we can create separate classes for database storage and file storage:
Now, BusinessLogic depends on the DataStorage interface, not on the Database class. You can switch to FileStorage without changing the core logic.
Conclusion
The SOLID principles are designed to help you write cleaner and more maintainable code. While these examples are simple, the concepts apply to any project, big or small. By following SOLID, your code will be easier to understand, extend, and fix when things go wrong.
Keep these principles in mind as you continue your development journey, and you’ll find that your code becomes much more flexible and robust over time!
If you find the article helpful and informative, hit a like ❤️ and consider subscribing for more such articles every week.
If you have any questions or suggestions, drop a comment.