SOLID Principles in 5 min

The SOLID principles are sets of design principles. So before we go into more detail about the solid principles let’s briefly discuss what is the design principle.

So what is the design principle?

Design principles are high-level guidelines that help us to design better software applications. Design principles mostly do not provide implementation guidelines. That's what the design patterns are for.

The SOLID principles are one of the most popular sets of design principles.

The SOLID principles

SOLID  is an acronym that's made up of the first letter of the five principles. Those are,

  1. S - Single-responsibility Principle
  2. O - Open-closed Principle
  3. L - Liskov Substitution Principle
  4. I - Interface Segregation Principle
  5. D - Dependency Inversion Principle

The SOLID principles were introduced by the software engineer Robert Martin.

1. S - Single-responsibility Principle

This principle is -

“There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.”

There is sanity testing we can do to check whether a component eg. class, function, component or microservice follows the single-responsibility principle or not. The question for the test is, “What is the responsibility of the component?”. 

If the answer comes up with more than one responsibility then the component is breaking the single-responsibility Principle.

2. O - Open-closed Principle

The principle is -

“Software entities should be open for extension, but closed for modification.”

Let's discuss the principle with an example. Let's say we have an online store and we are writing a check-out functionality of the store. One way to implement it is -

function completeCheckOut(cart, address, paymentType) {
  totalToPay = cart.totalItemPrice() + address.shipingCharge()

  if (paymentType == 'cash-on-delivery') {
    chargeLater()
  } else if (paymentType == 'credit-card') {
    chargeWithBankGateway()
  }
}
        

Here we are using if-else to accept payment. Now if we want to add PayPal support we would have to add another if-else block to this `completeCheckOut()` function. This would violate the principle.

One way we could fix this is -


function completeCheckOut(cart, address, paymentMethod){
    totalToPay = cart.totalItemPrice() + address.shipingCharge()

   paymentMethod.charge(totalToPay)
}
        

So now we can add any new payment method but `completeCheckOut()` will not need any change.

One note: This does not mean we always have to think about how we can make everything an open-closed system. We can always implement the simplest solution first and then when there is a new feature or functionality we need to implement we can refactor and make it an open-closed system for the new feature.

The sanity testing

When adding new functionality, we can ask, "Do we need to modify an existing function or entity?". If the answer is yes then most likely we are violating the open-closed principle. In this case, it would be a good time to take one step back and refactor the code to make it an open-closed system and then implement the new functionality after.

3. L - Liskov Substitution Principle

The principle is -

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it

Basically, the principle is trying to say is -

If a client has a dependency on a class we should be able to switch the class with the class's subclass without the client knowing about the change.

Let's see an example that violates the  Liskov Substitution Principle.

class Rectangle {
    int height
    int width

    setHeight(height){
        this.height = height
    }

    setWidth(width){
        this.width = width
    }
}

class Square extends Rectangle{
    setHeight(height){
        this.height = height
        this.width = height
    }
}        

Here the Square class extends the Rectangle. Unlike the base class Rectangle in the subclass Square, we are setting both the height and width in the `setHeight()` function. Also, any client that uses the Rectangle class can still set the width via the `setWidth()` function. This is violating the Liskov Substitution Principle.

It is clear from the example that violations of the Liskov Substitution Principle cause buggy behavior, which can lead to a lot of wasted time trying to find out where the bug is.

In this example of Rectangle and Square, it is better to use the different classes without making them dependent on each other.

The sanity testing

  1. Is the subclass's override functions returning a value that's type is different from the base class?
  2. Is the subclass's override functions throwing a new exception which is not thrown in the base class?
  3. Is the subclass violating any constraints that are set in the base class?
  4. Is the subclass adding a new constraint that is not in the base class?

If either of the questions' has the answer yes, then most likely the class is violating the Liskov Substitution Principle.

4. I - Interface Segregation Principle

The principle is -

A client should never be forced to implement an interface that it doesn’t use

Let's see an example that violates the Interface Segregation Principle.

class Bird{
    fly(){}
}
class Eagle extends Bird{}

class Penguin extends Bird{
    fly(){
        throw NoFlyException()
    }
}
        

Here we have the `Bird` class which can fly. If we extend the Bird class to declare Eagle class then it makes sense because obviously, Eagle can fly. Now look if we use Bird class to declare Penguin class, we have to throw an exception because Penguin cannot fly. This is violating the Interface Segregation Principle.

Let's modify it so that it will not violate the principle.

class Bird{}
class Flyable{
    fly(){}
}

class Penguin extends Bird{}

class Eagle extends Bird, Flyable{}
        

Here we are extending the Bird class and Flyable class so that we can use them to declare Eagle class.

The sanity testing

Is there any subclass that has to implement a function only because the base class has the function, but the subclass does not need the functionality?

If the answer is yes, then most likely the  Interface Segregation Principle has been violated. One quick way to find the answer is to look for a function throwing exception `NotImplemented` or returning null only because it has extended a base class.

5. D - Dependency Inversion Principle

The principle is,

 High-level modules should not depend on low-level modules. Both should depend on the abstraction.

Here, for High-level modules, we can think of a module or entity which uses low-level modules or entities to execute an operation.

Let's discuss this principle with an example. Let's say we are implementing a user login service that can log in a user with the email or with google.

One way to implement it is -


class LoginService {
    login(loginType) {
        if (loginType == 'google') {
            googleService = new GoogleService()
            googleAuthenticator = googleService.getAuthenticator()
            googleAuthenticator.authenticate()
        } else if (loginType == 'email') {
            userService = new UserService()
            user = userService.getUserByEmail()
            user.authenticateWithPassword()
        }
    }
}
        

In this example, the high-level module LoginService depends too much on the low-level modules like GoogleService and UserService. This violates the Dependency Inversion Principle.

So let's fix this,


class EmailAuthenticator {
    authenticate() { }
}

class GoogleAuthenticator {
    authenticate() { }
}

class AuthenticatorFactory {
    getAuthenticator(loginType) {
        // return an Authenticator based on the login type
    }
}

class LoginService {
    constructor(authenticatorFactory) {
        this.authenticatorFactory = authenticatorFactory
    }

    login(loginType) {
        this.authenticatorFactory
            .getAuthenticator(loginType)
            .authenticate()
    }
}
        

Now the LoginService does not need to know all the low-level details of the different services. The LoginService will just call the `authenticate()` function and that's it!

To view or add a comment, sign in

More articles by Rakibul Hasan

  • Writing Effective OKRs: A Guide

    Objectives and Key Results (OKRs) are a powerful way to align an organization and focus everyone on the same…

    3 Comments

Others also viewed

Explore content categories