DEV Community

Roshansahurk
Roshansahurk

Posted on

SOLID Concepts

  • Solid is a principle that is written in conjunction with code to improve the problems of bad code which are rigidity, fragility and non-usability.

  • Implementing the solid principle into the code helps the programmer to easily add the changes required onto the code with lesser effort.

Why Solid?

  • In the world of software development, programmers tend to tangle up while making changes in the code.
  • A good programmer writes readable, clear and concise code.
  • Even if the quality of the code is good, the programmer could find it hard to make some changes to it.

Why solid should be used?

  • To understand this let us look at the below example:

    • A bank company hires a programmer to make software which stores the credential id and could only be accessed by the customer.
    • The programmer makes the software and hands it over to the bank company.
    • After using it for a while, the bank company asks the programmer for some changes to it.
    • The programmer looks up to the code and do the changes required.
    • The highest probability is that the software will crash if the quality of the code is bad.
    • He has to make changes to the other modules as well.
    • The bigger picture in the development field is that the programmer goes to such lengths for making those small changes.
    • If we don't follow solid principles we end up with tight or strong coupling of the code with many other modules/applications.
    • Tight coupling causes time to implement any new requirement, feature or bug fixes and sometimes it creates some unknown issues.
  • Therefore, to resolve such problems an American software engineer and instructor Robert C. Martin came up with a solid principle.

Getting started with Solid

  • The acronyms for class design principles in the solid are:

    • SRP - Single Responsibility Principle
    • OCP - Open Close Principle
    • LSP - Liskov Substitution Principle
    • ISP - Interface Segregation Principle
    • DIP - Dependency Inversion Principle

Single Responsibility Principle

"A class should have only one reason to change"

  • Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

  • Each class and module focuses on a single task at a time.

  • If a class has more than one responsibility, it becomes coupled.

    class Vehicle:
        def __init__(self, name):
                self.name = name
    
        def get_name(self):
            pass
    
    class VehicleDB:
        def get_vehicle(self):
            pass
    
        def type(self, vehicle: Vehicle):
            pass
    
    
  • When designing our classes, we should aim to put related features together, so whenever they tend to change they change for the same reason.

Open Close Principle

"Modules should be open for extension, but closed for modification."

  • To understand more about the open-close principle let us take an example.
  • Let’s imagine you have a store, and you give a discount of 20% to your favourite customers using this class.
  • When you decide to offer double the 20% discount to VIP customers. You may modify the class like this:

    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price
    
        def give_discount(self):
                if self.customer == 'fav':
                    return self.price * 0.2
                if self.customer == 'vip':
                    return self.price * 0.4
    
  • This fails the OCP principle due to the changes made in the module.

  • To make it follow the OCP principle, we will add a new class that will extend the Discount.

  • In this new class, we would implement its new behaviour.

    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price
    
        def get_discount(self):
                return self.price * 0.2
    
    class VIPDiscount(Discount):
        def get_discount(self):
            return super().get_discount() * 2
    
  • You see, extension without modification.

Liskov Substitution Principle

"Derived class must be usable through the base class interface, without the need for the user to know the difference."

  • Liskov principle is a particular definition of a subtyping relation called behavioural subtyping.
  • The Liskov substitution principle can be implemented by the following:

    • No new exceptions can be thrown by the subtype.
    • Clients should not know which specific subtype they are calling.
    • New derived classes just extend without replacing the functionality of old classes.
    • This principle aims to ascertain that a sub-class can assume the place of its super-class without errors.
  • If the code finds itself checking the type of class then, it must have violated this principle.

    def animal_leg_count(animals: list):
        for animal in animals:
            if isinstance(animal, Lion):
                print(lion_leg_count(animal))
            elif isinstance(animal, Mouse):
                print(mouse_leg_count(animal))
            elif isinstance(animal, Pigeon):
                print(pigeon_leg_count(animal))
    
    animal_leg_count(animals)
    
  • The above example does not follow liskov's principle, the following changes should be made to make it follow liskov's principle.

        def animal_leg_count(animals: list):
            for animal in animals:
                print(animal.leg_count())
    
    animal_leg_count(animals)
    
  • The animal_leg_count function cares less about the type of Animal passed, it just calls the leg_count method.

  • All it knows is that the parameter must be of an Animal type, either the Animal class or its sub-class.

  • The Animal class now have to implement/define a leg_count method and its sub-classes have to implement the leg_count method.

    class Animal:
        def leg_count(self):
            pass
    
    class Lion(Animal):
        def leg_count(self):
            pass
    
  • animal_leg_count just calls the leg_count method of the Animal type because by contract a sub-class of the Animal class must implement the leg_count function.

