The principles promote good programming habits and maintainable code. SOLID principles enhance Agile code development by focusing on code maintenance and extensibility over the long term. Accounting for and optimizing code dependencies helps create a more straightforward and organized software development lifecycle.

What Are SOLID Principles?

SOLID represents a set of principles for designing classes. Robert C. Martin (Uncle Bob) introduced most design principles and coined the acronym.

SOLID stands for:

  • Single-Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
  • SOLID principles represent a collection of best practices for software design. Each idea represents a design framework, leading to better programming habits, improved code design, and fewer errors.
  • SOLID: 5 Principles Explained

The best way to understand how SOLID principles work is through examples. Each principle is complementary and applies to specific use cases. The order in which the principles are applied is unimportant, and not all principles are applicable in every situation.

Each section below provides an overview of each SOLID principle in the Python programming language. SOLID principles can be applied to all object-oriented languages, including PHP, Java and C#. Generalizing the rules makes them applicable to modern programming approaches, such as microservices.

Single-Responsibility Principle (SRP)

5 Solid principles explained and listed.

The single-responsibility principle (SRP) states:

“There should never be more than one reason for a class to change.”

When changing a class, we should only change a single functionality, which implies every object should have only one job. As an example, consider the following class.# Class with multiple responsibilities
Class Animal
# Property Constructor
def __init__(self, name):
Self-name = name

# Property Representation
def __repr__(self):
return f’Animal(name=”{self.name}”)’

Database Management
def save(animal):
{ print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}

If __name__ is equal to ‘__main__:
# Property Instantiation
a = animal(‘Cat”).
# Save property to a database
Animal.save(a)
When making any changes to save method, the change happens in the

Animal

class. When making property changes, the modifications also occur in the Animal class.The class has two reasons to change and violates the single-responsibility principle. Even though the code works as expected, not respecting the design principle makes the code harder to manage in the long run.To implement the single-responsibility principle, notice the example class has two distinct jobs:Property management (the constructor and get_name).

Database management (save).Therefore, the best way to address the issue is to separate the database management method into a new class. As an example:

  • # a class responsible for managing property
    Class Animal
    def __init__(self, name):
    Self-name = name
    def __repr__(self):
    return f’Animal(name=”{self.name}”)’# A class responsible for database management
    Class AnimalDB
    Save yourself or your animal:
    { print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}

    If __name__ is equal to ‘__main__:
    # Property Instantiation
    a = animal(‘Cat”).
    # Database Instantiation
    AnimalDB

    # Save property to a database
    db.save(a)
    Changing the AnimalDB

Single responsibility principle claims that there should never be more than one reason for a class to change.

class does not affect the

Animal

class with the single-responsibility principle applied. The code is intuitive and easy to modify.Open-Closed Principle (OCP)The open-closed principle (OCP) states: "Software entities should be open for extension but closed for modification."Adding functionalities and use-cases to the system should not require modifying existing entities. The wording seems contradictory – adding new functionalities requires changing existing code.

The idea is simple to understand through the following example:

class Animal:
def __init__(self, name):
Self-name = name
def __repr__(self):
return f’Animal(name=”{self.name}”)’

Class Storage:
Save_to_db (self, animal)
{ print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}
The

Storage

class saves the information from an

Animal

instance to a database. Adding new functionalities, such as saving to a CSV file, requires adding code to the Storage class:class Animal:
def __init__(self, name):
Self-name = name
def __repr__(self):
return f'Animal(name="{self.name}")'

Class Storage:
Save_to_db (self, animal)
{ print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}
def save_to_csv(self,animal):
{ printf(f’Saved {animal} to the CSV file’)|Printf(f”Saved animal to CSV file:”)}
The save_to_csv method modifies an existing

Storage

class to add the functionality. This approach violates the open-closed principle by changing an existing element when a new functionality appears.The code requires removing the general-purpose Storage class and creating individual classes for storing in specific file formats.The following code demonstrates the application of the open-closed principle:

class DB

:
Save yourself or your animal:
{ print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}

Class CSV

