DEV Community

Tushar Singh
Tushar Singh

Posted on • Updated on

SOLID PRINCIPLES TECHNICAL PAPER

Tushar Singh (susst94@gmail.com)

Introduction:

Software development as we know it today has become resource centric from an efficiency standpoint. The metrics for efficiency which all the different designs have been aiming for a long time now are all boiling down to one term which is Resource Allocation. This one term can be applied to all the components relevant to designing any small-scale or large-scale project.

The most common components that require efficient Resource Allocation in today's software are as follows:

  • Expansive Architecture (Upgradation)
  • Limited Dependency between features
  • Efficient testing and bug tracking
  • Efficient Maintenance

These components in conjunction with efficient Resource Allocation become the make-or-break prerequisites for any software to either survive and thrive in today's fast-moving market or succumb to the pressure and not even see the light of day on any system and ultimately get shut down.

Developers from their very early coding days are exposed to the idea of Object Oriented Programming (OOP) and its features which become a stepping stone for building robust and efficient software. There is one other set of rules and principles that in conjunction with OOP become the very foundation for the inception of any software. Those principles are known as the SOLID Principles of design architecture. The purpose of this paper is to get the reader a better perspective of these principles with the help of some code snippets to help form a relational map.

SINGLE RESPONSIBILITY PRINCIPLE (S):

The long and short of this principle is that "A class should have only one reason to change". To further expand on this idea, what it means is that each class and its modules serve a singular purpose for the project to be undertaken and nothing else. This ensures having small and readable code for our project. Consider the following snippet:

    class Library():

        def __init__ (self, Lib_name, pincode, regno):
            self.Lib_name = Lib_name
            self.pincode = pincode
            self.regno = regno

        def Lib_details(self):
            print('Library Name: ',self.Lib_name)
            print('Pincode: ',self.pincode)
Enter fullscreen mode Exit fullscreen mode

We will be making use of different projects to expand on all the design principles. In this snippet of our library project, we are defining our library class to contain primitive attributes such as the name of the library, its associated pincode and registration number, and a method to print out the first two details. This definition makes our purpose for the class clear and concise and that is to hold the address and registration info for any library and to only print out the name and its associated pincode.

OPEN CLOSED PRINCIPLE (O):

This principle states that entities such as classes, and modules should be open only for extension and not for modification. This means that any new functionalities or features should be added with the help of either new classes, attributes, or methods and one should not try to modify the current existing classes, attributes, or methods. This principle is at times considered to be the most important principle for software design.
Consider the following snippet:

    class Library():

        def __init__ (self, Lib_name, pincode, regno):
            self.Lib_name = Lib_name
            self.pincode = pincode
            self.regno = regno

        def Lib_details(self):
            print('Library Name: ',self.Lib_name)
            print('Pincode: ',self.pincode)


    class Books_data():

        def __init__(self):
            self.books = []

        def add_books(self,book,author,year):
            current_book = [book,author,year]
            self.books.append(current_book)

Enter fullscreen mode Exit fullscreen mode

Say we want to store the details of all the books stored in a specific library. Instead of changing our initialization parameters in the Library class, we can simply create another class that will take care of all details for all the books in the library. Here is where Books_data comes into the picture.

LISKOV SUBSTITUTION PRINCIPLE (L):

This principle states that any derived types or references to a class must be substituted for references to the base class. This can also be interpreted as an extension of the Open Closed Principle(O). A very important guideline to follow which enables Liskov Substitution in our design architecture is that our user client should always behave the same regardless of the derived type instance and any new classes should extend without clashing with any functions of already present classes.
Consider the following snippet:

   class Mclaren():

        def __init__(self, model_name, year):
            self.model = model_name
            self.year = year

        def model(self):
            print('Presenting Mclaren {self.model}')
            print('Year of production {self.year}')

    class Anniversary(Mclaren):

        def __init__(self,rgb_freq):
            Mclaren.__init__(self)
            self.freq = rgb_freq

        def lights(self):
            print('Using rgb lights at {self.freq} Hz')
            ""
            RGB lights code
            "" 
Enter fullscreen mode Exit fullscreen mode

Say we have a class called Mclaren which creates instances for different Mclaren Models produced. Mclaren now decides for pushing a special Anniversary model with extra rgb lights into production and wants to maintain the necessary data. An efficient way would be to create a new Anniversary class with Mclaren as its base class and introduce a function in the class called lights which will take care of generating the new rgb lights as part of the anniversary model.

INTERFACE SEGREGATION PRINCIPLE (I):

This principle states that the user should never be forced to depend on features of a project with which he doesn't have any reason to interact in the first place. Another interpretation of this is that a project's features should be broken down into smaller interfaces. Hence the user will interact with interfaces relevant to his needs. Consider the following snippet:

    class Mclaren():

        def __init__(self, model_name, year, mileage, capacity):
            self.model = model_name
            self.year = year
            self.mileage = mileage
            self.capacity = capacity

        def model(self):
            print('Presenting Mclaren {self.model}')
            print('Year of production {self.year}')

        def distance(self):
            dist = self.mileage*self.capacity
            print('Max distance travel potential(miles)',dist)

        def maintenance(self):
            engine_life = 200000/(self.mileage*self.capacity)
            print('Potential engine life in years: ',engine_life)
Enter fullscreen mode Exit fullscreen mode

Consider the code snippet provided. We will again expand on our Mclaren class and introduce a few more functions in the class definition which all achieve a different purpose and hold no relation to each other. The responsibility for accessing these features will fall on the user. This distributed availability of features is the premise for the Interface segregation principle.

DEPENDENCY INVERSION PRINCIPLE (D):

This principle states that high-level modules should never depend upon lower-level modules, rather both should depend upon abstractions. It also states that the details should depend upon the abstractions and not the other way around. This may seem straightforward to understand at first, but to gain clarity we will make use of the Abstract Class module to introduce abstraction. Consider the code snippet:

from abc import ABC

class game(ABC):

    def elo_calc(self):
        pass


class ELO(game):

    def elo_calc(self, wins, loss):
        total = wins+loss
        ratio = wins/loss
        if total > 1000:
            if ratio >= 1:
                return(1500)
            elif ratio != 0:
                return(1000 - (ratio)*50)
            else:
                return(500)
        else:
            print('Need 1000 minimum matches to calculate ELO')
            return 0


class chess():

    def __init__(self, player, country, wins, loss,c: game):
        self.point = c
        self.player = player
        self.country = country
        self.wins = wins
        self.loss = loss

    def get_elo(self):
        self.rating = self.point.elo_calc(self.wins, self.loss)
        return self.rating


judge = ELO()
p1 = chess('Test','India',580,470,judge)
s = p1.get_elo()
print(s)   
Enter fullscreen mode Exit fullscreen mode

Say we want to calculate elo ratings for chess players based on the win-loss ratio and the number of games they've played. The formula used for calculating is very rudimentary and is used with the purpose of just demonstrating Dependency Inversion. In our snippet, we make use of the game abstract class to become the interface that helps in establishing a bridge between the higher level module ELO class and the lower level module chess class. The ELO class inherits the abstract class game, while the chess class requires an object c of the abstract class game hence completing the bridge. In the main function, we create a judge instance of ELO (in turn, game abstract class) and pass it as an argument for chess player p1. Thus we have inverted our dependency.

Conclusion:

I hope this paper served its purpose in getting you the reader acquainted with the SOLID principles and how they function. You cannot gain complete clarity of these principles without attempting to implement these principles in your projects and identifying your mistakes during execution and then retroactively improving upon those mistakes.

References:

SOLID SUMMARY

Top comments (0)