Solid. Is It Still Useful In 2021?
Huzaifa Rasheed
Posted on April 3, 2021
Why Even Bother?
In the software development world, there are 2 extremes.
- People who don't follow best practices.
- People who follow them to the extreme.
If you are lazy like me you mostly don't follow best-practices because YAGNI(You aren't gonna need it) but if you are like me you mostly do follow best-practices like SOLID Design Principles.
Wait. Why am I on both sides? Because I follow both depending on what I am doing. If it's simple, limited/scoped, and predictable then who needs to overthink about best practices but if it's complex, may-become-complex, should be scalable and maintainable then yeah, we need best practices.
If you are building a system that would need changes in the future then you are going to be happy about how SOLID can make your life easy.
What is SOLID?
SOLID is an acronym for 5 principles
- S. 👉 Single Responsibility
- O. 👉 Open/Close
- L. 👉 Liskov Substitution
- I. 👉 Interface Segregation
- D. 👉 Dependency Inversion
They aim to make your code manageable, maintainable, and scalable along with other benefits.
Note
They are not Rules, but best practices.
The Person Behind SOLID
This was in the year 2000. Robert C. Martin first introduced SOLID as a subset of different design principles in his paper Design Principles and Design Patterns.
Design principles and patterns are different, SOLID are principles.
So What Do The Principles Mean?
Each SOLID principle aims to achieve a certain goal by following a certain rule.
1. Single Responsibility Principle
It aims to separate behaviors or concerns. Meaning that every piece of code should have a specific purpose of existence and it should be used for that purpose only.
Example
The following function should only validate a user given their id.
function validateUser(userId){
// will validate user with their userId
}
For a complete reference, check out the Single Responsibility Principle in detail.
2. Open/Close Principle
The goal is to prevent those situations in which changing a piece of code from a module also requires us to update all depending modules. Basically, we don't allow new code to make changes to our old code.
We can extend code but not modify it. One real-life use case is of those softwares that have backward compatibility.
Example
A typescript example
interface PaymentMethod {
pay() : boolean
}
class Cash implements PaymentMethod {
public pay(){
// handle cash pay logic here
}
}
function makePayment(payMethod: PaymentMethod) {
if(payMethod.pay()){
return true;
}
return false;
}
In the above code if we want to add credit card payment all we have to do is add the following code (along with the actual implementation) and it will work just fine
class CreditCard implements PaymentMethod {
public pay(){
// handle credit pay logic here
}
}
For a complete reference, check out my other article on Open/Close Principle.
3. Liskov Substitution Principle
What this principle tells us is that if we replace an instance of a child class with a parent class, our code should still work fine without breaking or having side effects.
Example
class Printer{
function changeSettings(){
// common functionality
}
function print(){
// common functionality
}
}
class LaserPrinter extends Printer{
function changeSettings(){
// ... Laser Printer specific code
}
function print(){
// ... Laser Printer specific code
}
}
class _3DPrinter extends Printer{
function changeSettings(){
// ... 3D printer specific code
}
function print(){
// ... 3D printer specific code
}
}
This principle however has its limitations some of which I discussed in its own separate article. See Liskov Substitution Principle for an example of its limitations.
4. Interface Segregation Principle
This principle aim's to use role interfaces (or role modules in general) which are designed for a specific purpose and should only be used for those. It says
Clients Should Not Be Forced To Depend Upon Interfaces That They Do Not Use.
This principle solves some of the issues with the Interface Segregation Principle like the Bird example I mentioned in my article on Liskov Substitution Principle
Example
This is a typescript example but still not too difficult to understand.
interface BirdFly{
fly(): void;
}
interface BirdWalk{
walk(): void;
}
class Duck implement BirdFly, BirdWalk{
fly(){
// Duck can fly
}
walk(){
// Duck can walk
}
}
class Ostrich implement BirdWalk{
walk(){
// Ostrich can walk
}
}
For a complete reference, check out the Interface Segregation Principle in detail.
5. Dependency Inversion Principle
It focuses on using abstraction or facade/wrapper patterns to hide details of low-level modules from their high-level implementation.
We basically create wrapper classes that sit in between high-level and low-level modules. This helps a lot if the low-level implementations are different from each other.
Example
Again a typescript example
interface Payment {
pay(): boolean
}
// (Wrapper/Abstraction around cash payment)
class CashHandler implements Payment {
constructor(user){
this.user = user
this.CashPayment = new CashPayment();
}
pay(amount){
this.CashPayment.pay(amount)
}
}
// (low-level module)
class CashPayment {
public pay(amount){
// handle cash payment logic
}
}
// (High-level Module)
function makePayment(amount: number, paymentMethod: Payment){
if(paymentMethod.pay(amount)){
return true;
}
return false;
}
For a complete reference, check out the Dependency Inversion Principle in detail.
When To Use What And Avoid What
Now that we know a brief about each principle, we will look at when to use and avoid them.
Use | Avoid | |
---|---|---|
Single Responsibility | For Scalable and Maintainable Code. | When too much Fragmentation occurs without predictable future changes. |
Open Close | To prevent old code from breaking due to a new one. | When over-engineering. |
Liskov Substitution | Parent/Child used interchangeably without breaking. | When substitutions do not make sense. (Bird example) |
Interface Segregation | For Role-specific interfaces. | When difficult to aggregate (due to a lot of modules) from segregation. |
Dependency Inversion | For different low-level implementations. | When different implementations of a low-level module are not needed, like the String class in most languages are not changed because it's not needed to mostly. |
These are mostly the reasons and you can disagree but it all comes down to what you are dealing with.
Is SOLID Still Useful in 2021?
Ask yourself. Is there a language that does everything with one line of code?
do_everything();
I guess not, unless you or someone makes a language that uses less code than python and does everything with one line of code, you do need SOLID design principles.
Of course, there are extremes and cases where it's just not possible to implement SOLID, but if you are comfortable and can use SOLID then you probably should.
Conclusion
So, what's your take on this? Do you follow an approach similar to mine? Be sure to give this article a 💖 if you like it.
Posted on April 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.