DEV Community

Siddharth Shanker
Siddharth Shanker

Posted on • Edited on

SOLID Principles

The acronym originated by Robert C. Martin, and it stands for five principles of Object-Oriented Programming. These are a set of rules for certain design patterns and software architecture.

The SOLID principles are:

  • The Single-Responsibility Principle (SRP)

  • The Open-Closed Principle (OCP)

  • The Liskov Substitution Principle (LSP)

  • The Interface Segregation Principle (ISP)

  • The Dependency inversion Principle (DIP)

The Single-responsibility principle (SRP)

“A class should have one, and only one, reason to change”

  • Every component of our code should have only one responsibility.

class User:

    def __init__(self, name, email, password):

        self.name = name

        self.email = email

        self.password = password



    def login(self):

        # Some code to authenticate user

        return True



    def change_password(self, new_password):

        # Some code to change user's password

        return True

Enter fullscreen mode Exit fullscreen mode
  • Here, User class is responsible for authentication from login and password management through change_password method.

  • This violates SRP because the class is responsible for two distinct tasks.


class UserAuthenticator:

    def __init__(self, user):

        self.user = user



    def login(self):

        # Some code to authenticate user

        return True





class PasswordManager:

    def __init__(self, user):

        self.user = user



    def change_password(self, new_password):

        # Some code to change user's password

        return True

Enter fullscreen mode Exit fullscreen mode
  • Now we have separated the responsibilities, each class is now focused on a single task.

  • This makes code easier to understand and maintain.

The Open–closed principle (OCP)

Software entities … should be open for extension but closed for modification”

  • If we need to add a new functionality or method, we don't need to modify the class, rather we should be able to extend it.

class checkShape:

    def handle_shape(self, shape):

        if request.type == 'Circle':

            # Handle request type Circle

        elif request.type == 'Rectangle':

            # Handle request type Rectangle

        elif request.type == 'Parallelogram':

            # Handle request type Parallelogram

        else:

            # Handle other request types

Enter fullscreen mode Exit fullscreen mode
  • Now, suppose we want to check for Triangle, we would need to modify if else ladder by adding another elif statement for Triangle.

  • This violates OCP.

  • Better solution would be to use polymorphism and create separate classes for each request type that inherit from a common abstract class or interface.


from abc import ABC, abstractmethod



class Shape(ABC):

    @abstractmethod

    def process_shape(self):

        pass



class RequestCircle(Shape):

    def process_shape(self):

        # Handle request type Circle



class RequestRectangle(Shape):

    def process_shape(self):

        # Handle request type Rectangle



class RequestParallelogram(Shape):

    def process_shape(self):

        # Handle request type Parallelogram



class checkShape:

    def __init__(self, request):

        self.request = request



    def handle_shape(self):

        self.request.process_request()

Enter fullscreen mode Exit fullscreen mode

The Liskov substitution principle (LSP)

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

  • Derived classes must be substitutable for their base classes”.

  • If a subclass redefines a function also present in the parent class, a client-user should not be noticing any difference in behaviour, and it is a substitute for the base class.

If a subclass violates the structure of Parent class, this would result in violation of LSP.


class Rectangle:

    def __init__(self, width, height):

        self.width = width

        self.height = height



    def area(self):

        return self.width * self.height





class Square(Rectangle):

    def __init__(self, side):

        self.width = self.height = side

Enter fullscreen mode Exit fullscreen mode
  • The Square class sets both the width and height to the same value, which means that the area method will return an incorrect value for rectangles.

  • We can change the design to ensure that Square and Rectangle have a common interface but different implementations.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2



Enter fullscreen mode Exit fullscreen mode
  • Rectangle and Square can both inherit from the common abstract Shape class and implement the area method in a way that is consistent with their behaviour.

The Interface Segregation Principle (ISP)

Many client-specific interfaces are better than one general-purpose interface

  • A class should only have the interface needed (SRP) and avoid methods that won’t work or that have no reason to be part of that class.

  • This problem arises, primarily, when, a subclass inherits methods from a base class that it does not need.


class Vehicle:

    def flight_speed(self):

        pass

    def sail_speed(self):

        pass

    def speed(self):
        pass

    def acceleration(self):
        pass

    def brake(self):

        pass



class Car(Vehicle):

    # class would inherit all methods which might not be necessary

Enter fullscreen mode Exit fullscreen mode
  • Vehicle interface defines 3 methods and forces Car class to use all 3 methods.

  • Car class might not require other methods defined in the Vehicle, which could be used by other classes like Ship and Aeroplanes.

  • Splitting the Vehicle interface into smaller interfaces that group together related methods. Then, the Car class can inherit from only the interfaces that it needs to implement.


class LandVehicle:

    def speed(self):

        pass

class AirVehicle:

    def flightspeed(self):

        pass 

class WaterVehicle:

    def sailspeed(self):
        pass

class Acceleration(self):

    def acceleration(self):
        pass

    def brake(self):

        pass 

class Car(LandVehicle, Acceleration):

    def speed(self):

        # implementation of speed method

        pass

    def accelerate(self):

        # implementation of accelerate method

        pass

    def brake(self):

        # implementation of brake method

        pass

class Aeroplane(AirVehicle, Acceleration):
        pass
        # define the methods from both classes

Enter fullscreen mode Exit fullscreen mode
  • This way class Car would inherit only those interfaces it needs to implement.

# The Dependency Inversion Principle (DIP)

Abstractions should not depend on details. Details should depend on abstraction. High-level modules should not depend on low-level modules. Both should depend on abstractions”


class Engine:

    def start(self):

        print("Engine started")



class Vehicle:

    def __init__(self):

        self.engine = Engine()



    def start(self):

        self.engine.start()

Enter fullscreen mode Exit fullscreen mode
  • The Vehicle class depends directly on the low-level Engine class to start the vehicle's engine. This violates the Dependency Inversion Principle because the Vehicle class depends directly on a low-level module.

  • Dependency injection can be used to invert the dependencies and decouple the high-level Vehicle class from the low-level Engine class.


class Engine:

    def start(self):

        print("Engine started")



class Vehicle:

    def __init__(self, engine: Engine):
        self.engine = engine

    def start(self):

        self.engine.start()

Enter fullscreen mode Exit fullscreen mode
  • We inject the engine object into the Vehicle constructor as a dependency.

  • This way, the Vehicle class is decoupled from the specific implementation of the engine and can work with any implementation that conforms to a common interface. This meets the Dependency Inversion Principle.


class ElectricEngine(Engine):
    def start(self):
        # implementation of start method for electric engine
        print("Electric Engine")
class FuelEngine(Engine):
    def start(self):
        print("Fuel Engine")

electric_engine = ElectricEngine()
fuel_engine = FuelEngine()
vehicle = Vehicle(electric_engine)
vehicle = Vehicle(fuel_engine)
vehicle.start()

Enter fullscreen mode Exit fullscreen mode
  • We can now create an ElectricEngine or GasEngine class that conforms to this interface and pass it to the Vehicle constructor.

  • The Vehicle class is decoupled from the specific implementation of the engine and can work with any implementation that conforms to the engine interface.

References

Top comments (0)