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)
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
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.
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.
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.
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
LatinConverterclass, 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):
passclass 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 = LatinConverterconverter = Converter(latin)
converter.startThe
Converterclass now depends on the
NameConverterinterface instead of on the
LatinConverter
directly. Future updates allow defining name conversions using a different language and API through the
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.