The S.O.L.I.D. Foundation of System Design
Sourabh Ligade
Full-Stack Developer | AI/ML Engineer | Python, Django, React | Machine Learning, LLMs, Open-Source Contributor | Continuous Learner
S — Single Responsibility Principle
O — Open Close Design Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle
Advantages
The SOLID principles are like a set of rules to help make computer programs better. Here’s how they help:
1. Easy to Change: If you need to update or fix something, it’s much easier because everything has its own job.
2. Easy to Grow: You can add new things to your program without messing up what you already have.
3. Less Breaking: When you add something new or change something, there’s less chance of breaking other parts of your program.
4. Easy to Check: It’s easier to test if each part of your program works right, so you can find and fix problems faster.
5. Less Mixing Up: Parts of your program talk to each other in a neat way, so there’s less confusion and mess.
6. Clear to Understand: Your program is easier to read and understand, like a well-organized book, so others can figure out how it works more easily.
1. Single Responsibility Principle (SRP)
A class should not have more than one reason to change. If there are multiple responsibilities help by a single class - it may lead to introducing bugs in existing piece of code and breaking one functionality while making changes in another functionality.
Version-1
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
def pay(self, payment_type, security_code):
if payment_type == "debit":
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
elif payment_type == "credit":
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
else:
raise Exception(f"Unknown payment type: {payment_type}")
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
order.pay("debit", "0372846")
Version-2
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor:
def pay_debit(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
def pay_credit(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = PaymentProcessor()
processor.pay_debit(order, "0372846")
The original version of the Order class was doing too much. It was keeping track of items in the order, calculating the total price, and also handling the payment process. This is like having a chef in a restaurant who also serves the food and handles the cash register—all by themselves!
Applying the Single Responsibility Principle, we made a big change: we split the jobs. Now, the Order class only takes care of the order itself, like keeping track of items and calculating how much they cost. It's like the chef focusing on cooking only.
Then, we created a new class called PaymentProcessor. This class has one job: handling payments. Whether it's paying by debit or credit, this class takes care of checking the payment details and updating the order status to "paid." This is like having a cashier who handles all the money stuff, separate from the chef.
By doing this, each class has its own responsibility. The Order class focuses on the order, and the PaymentProcessor class focuses on payments. This makes the code easier to understand, manage, and change in the future because each part does one specific thing.
2. Open/Closed Principle (OCP)
We should not make changes/ modifications in existing classes to provide new functionality. Instead we should extend the existing classes to support add-on features/ functionalities.
O stands for Open for extension and closed for Modifications.
Version-1
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor:
def pay_debit(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
def pay_credit(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = PaymentProcessor()
processor.pay_debit(order, "0372846")
Version-2
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order, security_code):
pass
class DebitPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = DebitPaymentProcessor()
processor.pay(order, "0372846")
In the newest version of the code, there's a special rule being followed called the Open/Closed Principle. This principle says that a piece of software (like a class in programming) should be open for extension but closed for modification. In plain language, it means you should be able to add new features without changing the existing code.
Before:
Previously, the PaymentProcessor class didn't exist. The Order class had methods to handle payments directly, and if we wanted to add a new payment method, we'd have to change the Order class itself. That's like having a toolbox where you need to change the toolbox itself every time you get a new tool.
After:
What Changed for Open/Closed Principle:
3. Liskov Substitution Principle (LSP)
Every Parent Class should be replaceable by it’s sub-class. If a class B extends a class A - then it should be possible to replace object of class A will object of class B without breaking any of the client’s behavior. In simple words, child class should extend the capability of parent class and not narrow it down.
Version-1
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order, security_code):
pass
class DebitPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing paypal payment type")
print(f"Using email address: {security_code}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = PaypalPaymentProcessor()
processor.pay(order, "[email protected]")
Version-2
领英推荐
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def pay(self, order):
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def pay(self, order):
print("Processing credit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address):
self.email_address = email_address
def pay(self, order):
print("Processing paypal payment type")
print(f"Using email address: {self.email_address}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = PaypalPaymentProcessor("[email protected]")
processor.pay(order)
What is the Liskov Substitution Principle (LSP)?
The LSP is like saying you can replace something with a version of it, and everything should still work fine. Imagine you have a favorite app you use all the time. If an update for the app comes out, you expect it to do all the things the old one did, maybe with some extra features or fixes. You don't want the update to break the app or remove something you rely on.
How Does It Apply Here?
In the provided code, there's a PaymentProcessor class that acts like a general idea of "paying for an order." It says, "If you want to be a payment processor, you need to have a pay method that takes an order."
Then, there are specific versions of this idea: DebitPaymentProcessor, CreditPaymentProcessor, and PaypalPaymentProcessor. Each of these is like an update or a special version of the PaymentProcessor. According to LSP, you should be able to use any of these payment processors in place of just a general PaymentProcessor, and they should all work properly—they should all be able to pay for the order without causing problems.
The Practical Example:
Why It's Cool:
This is great because it lets the rest of your code (like where you handle orders) be simple and not worry about the details of payment processing. Whether it's a debit card, a credit card, or PayPal, your order code doesn't need to change. This makes your code more flexible and easier to extend with new payment methods in the future without messing with existing functionality.
4. Interface Segregation Principle (ISP)
Clients should not be forced to implement methods that they don’t use. Try to make your interfaces narrow enough that client classes don’t have to implement behaviors they don’t need.
Version-1
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor(ABC):
@abstractmethod
def auth_sms(self, code):
pass
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
self.verified = False
def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True
def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def auth_sms(self, code):
raise Exception("Credit card payments don't support SMS code authorization.")
def pay(self, order):
print("Processing credit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address):
self.email_address = email_address
self.verified = False
def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True
def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Using email address: {self.email_address}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = DebitPaymentProcessor("2349875")
processor.auth_sms(465839)
processor.pay(order)
Version-2
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass
class PaymentProcessorSMS(PaymentProcessor):
@abstractmethod
def auth_sms(self, code):
pass
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessorSMS):
def __init__(self, security_code):
self.security_code = security_code
self.verified = False
def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True
def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def pay(self, order):
print("Processing credit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessorSMS):
def __init__(self, email_address):
self.email_address = email_address
self.verified = False
def auth_sms(self, code):
print(f"Verifying SMS code {code}")
self.verified = True
def pay(self, order):
if not self.verified:
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Using email address: {self.email_address}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
processor = PaypalPaymentProcessor("[email protected]")
processor.auth_sms(465839)
processor.pay(order)
Applying ISP in Our Code:
We've applied the Interface Segregation Principle by developing two distinct interfaces (abstract classes in Python): PaymentProcessor and PaymentProcessorSMS.
Before ISP:
Initially, a singular, bulky interface might have been used, demanding all payment methods to support SMS authentication, even when unnecessary. This approach could lead to superfluous implementations, cluttering the codebase.
After ISP:
The introduction of specialized interfaces ensures:
5. Dependency Inversion Principle (DIP)
High-level classes shouldn’t depend on low-level classes. Both should depend on abstractions. Abstractions shouldn’t depend on details. Details should depend on abstractions.
In short, Class should depend on interfaces rather than concrete classes.
Version-1
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class SMSAuthorizer:
def __init__(self):
self.authorized = False
def verify_code(self, code):
print(f"Verifying SMS code {code}")
self.authorized = True
def is_authorized(self) -> bool:
return self.authorized
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code, authorizer: SMSAuthorizer):
self.security_code = security_code
self.authorizer = authorizer
def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def pay(self, order):
print("Processing credit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address, authorizer: SMSAuthorizer):
self.email_address = email_address
self.authorizer = authorizer
def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Using email address: {self.email_address}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
authorizer = SMSAuthorizer()
authorizer.verify_code(465839)
processor = PaypalPaymentProcessor("[email protected]", authorizer)
processor.pay(order)
Version-2
from abc import ABC, abstractmethod
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name, quantity, price):
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for i in range(len(self.prices)):
total += self.quantities[i] * self.prices[i]
return total
class Authorizer(ABC):
@abstractmethod
def is_authorized(self) -> bool:
pass
class AuthorizerSMS(Authorizer):
def __init__(self):
self.authorized = False
def verify_code(self, code):
print(f"Verifying SMS code {code}")
self.authorized = True
def is_authorized(self) -> bool:
return self.authorized
class AuthorizerGoogle(Authorizer):
def __init__(self):
self.authorized = False
def verify_code(self, code):
print(f"Verifying Google auth code {code}")
self.authorized = True
def is_authorized(self) -> bool:
return self.authorized
class AuthorizerRobot(Authorizer):
def __init__(self):
self.authorized = False
def not_a_robot(self):
self.authorized = True
def is_authorized(self) -> bool:
return self.authorized
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code, authorizer: Authorizer):
self.security_code = security_code
self.authorizer = authorizer
def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def __init__(self, security_code):
self.security_code = security_code
def pay(self, order):
print("Processing credit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
class PaypalPaymentProcessor(PaymentProcessor):
def __init__(self, email_address, authorizer: Authorizer):
self.email_address = email_address
self.authorizer = authorizer
def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing paypal payment type")
print(f"Using email address: {self.email_address}")
order.status = "paid"
order = Order()
order.add_item("Keyboard", 1, 50)
order.add_item("SSD", 1, 150)
order.add_item("USB cable", 2, 5)
print(order.total_price())
authorizer = AuthorizerRobot()
authorizer.verify_code(465839)
authorizer.not_a_robot()
processor = PaypalPaymentProcessor("[email protected]", authorizer)
processor.pay(order)
## Changes Made:
1. Before DIP Implementation: Initially, the payment processors directly depended on a specific type of authorization (SMSAuthorizer), making the system less flexible. If we wanted to introduce a new authorization method, we would have to modify the payment processors directly, which goes against the open/closed principle.
2. After DIP Implementation:
- An Authorizer abstract class (interface) was introduced, representing the abstraction for authorization mechanisms. This abstraction defines what it means to be an authorizer without specifying how the authorization is performed.
- Concrete classes like AuthorizerSMS, AuthorizerGoogle, and AuthorizerRobot implement this interface, each providing a specific authorization mechanism. This setup allows for new authorization mechanisms to be added without modifying existing payment processors.
- Payment processors (`DebitPaymentProcessor`, CreditPaymentProcessor, PaypalPaymentProcessor) now depend on the Authorizer abstraction rather than concrete implementations. This means any object that implements the Authorizer interface can be used, increasing flexibility and enabling the system to easily adapt to new requirements or changes.
### Impact of DIP:
- Flexibility: The system can now support various authorization mechanisms without changing the payment processing logic. This makes it easy to introduce new authorization methods in the future.
- Decoupling: Payment processors are no longer tightly coupled to a specific authorization method. This reduces dependency on specific implementations, making the system more robust and easier to maintain.
- Scalability: It's easier to scale the system by adding new authorization methods or payment processors, as the core architecture doesn't need to change.
By adhering to the Dependency Inversion Principle, the updated design improves the system's ability to evolve and adapt, ensuring that changes in authorization mechanisms or payment processing strategies can be accommodated with minimal impact on the existing codebase.
Thank you for reading the Article!