DEV Community

Dev Kadiem
Dev Kadiem

Posted on

Upgrade Your Development Skills: Introduction to SOLID Principles

A clear distinction between junior and senior developers is how well senior developers future-proof their programs; one way of achieving this is to use a well-known code or rules, like SOLID principles, which Robert C. Martin developed in his essay, "Design Principles and Design Patterns."

SOLID is an acronym for single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion principle, and it's important to state that SOLID is mainly for OOP.

It's important to recall that this is an introduction; thus, I will use easy scenarios, and the examples shown are for learning purposes only and not for commercial use. Lastly, I'll use sudo code to make it reachable to most people.


Single Responsibility Principle

The single responsibility principle states, "A class should have only one reason to change." According to the previous state, each class is responsible for only one part during the software lifespan.

To kick off the article, I'll create a simple User class:

class User {
    function profile()
    function login()
    function register()
    function sendVerificationEmail()
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the class is responsible for more than one part, and my way of applying the principles is the following:

class User {
    function profile()
}

class Authentication {
    function login()
}

class Signup {
    function register()
    function sendVerificationEmail()
}
Enter fullscreen mode Exit fullscreen mode

We now have three classes, each responsible for only one part.


Open Closed Principle

The open-closed principle states, "A class should be open to extension but closed to modifying." According to the previous state, each class should be written in a way that allows the addition of functionality but does not allow alteration of its core.

To demonstrate a demo of this principle, I'll create another User class but this time, it will have a variable called type:

class User {
    var type

    constructor(var type) {
        this.type = type
    }

    function doSomething() {
      if(type == "normal") {
        // ...
      } else if(type == "professional") {
        // ...
      }
    }

    // ...
}

User normalUser = new User("normal")
User professionalUser = new User("professional")
Enter fullscreen mode Exit fullscreen mode

The class is open for modification because type has a direct influence on the class's functionality; this is an example of why we need the open-closed principle. Let's see an example of how to apply the principle here:

abstract class User {
    abstract function doSomething()
}

class NormalUser Inherits User {
    function doSomething() {
      // ...
    }
}

class ProfessionalUser Inherits User {
    function doSomething() {
      // ...
    }
}

User normalUser = new NormalUser()
User professionalUser = new ProfessionalUser()
Enter fullscreen mode Exit fullscreen mode

By implementing the principle, we can write a more precise functionality.


Liskov Substitution Principle

The Liskov substitution principle states, "if S is a subtype of T, then objects of type T may be replaced with objects of type S" Liskov substitution is better explained with an example:

abstract class Employee {
    abstract function getBonus()
}

class HardWorkingEmployee Inherits Employee {
    function getBonus() {
        // ...
    }
}

class LackingEmployee Inherits Employee {
    function getBonus() {
        Error
    }
}
Enter fullscreen mode Exit fullscreen mode

And now, to instantiate an instance of HardWorkingEmployee class:

Employee employee = new HardWorkingEmloyee()
employee.getBonus()
Enter fullscreen mode Exit fullscreen mode

The example above would work perfectly, now let's change HardWorkingEmployee with LackingEmployee:

employee = new LackingEmployee()
employee.getBonus()
Enter fullscreen mode Exit fullscreen mode

The example above would not work because, LackingEmployee does not implement bonus function, even though they both are of type Employee, and that is a violation of the Liskov Substitution; an easy fix for this situation is the following:

abstract class Employee {
    // ...
}

interface EligableForBonus {
    function getBonus()
}

class HardWorkingEmployee Inherits Employee, EligableForBonus {
    function getBonus() {
        // ...
    }
}

class LackingEmployee Inherits Employee {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Now LackingEmployee does not inherit any non-implemented functionality.


Interface Segregation Principle

The interface segregation principle states, "Clients should not be forced to depend upon interfaces that they do not use." It means that when a class inherits a parent, it should implement all of its functionality.

interface UserFunctions {
    function function1()
    function function2()
    function function3()
    function function4()
}

class User inherits UserFunctions {
    function function1() {
      // ..
    }

    function function2() {
      // ..
    }

    function function3() {
      // ..
    }

    function function4() {
      Error
    }
}
Enter fullscreen mode Exit fullscreen mode

The User class does not implement function4 and thus, it violates the interface segregation principle; one way of fixing it is the following:

interface CoreUserFunctions {
    function function1()
    function function2()
    function function3()
}

interface ExtraUserFunctions {
    function function4()
}

class BasicUser inherits UserFunctions {
    function function1() {
      // ..
    }

    function function2() {
      // ..
    }

    function function3() {
      // ..
    }
}

class UpgradedUser inherits UserFunctions, ExtraUserFunctions {
    function function1() {
      // ..
    }

    function function2() {
      // ..
    }

    function function3() {
      // ..
    }

    function function4() {
      // ..
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have BasicUser and UpgradedUser and both fully implement what they inherit.


Dependency Inversion Principle

The Dependency Inversion Principle states, "The interaction between high level and low level modules should be thought of as an abstract interaction between them." It means that if we have interactions between high-level and low-level modules or classes, we should add a middle layer because they both operate at different levels of complexity.

 

class DataAccesslayer {
    function saveToDatabase(data) {
        // ...
    }
}

class User {
    DataAccesslayer dataAccesslayer
    dataAccesslayer.saveToDatabase(data)
}
Enter fullscreen mode Exit fullscreen mode

The example above does not implement the Dependency inversion principle, thus any modification to DataAccessLayer might means we need to modify User class also, a better implementation would be like this:

interface DataAccesslayerInterface {
    function saveToDatabase(data)
}

class DataAccesslayer inherits DataAccesslayerInterface {
    function saveToDatabase(data) {
        // ...
    }
}

class User {
    DataAccesslayerInterface dataAccesslayer
    dataAccesslayer.saveToDatabase(data)
}
Enter fullscreen mode Exit fullscreen mode

Now we have an abstraction layer, which means that modification on DataAccesslayer will no longer need modification on User class because the abstraction layer does not change; thus function signature does not change

Top comments (0)