SOLID Principles

 

 SOLID Principles

1. S — Single Responsibility Principle (SRP)

👉 A class should have only one reason to change.

  • Bad Example ❌

class Payment {

    void makeUPIPayment() { }
    void generateReceipt() { }
    void sendNotification() { }
}


This class does too much → payment + receipt + notification.

  • Good Example 

class UPIPayment {
    void makePayment() { }
}

    void generateReceipt() { }
}

    void sendNotification() { }
}


Each class has one responsibility.
If tomorrow notification changes (SMSWhatsApp), only NotificationService changes.



2. O — Open/Closed Principle (OCP)

👉 Classes should be open for extension but closed for modification.

  • Bad Example ❌


    void process(String type) {
        if(type.equals("UPI")) { /* UPI logic */ }
        else if(type.equals("CARD")) { /* Card logic */ }
        else if(type.equals("WALLET")) { /* Wallet logic */ }
    }
}

If a new method like NETBANKING comes, you must modify this class → breaks OCP.

  • Good Example (Use abstraction)

interface PaymentMethod {
    void pay();
}

class UPIPayment implements PaymentMethod {
    public void pay() { /* UPI logic */ }
}

class CardPayment implements PaymentMethod {
    public void pay() { /* Card logic */ }
}

class PaymentProcessor {
    private PaymentMethod method;
    PaymentProcessor(PaymentMethod method) { this.method = method; }

    void process() {
        method.pay();
    }
}


Now adding NetBanking means just creating a new class → extend, not modify.



3. L — Liskov Substitution Principle (LSP)

👉 Subclasses should be substitutable for their base classes without breaking the program.


LSP Rule in One Line

If you replace a parent class with a child class, your code should still work fine.


 Means: Child class should behave like parent class without breaking anything.

🍽️ Real-Life Analogy: Spoon vs Fork

  • You have a function that eats soup using a Spoon.

  • You create a subclass Fork and pass it to the same function.

  • But Fork can’t hold soup — so the function fails.

That’s a violation of LSP.

  • Bad Example ❌

class PaymentMethod {
    void pay() { }
}

class CODPayment extends PaymentMethod {
    void pay() {
        throw new UnsupportedOperationException("COD not supported in UPI app");
    }
}

Here, if you use CODPayment instead of UPIPayment, it breaks things.

  • Good Example 

interface PaymentMethod {
    void pay();
}

class UPIPayment implements PaymentMethod {
    public void pay() { /* pay via UPI */ }
}

class CardPayment implements PaymentMethod {
    public void pay() { /* pay via Card */ }
}


Every subclass truly works as PaymentMethod.



4. I — Interface Segregation Principle (ISP)

👉 No client should be forced to depend on methods it doesn’t use.

  • Bad Example ❌

interface PaymentService {
    void pay();
    void refund();
    void emiConversion();
}


Now, UPIPayment doesn’t support emiConversion, but it’s forced to implement it.

Good Example 

interface Payment {
    void pay();
}

interface Refundable {
    void refund();
}

interface EMIConvertible {
    void emiConversion();
}

class UPIPayment implements Payment { 
    public void pay() { /* UPI logic */ } 
}

class CardPayment implements Payment, EMIConvertible {
    public void pay() { /* card logic */ }
    public void emiConversion() { /* EMI logic */ }
}

Each class implements only what it needs.

5. D — Dependency Inversion Principle (DIP)

👉 High-level modules should not depend on low-level modules. Both should depend on abstractions.

🔑 Definition

High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • High-level module = big boss logic (e.g. PaymentProcessor that handles checkout).

  • Low-level module = detail logic (e.g. UPIPayment, CardPayment, etc.).

  • Abstraction = an interface or abstract class (e.g. PaymentMethod).


❌ Without Dependency Inversion (bad design)



// Low-level modules
class UPIPayment {
    void pay() {
        System.out.println("Paying via UPI");
    }
}

class WalletPayment {
    void pay() {
        System.out.println("Paying via Wallet");
    }
}

class CardPayment {
    void pay() {
        System.out.println("Paying via Card");
    }
}

// High-level module
class PaymentProcessor {
    private UPIPayment upi = new UPIPayment();
    private WalletPayment wallet = new WalletPayment();
    private CardPayment card = new CardPayment();

    void process(String type) {
        if (type.equals("UPI")) {
            upi.pay();
        } else if (type.equals("Wallet")) {
            wallet.pay();
        } else if (type.equals("Card")) {
            card.pay();
        } else {
            System.out.println("Invalid payment method");
        }
    }
}

// Test app
public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();
        processor.process("UPI");
        processor.process("Wallet");
        processor.process("Card");
    }
}


🚨 Problems here:

  1. Tightly coupled

    • PaymentProcessor knows all payment methods (UPI, Wallet, Card).

    • If tomorrow you add NetBanking, you must open and change PaymentProcessor.

    • Violates OCP (Open-Closed Principle) too.

  2. Hard to test
    You cannot replace real payment classes with mocks for testing.

  3. Business logic depends on low-level details
    High-level (PaymentProcessor) is directly tied to low-level classes (UPIPayment, WalletPayment, CardPayment).
    👉 This breaks the Dependency Inversion Principle (DIP).


We’ll introduce a PaymentMethod interface → all payment types (UPI, Wallet, Card, etc.) will implement it.
Then, PaymentProcessor will depend only on the abstraction, not the concrete classes.



// Abstraction (interface)
interface PaymentMethod {
    void pay();
}

// Low-level modules implement the interface
class UPIPayment implements PaymentMethod {
    public void pay() {
        System.out.println("Paying via UPI");
    }
}

class WalletPayment implements PaymentMethod {
    public void pay() {
        System.out.println("Paying via Wallet");
    }
}

class CardPayment implements PaymentMethod {
    public void pay() {
        System.out.println("Paying via Card");
    }
}

// High-level module (depends on abstraction, not concrete class)
class PaymentProcessor {
    private PaymentMethod paymentMethod;

    // Dependency Injection (pass payment method at runtime)
    public PaymentProcessor(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void process() {
        paymentMethod.pay();
    }
}

// Test app
public class Main {
    public static void main(String[] args) {
        PaymentProcessor upiProcessor = new PaymentProcessor(new UPIPayment());
        upiProcessor.process();

        PaymentProcessor walletProcessor = new PaymentProcessor(new WalletPayment());
        walletProcessor.process();

        PaymentProcessor cardProcessor = new PaymentProcessor(new CardPayment());
        cardProcessor.process();
    }
}


🔑 Advantages:

  1. PaymentProcessor is not tightly coupled to UPI, Wallet, or Card.

  2. Adding new method (e.g., NetBanking, Crypto, PayLater) = No change in PaymentProcessor.
    Just create a new class implementing PaymentMethod.

  3. Easy to test → you can inject a mock payment class in unit tests.



Comments

Popular posts from this blog

Two Sum II - Input Array Is Sorted

Comparable Vs. Comparator in Java

Increasing Triplet Subsequence