SOLID principles

One of the biggest advantages of object oriented programming is the amount of good practices and design patterns that have been researched and documented. Most of these exist so that introducing changes in future is easy and safe. Once a piece of software reaches critical mass, it inevitably ‘hardens’ – making changes to it is risky, it becomes extremely tough to even read and understand. Someone who attempts to fix a bug or extend a functionality, feels threatened that they might break something by making their change – and rightly so. If code is not well written, something will almost always break when a change is made.

Being able to write code which is easy to understand, maintain and extend is what separates the great coders from the thousands of people who simply write code that barely works. The SOLID principles is one set of simple guidelines that are extremely effective in elevating the quality of your code. If you don’t already know, SOLID is an acronym for 5 principles, namely –

  1. Single Responsibility Principle: Each class has one and only a single responsibility.
  2. Open-Closed Principle: Classes should be open for extensions, but closed for modifications.
  3. Liskov Substitiution Principle: Subclasses should be substitutable for parent classes.
  4. Interface Segregation Principle: Interfaces should be segregated into multiple, small, specific ones.
  5. Dependency Inversion Principle: A class should not be used directly inside another class. It should be abstracted using an interface.

I’ll explain my understanding of the 5 concepts in a bit more detail here.

Single Responsibility Principle

This one is easy to get. A class you create, should have only one responsibility. For example, when you are developing a web application, you will probably be using the MVC pattern. Most web applications also involve authorizations – like whether the current user have permissions to edit this record. Where does the code for doing this authorization go? If it is in the controller class, then the single responsibility principle is broken. The controller code should have only code that does the orchestration. Another example is validation. Your application should have a class that has all your validation code. The controller should only use this validation class to perform validations.

In the following code, UserController has only one responsibility – to orchestrate all the steps. Validator class has only one responsibility – to validate requests. Authorizer class has only one responsibility – to perform authorization.

class UserController {
    Validator userValidator;
    Authorizer userAuthorizer;

    public UserController(Validator userValidator, Authorizer userAuthorizer) {
        this.userValidator = userValidator;
        this.userAuthorizaer = userAuthorizer;
    }

    public Response createUser(Request request) {
        if(!userValidator.isValidRequest(request)) {
            return ErrorResponse("400: Bad Request");
        }
        if(!userAuthorizer.isAuthorized(request)) {
            return ErrorResponse("403: Forbidden");
        }
        // .. code to create user .. //
    }
}

Open Closed Principle

Classes should be open to extension but closed for modification. Let me put this principle in two parts –

  • It should be possible to extend a class when requirements of the application change. If you are building a bookstore application, you might have an Order object inside a OrderProcessor class. Your application should allow for future extension like ‘urgent’ orders.
class BookStore {
    OrderProcessor op;
    op.process()
}
class OrderProcessor {
    Order o;
    public void process() {
        // order processing logic
    }
}
  • It should not be possible to modify a class. This means, if you introduce a new class like ‘UrgentOrder’, you should not modify code inside the OrderProcessor class. Like ‘if (order instanceof UrgentOrder) … ‘ etc.
/* This is not allowed ! Closed for modification => must not change the OrderProcessor class. */
class BookStore {
    OrderProcessor op;
    op.process()
}
class OrderProcessor {
    Order o;
    public void process() {
        if (o.type = normal) {
            // order processing logic
        } else if (o.type == urgent) {
           // urgent order processing logic
        }
    }
}

The way we achieve this principle is by ensuring our classes will never be changed. We will only write new code to support new functionality. So this example, if written following the Open/Closed principle will be –

class BookStore {
    OrderProcessor op;
    public void execute() {
        op.process()
    }
    /* .. */
}
interface OrderProcessor {
    public void process();
}
class NormalOrder {
    Order o;
    public void process() {
        // order processing logic
    }
}
/* When a new type of order - 'urgent' order is introduced, we don't have to change the above code. Just write a new class implementing the OrderProcessor interface. */
class UrgentOrder {
    Order o;
    public void process() {
        // urgent order processing logic
    }
}

Liskov Substitution Principle

When you inherit a class, the new inherited class should be substitutable for the original parent class. That is, if your code has a ‘DatabaseConnection’ class, and you are extending this and creating a ‘CachedDatabaseConnection’, then this new type of object should be usable wherever the DatabaseConnection object was used.

If you are using a strongly typed language (like Java), you probably don’t even have to worry about this principle. It is automatically ensured that the extending class will be usable wherever the extended (parent) class was used. But if you are not using a strongly typed language, it is your responsibility to ensure this principle is not broken. An example scenario (in JavaScript) that breaks this principle –

class Item {
    stockQuantity() {
        return 5;
    }
}

class SpecialItem extends Item {
    stockQuantity() {
        return {"warehouse1":3, "warehouse2":2}
    }
}

The above example breaks Liskov Substitution principle because other code expects Item.stockQuantity to return a simple integer. If the new SpecialItem is substituted there, Item.stockQuantity will return a compound object and the application might break!

Interface Segregation Principle

I like to think of interface segregation principle as same as the single-responsibility principle, but for interfaces. It simply means that your interfaces should have enough segregation between them so that no class will have to implement methods not applicable for them.

interface Animal {
    public void swim();
    public void fly();
}

class Shark implements Animal {
    public void swim() {
        // I'm swimming
    }
    public void fly() {
        // Error: I'm a shark! I can't fly!
    }
}

class Crow implements Animal {
    public void swim() {
        // Error! I'm a crow, I can't swim!
    }
    public void fly() {
        // I'm flying
    }
}

The above example breaks interface segregation principle, because the Shark class has to unnecessarily implement the fly method, only to throw an error. Similarly the Crow class implements the swim method. To make this code compliant with interface segregation, the Animal interface should be segregated into two interfaces.

interface Fish {
    public void swim();
}

interface Bird {
    public void fly();
}

class Shark implements Fish { ... }
class Crow implements Bird { ... }
class FlyingFish implements Fish, Bird { ... }

Now that the interfaces are segregated, we can implement sharks, crows without having to write unnecessary code. And when a new class needs both functionality, we just implement both interfaces.

Dependency Inversion Principle

Using a class inside another class couples them tightly. Dependency inversion avoids this by introducing a layer of abstraction.

For example, a repository class will have a database connection object as a member. Now the repository class depends on the database connection object. If the database connection object needs to be changed (for example, you decide to use MySQL database instead of Oracle), even though the functionality doesn’t change, the repository class needs to be rewritten to use the new Oracle database connection object.

In the above example, if dependency inversion is deployed, the repository will no longer be dependent on the MySQL connection object. It will be dependent on an ‘interface’ like DBConnection. Now both MySQL and Oracle connection objects should ‘implement’ this DBConnection interface. In other words, the repository object, the MySQL connection, and the Oracle connection have all been made to depend on the DBConnection interface. The dependency has been ‘inverted’. As long as the MySQL and Oracle connection classes implement the DBConnection interface, they can be switched seamlessly.

Conclusion

So there, I’ve tried to explain the SOLID principles in the shortest possible way that I could think of. SOLID principles are like the foundations of object oriented programming – and this article doesn’t do justice to their importance at all. Watch this video – https://www.youtube.com/watch?v=zHiWqnTWsn4 to get a deeper understanding of these principles from Mr. Robert Martin. Disclaimer! The name of that YouTube channel might be offensive to some – I’m not related to that channel in anyway, I don’t condone bad language, I do not have any informed opinions about subjects like communism.