SOLID

SOLID programming design principles help to develop systems that are more scalable, robust and flexible which can increase the maintainability and reduce technical debt.

What is SOLID?

Solid is an acronym that stands for:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

And they all provide benefits to Software Engineers when building software.

1. Single Responsibility Principle

The single responsibility principle (SRP) is about making sure that your functions only have one responsibility or action to complete. For example, let's think about a Baker whose sole job is to bake quality bread. If this Baker then also has to worry about keeping inventory stocked and tending to customers this would break his SRP of just baking the bread so these tasks should be split between different people.

Let’s look at some code to visualise this example.

class Baker {
    static bakeBread() {
        console.log('Baking bread');
    }
}

class InventoryManager {
    static manageInventory() {
        console.log('Managing inventory');
    }
}

class CustomerServer {
    static serveCustomer() {
        console.log('Serving customer');
    }
}

class Cleaner {
    static cleanBakery() {
        console.log('Cleaning bakery');
    }
}

Baker.bakeBread();
CustomerServer.serveCustomer();
InventoryManager.manageInventory();
Cleaner.cleanBakery();

In the code snippet above, you can see that there are 4 classes and each one has one job to do.

  • Baker: Solely responsible for baking bread.
  • CustomerServer: Solely responsible for serving customers that visit the bakery.
  • InventoryManager: Solely responsible for managing the bakery’s inventory.
  • Cleaner: Solely responsible for keeping the bakery clean.

$ node example.js

Baking bread

Managing inventory

Serving customer

Cleaning bakery

Example code snippet output

2. Open/Closed Principle

This principle is about writing classes and functions in ways where they are open to be extended but closed for being modified which means that you should be able to extend its functionality without modifying it.

Imagine the bakery’s payment system, you don’t know what payment methods they may want to use in the future so you would want the system to be easy to extend. You can do this by having a PaymentMethod class with a method to set the payment method that each payment type that is added goes through to set the payment method.

class PaymentMethod {
    setPaymentMethod(paymentMethod) {
        this.paymentMethod = paymentMethod;
    }
}

This is how a card payment method might work. It extends PaymentMethod and uses its constructor to set the payment method by calling PaymentMethod’s setPaymentMethod method.

class CreditCardPaymentMethod extends PaymentMethod {
    constructor(cardNumber) {
        super().setPaymentMethod('Credit Card');
        this.cardNumber = cardNumber;
    }

    pay(amount) {
        console.log(`Paid £${amount} via ${this.paymentMethod} ending in ${this.cardNumber}`);
    }
}

let creditCardPaymentMethod = new CreditCardPaymentMethod(9203);
creditCardPaymentMethod.pay(1.49);

In the snippet above you can see 2 lines of code that show how to run this code snippet. You start by instantiating a new instance of the CreditCardPaymentMethod class and to keep it simple, pass in the last 4 digits of a card number so that this.cardNumber in the constructor gets set. You then call the pay function on the variable that you stored your class instance in and pass in the payment amount.

$ node example.js

Paid £1.49 via Credit Card ending in 9203

Example code snippet output

3. Liskov's Substitution Principle

Introduced in 1987, this principle states that any child or sub classes should be usable instead of the parent class without any unexpected behaviour.

Let's take a look at an example of this using birds.

Below is an example that breaks this principle.

class Bird {
    fly() {
        console.log('Flying through the clouds');
    }
}

class Parrot extends Bird {
    repeat(){}
}

class Duck extends Bird {
    quack(){}
}

class Penguin extends Bird {
    honk(){}
}

In the snippet above you can see a few different classes that extend a base Bird class that has the method fly on it, these different classes being a parrot, a duck and a penguin however penguins cannot fly so we have to override the fly method to throw an error in case anyone tries to use the fly method on a penguin.

But now there’s a problem, our fly method doesn't expect an error to be thrown as the fly method was created only for birds who can fly. Penguins can't fly, so we break the Liskov Substitution Principle.

So, how can we make this example abide by the Liskov Principle?
We can do this by making a FlyingBird class that extends the base bird class and this new class is what contains the fly method. Now, anytime we want to add a bird that flies we should extend the FlyingBird class so that it can access to fly method and for birds that can’t fly we should extend the base bird class and this way birds like penguins don’t have the fly method to throw an error as it can’t fly.

class Bird {}
  
class FlyingBird {
    fly() {
        console.log('Flying through the clouds')
    }
}

class Parrot extends FlyingBird {
    repeat(){}
}

class Duck extends FlyingBird {
    quack(){}
}

class Penguin extends Bird {
    honk(){}
}

4. Interface Segregation Principle

This principle states that you should not force any code to implement a method that it will not use.
The previous example about Penguins and Birds is also a good example for this principle. See in the code snippet below that rather than have one bird class we could have it split into two so that we can only pull in necessary code for each type of bird, can fly or can’t fly. So we can extend the flying bird class for Parrot to give it access to the fly function but we extend the base bird class for Penguin so that it doesn’t have access to the fly function that it would never use.

class Bird {}
  
class FlyingBird {
    fly() {
        console.log('Flying through the clouds')
    }
}

class Parrot extends FlyingBird {
    repeat(){}
}

class Duck extends FlyingBird {
    quack(){}
}

class Penguin extends Bird {
    honk(){}
}

5. Dependency Inversion Principle

This principle is similar to the previous 2 that I’ve gone over in this blog, Liskov’s Substitution principle and Interface Segregation principle, in that it’s about separating code out to make it easier to edit or add to down the line. This principle states that high level code should never rely on low level code.

Below is some code that doesn’t follow this principle because ResetPassword is being passed the database connection directly so if we were to change database being used we would also have to make changes to the ResetPassword class.

class MariaDBConnection {
    connect() {}
}

class ResetPassword {
    constructor() {
        this.db = new MariaDBConnection();
    }
}

If we refactor this code to follow this principle then our ResetPassword class would receive a general connection interface through its constructor which allows you to easily change which database is being used by passing a different instance into ResetPassword when instantiating the class.

class MariaDBConnection {
    connect() {
        console.log("Connecting to MariaDB...");
    };
}

class RedisConnection {
    connect() {
        console.log("Connecting to Redis...");
    }
}

class ResetPassword {
    constructor(connection) {
        this.db = connection;
    }
}

let db = new RedisConnection();
let resetPassword = new ResetPassword(db);
resetPassword.db.connect();

Final Thoughts

SOLID principles are very useful tools to use when programming, but like all things they shouldn’t be followed blindly and used in all cases. They should be used where and when it makes sense to use them where they will help improve the maintainability and usability of your code.

Read more