Interface Segregation Principle

"No client should be forced to depend on the interface/methods it does not use."

  • Classes should not have to implement interfaces with methods they don't need.
  • One large interface should be split into many smaller and relevant interfaces so that the clients can know about the interfaces that are relevant to them.

  • Let’s look at the below IShape interface

    class Circle(IShape):
        def draw_square(self):
            pass
    
        def draw_rectangle(self):
            pass
    
        def draw_circle(self):
            pass
    
    class Square(IShape):
        def draw_square(self):
            pass
    
        def draw_rectangle(self):
            pass
    
        def draw_circle(self):
            pass
    
    class Rectangle(IShape):
        def draw_square(self):
            pass
    
        def draw_rectangle(self):
            pass
    
        def draw_circle(self):
            pass
    
  • It’s quite funny looking at the code above. Class Rectangle implements methods (draw_circle and draw_square) it has no use of, likewise Square implements draw_circle, and draw_rectangle, and class Circle (draw_square, draw_rectangle).

  • The classes must implement the new method or an error will be thrown.

  • We see that it is impossible to implement a shape that can draw a circle but not a rectangle or a square or a triangle.

  • We can just implement the methods to throw an error that shows the operation cannot be performed.

  • Clients (here Rectangle, Circle, and Square) should not be forced to depend on methods that they do not need or use.

  • To make our IShape interface conform to the ISP principle, we segregate the actions into different interfaces.

  • Classes (Circle, Rectangle, Square, Triangle, etc) can just inherit from the IShape interface and implement their draw behaviour.

    class IShape:
        def draw(self):
            raise NotImplementedError
    
    class Circle(IShape):
        def draw(self):
            pass
    
    class Square(IShape):
        def draw(self):
            pass
    
    class Rectangle(IShape):
        def draw(self):
            pass
    
  • We can then use the I-interfaces to create Shape specifics like Semi Circle, Right-Angled Triangles, Equilateral Triangle, Blunt-Edged Rectangle, etc.

Dependency Inversion Principle

"High-level modules should not depend on low-level modules."

  • Abstraction should not depend on details. Details should depend on abstraction.
  • The interaction between high-level and low-level modules should be thought of as an abstract interaction between them.

  • There comes a point in software development where our app will be largely composed of modules.

  • When this happens, we have to clear things up by using dependency injection.

  • High-level components depend on low-level components to function.

    class XMLHttpService(XMLHttpRequestService):
        pass
    
    class Http:
        def __init__(self, xml_http_service: XMLHttpService):
            self.xml_http_service = xml_http_service
    
        def get(self, url: str, options: dict):
            self.xml_http_service.request(url, 'GET')
    
        def post(self, url, options: dict):
            self.xml_http_service.request(url, 'POST')
    
  • Here, Http is the high-level component whereas HttpService is the low-level component.

  • This design violates DIP A: High-level modules should not depend on low-level level modules. It should depend upon its abstraction.

  • This Http class is forced to depend upon the XMLHttpService class.

    class XMLHttpService(Connection):
        xhr = XMLHttpRequest()
    
        def request(self, url: str, options:dict):
            self.xhr.open()
            self.xhr.send()
    
    class NodeHttpService(Connection):
        def request(self, url: str, options:dict):
            pass
    
    class MockHttpService(Connection):
        def request(self, url: str, options:dict):
            pass
    
  • We can create many Http Connection types and pass them to our Http class without any fuss about errors.

Conclusion

  • In the field of software development, solid principles are used to achieve flexibility in the code.
  • One can refactor their codes and revise it again and again very easily.
  • Implementing the five key principles in the code maintains the reusability of code as well.

References

Top comments (0)