Compiling the learnings at one place, so that it can be very easily revised in future when needed.
The need for SOLID principles in object-oriented design arises from the desire to create software that is:
- Maintainable: Easy to update and fix without breaking existing functionality.
- Scalable: Can grow and adapt to new requirements with minimal changes.
- Reusable: Components can be reused in different contexts.
- Testable: Easier to write unit tests for small, focused classes.
- Flexible: Supports extension and modification without major rewrites.
By following SOLID principles, developers reduce bugs, avoid code smells, and make their codebase easier to understand and evolve over time.
Single Responsiblity Principle
Open Closed Principle
Here’s a simple example of the Open/Closed Principle (OCP) in Python. Suppose you want to calculate the area of different shapes.
- Open for extension: Add new shapes by creating new classes.
- Closed for modification: No need to change existing code to support new shapes.
Bad Example (Not OCP): You modify the area method every time you add a new shape.
class AreaCalculator:
def area(self, shape):
if shape['type'] == 'circle':
return 3.14 * shape['radius'] ** 2
elif shape['type'] == 'rectangle':
return shape['width'] * shape['height']
# Need to modify this method for every new shape!Good Example (OCP): You extend functionality by adding new classes, not by modifying existing ones.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# AreaCalculator does not need to change for new shapes
def print_area(shape: Shape):
print(shape.area())
circle = Circle(5)
rectangle = Rectangle(4, 6)
print_area(circle)
print_area(rectangle)Liskov Substitution Principle (LSP):
Subclasses should be substitutable for their base classes without altering program correctness.
Child class implementation must be substitutable by the its parent class.
Bad Example (Violates LSP):
Orchid
child class is not able to implement fly method and it has unexpected
behaviour which vioulates the Liskov Substitution Principle. Ostrich is a Bird, but calling fly() on it breaks the expected behavior.
class Bird:
def fly(self):
print("Flying")
class Ostrich(Bird):
def fly(self):
raise Exception("Ostriches can't fly!")
def make_bird_fly(bird: Bird):
bird.fly()
ostrich = Ostrich()
make_bird_fly(ostrich) # Raises Exception: breaks expectationsGood Example (Follows LSP):
Parent classes Sparrow & Orchid are perfectly able to implement the move method defined in its parent class which makes it abide to Liskov Substitution Principle.
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class Sparrow(Bird):
def move(self):
print("Flying")
class Ostrich(Bird):
def move(self):
print("Running")
def make_bird_move(bird: Bird):
bird.move()
sparrow = Sparrow()
ostrich = Ostrich()
make_bird_move(sparrow) # Output: Flying
make_bird_move(ostrich) # Output: RunningHere’s another example of the Liskov Substitution Principle (LSP)
Bad Example (Violates LSP):
FreePaymentProcessor breaks the expected behavior of pay().
class PaymentProcessor:
def pay(self, amount):
print(f"Paid {amount}")
class FreePaymentProcessor(PaymentProcessor):
def pay(self, amount):
raise Exception("Cannot process payment for free items!")
def process_payment(processor: PaymentProcessor, amount):
processor.pay(amount)
processor1 = PaymentProcessor()
processor2 = FreePaymentProcessor()
process_payment(processor1, 100) # Output: Paid 100
process_payment(processor2, 0) # Raises Exception: breaks expectations!Good Example (Follows LSP):
All subclasses provide a valid implementation of pay(), so any subclass can be substituted for PaymentProcessor without breaking client code.
class PaymentProcessor:
def pay(self, amount):
print(f"Paid {amount}")
class FreePaymentProcessor(PaymentProcessor):
def pay(self, amount):
print("No payment needed for free items.")
def process_payment(processor: PaymentProcessor, amount):
processor.pay(amount)
processor1 = PaymentProcessor()
processor2 = FreePaymentProcessor()
process_payment(processor1, 100) # Output: Paid 100
process_payment(processor2, 0) # Output: No payment needed for free items.Interface Segregation Principle
Its similar to the first principle is SOLID i.e. Single Responsibility Principle. The Single Responsibility Principle which is to be applied to interfaces instead of classes.
Goal is to avoid the fat interface, instead come up with many small interfaces based on the semantic logic. Each single interface results in single reponsibility. And extend only required bahaviours/intefaces to child classes.
Interface Segregation Principle is about; not to force any class to implement an interface which is irrelevant to it. Classes should not be forced to depend on interfaces they do not use. Prefer small, specific interfaces over large, general ones. Robot class in good examples below only extends workable interface, and Eatable interface is not applicable to robot class.
Bad Example (Violates ISP): obot is forced to implement eat(), which it doesn't need.
class Worker:
def work(self):
pass
def eat(self):
pass
class Robot(Worker):
def work(self):
print("Robot working")
def eat(self):
raise Exception("Robots don't eat!") # Forced to implement unused methodGood Example (Follows ISP): Interfaces are split so classes only implement what they need.
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Human(Workable, Eatable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
class Robot(Workable):
def work(self):
print("Robot working")
human = Human()
robot = Robot()
human.work() # Output: Human working
human.eat() # Output: Human eating
robot.work() # Output: Robot workingDependency Inversion Principle
High-level modules should not depend on low-level modules; both should depend on abstractions.
I know, this statement is not straight forward to interprete and understand. Have al look at below example.
Bad Example 1 (Violates DIP): DataManager is tightly coupled to MySQLDatabase. Changing the MySQLDatabase class requires modifying DataManager
class MySQLDatabase:
def connect(self):
print("Connecting to MySQL")
class DataManager:
def __init__(self):
self.db = MySQLDatabase() # Direct dependency
def get_data(self):
self.db.connect()
print("Getting data")
manager = DataManager()
manager.get_data()Good Example (Follows DIP): DataManager depends on the abstract Database interface, not a concrete implementation. This makes it flexible and easy to extend.
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self):
pass
class MySQLDatabase(Database):
def connect(self):
print("Connecting to MySQL")
class PostgreSQLDatabase(Database):
def connect(self):
print("Connecting to PostgreSQL")
class DataManager:
def __init__(self, db: Database):
self.db = db # Depends on abstraction
def get_data(self):
self.db.connect()
print("Getting data")
mysql_db = MySQLDatabase()
postgres_db = PostgreSQLDatabase()
manager1 = DataManager(mysql_db)
manager2 = DataManager(postgres_db)
manager1.get_data() # Output: Connecting to MySQL \n Getting data
manager2.get_data() # Output: Connecting to PostgreSQL \n Getting dataMaking the DataManager class dependent on either of the class MySQLDatabase or PostgreSQLDatabase is going to violate the Dependency Inversion Principle. As both of these classes are extended from abstarction/interface Database. Making the DataManager class dependent on interface Database results in abiding the Dependency Inversion Principle.
Feel free to reach out on LinkedIn for networking.
Code examples & some the text content was assisted/generated by GitHub Copilot, an AI programming assistant by GitHub and OpenAI.