The SOLID principles1 are principles that help in designing the structure of a program that uses object-oriented programming. SOLID principles are a subset of principles of Object-oriented design by Robert C. Martin2. SOLID is an acronym for the following
- S - Single Responsibility Principle
- O - Open-Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
Why SOLID and other software design principles?
When software reaches its first release it is clean and elegant. The issue arises when there is any change in requirements and that change causes the software to not be clean and elegant which then results in redesign after a few releases down the road. Robert C. Martin calls this software rot and there are 4 symptoms of rotting. They are:
- Rigidity - The software becomes too hard to implement changes. Each change causes cascading changes.
- Fragility - When a part of the software is changed, another part breaks which has no relation.
- Immobility - When it's too hard to reuse part of software leading to rewriting it again.
- Viscosity(design) - When multiple ways are present to implement change, the design preserving change being the hard one to implement.
- Viscosity(environment) - The implementation of change is slow and inefficient.
SOLID principles are some of the principles that will help design better software from start.
Single Responsibility Principle3
The Single Responsibility Principle states that each software module should have one and only one reason to change. The 'only one reason to change' means that whenever there is a change request from a person/department/team on software when done should result in a new version of the software with a change in only one particular part of the software. No change request from another person/department/team should result in changes in the same part of the software.
It essentially means that software should be separated into different classes or modules depending upon from which stakeholder the request arises, therefore meaning that the class or module has a single responsibility.
Example:
class Employee():
def calculate_pay():
pass
def save():
pass
def report_hours():
pass
For this example, consider an organization with CFO, CTO, and COO at the top. Here the calculate_pay method is allocated to CFO, the report_hours method is allocated to the COO, save method which saves all data would be allocated to CTO inside the Employee class. Whenever there is any change by any C-level person, it would not affect other parts of the employee which are allocated to other C-level persons.
Open-Closed Principle
The Open-Closed Principle states that a module should be open for extension but closed for modification. It means that when we want to change the modules, we should be able to change them without changing the code of the modules. This is achieved through the use of abstraction and polymorphism.
Example:
class Modem(ABC):
@abstractmethod
def dial(self, phoneNumber):
pass
class AnalogModem(Modem):
def dial(self, phoneNumber):
print(f"Dialing {phoneNumber} using an analog modem.")
class DigitalModem(Modem):
def dial(self, phoneNumber):
print(f"Dialing {phoneNumber} using a digital modem.")
def logOn(modem: Modem, phoneNumber, username, password):
modem.dial(phoneNumber)
analog_modem = AnalogModem()
digital_modem = DigitalModem()
logOn(analog_modem, "12345", "user", "pass")
logOn(digital_modem, "67890", "user", "pass")
Here in the above example, the logOn function is open for modification by implementing analog and digital modems but, is closed from the logOn function. When a new type of modem is created in the future there is no need to modify logOn code but the logOn can be changed.
Liskov Substitution Principle
The Liskov Substitution Principle states that subclasses should be suitable for their base classes. When using inheritance the derived class must be able to substitute the base class in usage. If there is any significant change between the base class and the derived class then there shouldn't be inheritance.
Example:
class Animal:
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
print("bark")
class Cat(Animal):
def make_sound(self):
print("meow")
def make_animal_sound(animal: Animal):
animal.make_sound()
# Usage:
dog = Dog()
cat = Cat()
make_animal_sound(dog) # bark
make_animal_sound(cat) # meow
In the above example, we can pass both cat and dog instances with the Animal base class as an argument to the function and it still works as expected.
Interface Segregation Principle
The Interface Segregation Principle states that many client interfaces are better than one general-purpose interface. It means that if a class has several clients, rather than loading the class with all the methods that the clients need, having specific interfaces for each client and multiplying inherit them into class. This will help the client to inherit a class where they don't need to know the parts of the interface that they won't use.
Example:
class Printer(ABC):
@abstractmethod
def print(self, document):
pass
class Fax(ABC):
@abstractmethod
def fax(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan(self, document):
pass
class MultiFunctionDevice(Printer, Fax, Scanner):
def print(self, document):
print(f"Printer")
def fax(self, document):
print(f"Fax")
def scan(self, document):
print(f"Scanner")
class SimplePrinter(Printer):
def print(self, document):
print(f"modern printer")
Here the interfaces Printer, Fax, and Scanner are implemented in all or separately instead of one big fat PrintFaxScan class even though they are closely related in the real world. The person implementing only one interface does not know the others.
Dependency Inversion Principle
The Interface Segregation Principle states "Depend upon Abstractions. Do not depend upon concretions.". When a class depends upon other classes, it can result in repeated code. Every dependency in design should target an interface or abstract class. No dependency should target the concrete class. This is to ensure that a change in one part of the code won't result in a break in another of the code.
Example:
class Driver:
def __init__(self, name):
self.name = name
def drive(self, vehicle):
vehicle.drive()
class Vehicle:
def __init__(self, name):
self.name = name
def drive(self):
pass
class Car(Vehicle):
def drive(self):
print(f"Driving {self.name}")
class Boat(Vehicle):
def drive(self):
print(f"Sailing {self.name}")
In the above example, if the Driver is created by extending Car and Boat, it'll result in repeated code and will require cascading changes to implement change to Driver. Having the Driver target the class Vehicle will help it to automatically use the Car and Boat class.
Top comments (0)