:
Save yourself or your animal:
{ print(f’Saved {animal} to a CSV file’)|Print(f’Saved animal to CSV’)}
The code is compliant with the principle of open-closed. The code looks now like this:class:Animal
def __init__(self, name):
Self-name = name
def __repr__(self):
{ return f'"{self.name}"'|Return f""self.name'}

Class DB

:
Save yourself or your animal:
{ print(f’Saved {animal} to the database’)|Print(f’Saved animal to database’)}

Class CSV

:
Save yourself or your animal:
{ print(f’Saved {animal} to a CSV file’)|Print(f’Saved animal to CSV’)}

If __name__ is equal to ‘__main__:
a = animal(‘Cat”)
DB = db

CSV = CSV

db.save(a)
csv.save(a)
Extending with additional functionalities (such as saving to an XML file) does not modify existing classes.

Open close principle, the second SOLID principle for OOP.

Liskov Substitution Principle (LSP)

The Liskov substitution principle (LSP) states: 

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

The principle states that a parent class can substitute a child class without any noticeable changes in functionality.

Check out the file writing example below:

# Parent class
class FileHandling

:
def write_db(self):
return f’Handling DB’
def write_csv(self):
return f’Handling CSV’

Number of Child Classes
class WriteDB(FileHandling):
def write_db(self):
Return f’Writing a DB
def write_csv(self):
Return f”Error – Can’t write CSV file, wrong filetype.”

class WriteCSV(FileHandling):
def write_csv(self):
Return f’Writing into a CSV File’
def write_db(self):
Return f”Error : Can’t write DB, wrong File Type.”

If __name__ is equal to “__main__”, then:
# Instantiation of parent class and functions calls
db = FileHandling

csv = FileHandling

print(db.write_db

)
print(db.write_csv

)

# Child classes instantiations, function calls
db = WriteDB

csv = WriteCSV

print(db.write_db

)
print(db.write_csv

)
print(csv.write_db

)
print(csv.write_csv

)

The parent class (FileHandling

) consists of two methods for writing to a database and a CSV file. The class handles both functions and returns a message.

The two child classes (

WriteDB

and WriteCSV) inherit properties from the parent class (

FileHandling). Both children will throw an error if they try to use the wrong write function. This violates the Liskov Substitution Principle since the overriding methods do not correspond to the parent functions.
class FileHandling

:
def write(self):
return f’Handling file’

Number of Child Classes
class WriteDB(FileHandling):
def write(self):
Return f’Writing a DB

class WriteCSV(FileHandling):
def write(self):
Return f’Writing into a CSV File’

If __name__ is equal to “__main__”, then:
# Instantiation of parent class and functions calls
db = FileHandling

csv = FileHandling

print(db.write

)
print(csv.write

)

# Child classes instantiations, function calls
db = WriteDB

WriteCSV = csv

print(db.write

)
print(csv.write

)
The child classes correctly correspond to the parent function.Interface Segregation Principle (ISP)The interface segregation principle (ISP) states: "Many client-specific interfaces are better than one general-purpose interface." In other words, more extensive interaction interfaces are split into smaller ones. The principle ensures classes only use the methods they need, reducing overall redundancy.

Liskov substitution principle - example of how it works

The following example demonstrates a general-purpose interface:

class Animal


:
Def walk (self)
        pass
    def swim(self):
        pass

Class Cat (Animal)
Def walk (self)
        print("Struts")
    def fly(self):
        raise Exception("Cats don't swim")

Class Duck (Animal)
Def walk (self)
        print("Waddles")
    def swim(self):
        print("Floats")

The child classes inherit from the parent

Animal

class, which contains walk

and

fly

 methods. Although both functions are acceptable for certain animals, some animals have redundant functionalities.

To handle the situation, split the interface into smaller sections. For example:class Walk

:
Def walk (self)
pass

Class Swim (Walk):
def swim(self):
pass

Class Cat (Walk)
Def walk (self)
print(“Struts”)

Class Duck (Swim).
Def walk (self)
print(“Waddles”)
def swim(self):
print(“Floats”)
The Fly class inherits from the Walk, providing additional functionality to appropriate child classes. The example satisfies the interface segregation principle.

Interface segregation principle claims that many client-specific interfaces are better than one general-purpose interface.

Adding another animal, such as a fish, requires atomizing the interface further since fish can’t walk.

Dependency Inversion Principle (DIP)

The dependency inversion principle states: "Depend upon abstractions, not concretions."The principle aims to reduce connections between classes by adding an abstraction layer. The code becomes more robust when dependencies are moved to abstractions.
def latin(self, name):
print(f'{name} = “Felis catus”‘)
Return “Felis Catus”

class Converter:
def start(self):
converter = LatinConverter

converter.latin(‘Cat’)

If __name__ is equal to ‘__main__:
converter = Converter

converter.start

The example has two classes:LatinConverter
uses an imaginary API to fetch the Latin name for an animal (hardcoded ”

Felis catus

” for simplicity).Converter

is a high-level module that uses an instance of

LatinConverter

 and its function to convert the provided name. The 

Converter

  • heavily depends on the LatinConverter class, which depends on the API. This approach violates the principle.The dependency inversion principle requires adding an abstraction interface layer between the two classes.
  • An example solution looks like the following:from abc import ABCclass NameConverter(ABC):
    def convert(self,name):
    pass

    class LatinConverter(NameConverter):
    def convert(self, name):
    Print(“Converting using Latin api’)
    print(f'{name} = “Felis catus”‘)
    Return “Felis Catus”

    class Converter:
    def __init__(self, converter: NameConverter):
    self.converter = converter
    def start(self):
    self.converter.convert(‘Cat’)

    If __name__ is equal to ‘__main__:
    latin = LatinConverter

    converter = Converter(latin)
    converter.start

    The Converter class now depends on the NameConverter interface instead of on the LatinConverter

directly. Future updates allow defining name conversions using a different language and API through the

Dependency inversion principle reduces connections between classes.

NameConverter

 interface.

Why is There a Need for SOLID Principles?SOLID principles help fight against design pattern problems. SOLID principles are designed to make code easier to manage, maintain and change. Since the rules are better suited for large projects, applying the SOLID principles increases the overall development lifecycle speed and efficiency.Are SOLID Principles Still Relevant?Although SOLID principles are over 20 years old, they still provide a good foundation for software architecture design. SOLID provides sound design principles applicable to modern programs and environments, not just object-oriented programming.SOLID principles apply in situations where code is written and modified by people, organized into modules, and contains internal or external elements.Conclusion SOLID principles help provide a good framework and guide for software architecture design. These examples show how even dynamically-typed languages like Python can benefit from using SOLID principles in code design.

About The Author

By omurix

XIII. Unidentified Society

Leave a Reply

Your email address will not be published. Required fields are marked *

%d