SOLID Principles in Real-Life
Hany Sayed
Senior Software Engineer | Full-Stack Expert (Ruby on Rails, .NET, Angular, React) | Agile Leader | Scaled Apps to 500K+ Users | PSM II Certified
Have you ever wondered how software stays flexible, easy to maintain, and scalable? It’s all about following the SOLID principles!
Let me break it down for you with everyday examples
S. Single Responsibility Principle (SRP)
"One job, one person."
Imagine a chef in a restaurant. The chef’s job is to cook, not to serve food or clean tables. If the chef tries to do everything, the kitchen becomes chaotic. Similarly, in software, each component should have one clear responsibility. This makes it easier to manage and update.
Example
Let's assume we have the class in our e-commerce app.
This class now has 2 responsibilities. In other words, what if we want to send email?
At any other point in our code, will we need to create an instance for this class just for sending email? or will we duplicate this code in each class that needs to send email?
class OrderProcessor {
public void processOrder(String order) {
System.out.println("Processing order: " + order);
}
public void sendEmail(String customer, String message) {
System.out.println("Sending email to " + customer + ": " + message);
}
}
so whenever see something like this, we need to Separate responsibilities
by doing this, we are now able to use email service in whatever place we want and we are also able to test its functionality without depending on any other class
class OrderProcessor {
public void processOrder(String order) {
System.out.println("Processing order: " + order);
}
}
class EmailService {
public void sendEmail(String customer, String message) {
System.out.println("Sending email to " + customer + ": " + message);
}
}
O. Open/Closed Principle (OCP)
"Open for extension, closed for modification."
Think of a LEGO set. You can build a house, a car, or a spaceship using the same LEGO pieces without breaking or changing the original pieces. Similarly, software should be designed so you can add new features without changing the existing code.
Example
Lets assume this scenario We want to add new payment methods (e.g., PayPal, Crypto) without modifying the existing payment processing code.
so the normal way is to add another if condition for the new payment method, and you will also update the test code for this class and by time this class will be huge and debugging inside it will be annoying
class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("credit_card")) {
System.out.println("Processing credit card payment of $" + amount);
} else if (paymentType.equals("paypal")) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
}
Now let's refactor this code to validate OCP.
By doing this now, our application becomes open for extension and closed for modification.
Whenever we want to add a new payment method, we will neither modify other methods nor the testing for them; we will just add a new class to represent the new payment and its own tesing class.
interface PaymentProcessor {
void processPayment(double amount);
}
class CreditCardProcessor implements PaymentProcessor {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}
class PayPalProcessor implements PaymentProcessor {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
class CryptoProcessor implements PaymentProcessor {
public void processPayment(double amount) {
System.out.println("Processing Crypto payment of $" + amount);
}
}
L. Liskov Substitution Principle (LSP)
"If it looks like a duck and quacks like a duck, it should act like a duck."
Consider an online store that supports multiple payment methods, such as credit card, PayPal, and gift cards.
When you proceed to checkout, the system asks you to select a payment method. Regardless of which method you choose:
If one payment method works differently (e.g., a gift card fails), it would violate the Liskov Substitution Principle because the system relies on all payment methods behaving consistently as substitutes for the generic payment interface.
Substitutes should work seamlessly without breaking anything.
This ensures customers have a smooth and predictable checkout experience, regardless of how they pay.
领英推荐
Example
Let's assume that we have added a new payment method with the gift card, but we haven't implemented the way of confirmation yet, so there is no confirmation transaction for the gift card. the below code violates LSP thats because if the new payment method can process payments and confirm transaction, then we consider it a payment method, but in our case, a gift card cannot do that
interface PaymentMethod {
void processPayment(double amount);
void confirmTransaction();
}
class CreditCardPayment implements PaymentMethod {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
public void confirmTransaction() {
System.out.println("Credit card transaction confirmed!");
}
}
class PayPalPayment implements PaymentMethod {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
public void confirmTransaction() {
System.out.println("PayPal transaction confirmed!");
}
}
class GiftCardPayment implements PaymentMethod {
public void processPayment(double amount) {
System.out.println("Processing gift card payment of $" + amount);
}
public void confirmTransaction() {
// Violation: Gift cards cannot confirm transactions
throw new UnsupportedOperationException("Gift card transactions cannot be confirmed!");
}
}
Lets Refactor that to validate LSP
Now Gift card is acting as payment method since the payment method interface has function processPayment and our class can handle
interface PaymentMethod {
void processPayment(double amount);
}
interface Confirmable {
void confirmTransaction();
}
class CreditCardPayment implements PaymentMethod, Confirmable {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
public void confirmTransaction() {
System.out.println("Credit card transaction confirmed!");
}
}
class PayPalPayment implements PaymentMethod, Confirmable {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
public void confirmTransaction() {
System.out.println("PayPal transaction confirmed!");
}
}
class GiftCardPayment implements PaymentMethod {
public void processPayment(double amount) {
System.out.println("Processing gift card payment of $" + amount);
}
}
So whenever you see a code, think of this
"If it looks like a duck and quacks like a duck, it should act like a duck."
if your class doesn't do this, then it voilate the LSP
I. Interface Segregation Principle (ISP)
"Don’t force someone to use what they don’t need."
Think of a Swiss Army knife. It has many tools, but you only use the ones you need. You wouldn’t want a knife with 50 tools if you only needed a screwdriver! Similarly, software interfaces should be small and specific, so users only deal with what’s relevant to them.
Example
Lets assume in our e-commerce app we have many user types: admin, customer, and seller
Now in this code, you will see that customer, when implements user interface, it must override unnecessary methods that aren't related to him, like manage Inventory , manageUsers so this violates the ISP
interface User {
void viewProducts();
void purchaseProduct();
void manageInventory();
void manageUsers();
}
class Customer implements User {
public void viewProducts() {
System.out.println("Customer is viewing products.");
}
public void purchaseProduct() {
System.out.println("Customer is purchasing a product.");
}
public void manageInventory() {
// Violation: Customers don't manage inventory
throw new UnsupportedOperationException("Customers cannot manage inventory!");
}
public void manageUsers() {
// Violation: Customers don't manage users
throw new UnsupportedOperationException("Customers cannot manage users!");
}
}
to refactor this code by doing this, each user type will have the necessary functions only
interface ProductViewer {
void viewProducts();
}
interface ProductPurchaser {
void purchaseProduct();
}
interface InventoryManager {
void manageInventory();
}
interface UserManager {
void manageUsers();
}
class Customer implements ProductViewer, ProductPurchaser {
public void viewProducts() {
System.out.println("Customer is viewing products.");
}
public void purchaseProduct() {
System.out.println("Customer is purchasing a product.");
}
}
D. Dependency Inversion Principle (DIP)
"Depend on abstractions, not on details."
Imagine a universal charger adapter. It doesn’t care if you plug in a phone, laptop, or camera; it works because it depends on a standard plug shape, not the device itself. In software, high-level modules should depend on general ideas (abstractions), not specific implementations.
Example
Let's assume in our e-commerce app we have one payment gateway and we want to add a new one.
Now the order service directly depends on the StripePaymentGateway and each time we want to add a new payment gateway, it will be directly added to the order service, and this not only breaks DIP, it breaks the Object Origented Design
class StripePaymentGateway {
public void processPayment(double amount) {
System.out.println("Processing payment of $" + amount + " via Stripe");
}
}
class OrderService {
private StripePaymentGateway paymentGateway;
public OrderService() {
this.paymentGateway = new StripePaymentGateway(); // Direct dependency
}
public void checkout(double amount) {
paymentGateway.processPayment(amount);
}
}
so lets refactor this code to validate DIP
Now whenever we want to add a new gateway, there is direct abstraction.
all we will do is just add the new payment gateway and let it implement the interface
and not only our refactor follows DIP it also follow OCP
PaymentGateway {
void processPayment(double amount);
}
class StripePaymentGateway implements PaymentGateway {
public void processPayment(double amount) {
System.out.println("Processing payment of $" + amount + " via Stripe");
}
}
class PayPalPaymentGateway implements PaymentGateway {
public void processPayment(double amount) {
System.out.println("Processing payment of $" + amount + " via PayPal");
}
}
class OrderService {
private PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void checkout(double amount) {
paymentGateway.processPayment(amount);
}
}
Why Does This Matter?
SOLID principles are like the rules of good design—whether it’s building software, running a kitchen, or assembling LEGO sets. They make systems flexible, scalable, and easy to maintain.
Next time you see a well-organized system, think SOLID! ??