SOLID principles are five design principles applicable to object-oriented design which makes code understandable, maintainable and extensible.
Why do we need SOLID?
Most of the real-world applications we come across in daily life are complex, it incorporates many features and sub-modules that require multiple dependencies, third-party integrations, and so on.
It's possible to code it without following any design principles. It would work well in short term. As we scale and try adding more features to the existing app it becomes a nightmare.
In this article, I would explain how SOLID principles help us to write maintainable and scalable code.
To understand these principles better, let us take an E-mail sending application as an example here.
Single Responsibility principle
This rule states that the code must be split based on multiple modules such that each module has only one responsibility.
Let's consider our Email sending app that gets input from multiple places like databases, and local file server and sends Emails to target clients.
One common class file can do all the work to fetch data from multiple sources and use an Email processing service to send Emails.
This looks like an interview coding question solved in a given time. However, our app isn't going to stay the same way. As it grows, we would be adding more features, so the separation of modules based on their responsibilities is ideal for the long term.
Open-Closed principle
This rule states,
"Classes should be open for extension and closed for modification"
This applies to any code in general. Adding a new feature to our existing code base should allow us to extend it but not make us rewrite it.
Trying new features like integration with Gmail or building a newsletter on top of our Email sending app shouldn't make us touch or rewrite our entire app, instead, it should allow us to extend along with existing functionalities.
This ensures our code is more stable and scalable.
Liskov Substitution principle
Subtypes must be substitutable for their base types
With Object-oriented design, we must be familiar with inheritance and interfaces for providing reusability and abstraction. This rule gives us a guideline on how it has to be done.
Let's see this with our example app.
In the above-shown design, IFileFormatReader acts as a common interface for fetching information from multiple data sources like text files, XML files and databases as well.
Although, connecting to DB and fetching information is a different implementation when compared to file reading, it's forced to implement the FileReader interface, violating the rule.
Interface Segregation principle
This rule is again a guideline on how our abstractions (aka Interfaces) should be designed on a higher level.
"A Client shouldn't be forced to depend on methods they do not use"
We shouldn't have a common interface for all concrete implementations. This is more of applying Single responsibility for our interfaces as well.
Let's see how this has to be done in our Email sending app.
Database Reader interface is defined separately that can be extended for other databases. The file Reader interface has been abstracted to handle files like text, and XML.
Dependency Inversion principle
"Entities must depend on abstractions, not on concretions"
In simple terms, the high-level module should not depend on the low-level module but it must depend on its abstractions.
To build the complex app we use a lot of external dependencies like a logger, third-party plugins, database backend and REST APIs.
If we are going to use it directly in our code then we get tightly coupled with their implementation which becomes hard during updates or migration. This has to be loosely coupled via abstractions such as wrapper classes for better maintainability.
Here's a look at how our Email sending app after SOLID.
There is some overlap between the design principles which we saw. The main objective here is to make our code modular and extensible.
Anti-pattern: These design principles can help design something from scratch. For an existing app, implementing SOLID would take huge effort with less impact but trying to achieve this incrementally will have better outcomes.