Dependency Inversion Principle in Java with Example

Introduction

The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented design. It states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Furthermore, abstractions should not depend on details. Details should depend on abstractions. This principle promotes loose coupling between software components.

Table of Contents

  1. What is the Dependency Inversion Principle?
  2. Benefits of the Dependency Inversion Principle
  3. Example: Violation of DIP
  4. Example: Adherence to DIP
  5. Real-World Example
  6. Conclusion

1. What is the Dependency Inversion Principle?

The Dependency Inversion Principle (DIP) asserts that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

This principle ensures that the system's high-level policy does not depend on the low-level details but rather on an abstraction.

2. Benefits of the Dependency Inversion Principle

  • Loose Coupling: Promotes loose coupling between classes and components.
  • Flexibility: Enhances flexibility and makes it easier to change or replace components.
  • Reusability: Encourages the development of reusable components.
  • Testability: Improves testability by allowing dependencies to be easily mocked or stubbed.

3. Example: Violation of DIP

In this example, we'll create a LightBulb class and a Switch class that violates DIP by depending directly on LightBulb.

Example:

class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb is turned on");
    }

    public void turnOff() {
        System.out.println("LightBulb is turned off");
    }
}

class Switch {
    private LightBulb lightBulb;

    public Switch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
    }

    public void flip(boolean on) {
        if (on) {
            lightBulb.turnOn();
        } else {
            lightBulb.turnOff();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        LightBulb lightBulb = new LightBulb();
        Switch lightSwitch = new Switch(lightBulb);
        lightSwitch.flip(true);  // Output: LightBulb is turned on
        lightSwitch.flip(false); // Output: LightBulb is turned off
    }
}

Issues:

  • The Switch class depends directly on the LightBulb class, creating tight coupling.
  • Any change in the LightBulb class requires changes in the Switch class.

4. Example: Adherence to DIP

To adhere to DIP, we can introduce an abstraction for the LightBulb class and make the Switch class depend on this abstraction.

Example:

Step 1: Define the Switchable Interface

interface Switchable {
    void turnOn();
    void turnOff();
}

Step 2: Implement the LightBulb Class

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb is turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb is turned off");
    }
}

Step 3: Implement the Switch Class

class Switch {
    private Switchable switchable;

    public Switch(Switchable switchable) {
        this.switchable = switchable;
    }

    public void flip(boolean on) {
        if (on) {
            switchable.turnOn();
        } else {
            switchable.turnOff();
        }
    }
}

Step 4: Main Class to Demonstrate DIP

public class Main {
    public static void main(String[] args) {
        Switchable lightBulb = new LightBulb();
        Switch lightSwitch = new Switch(lightBulb);
        lightSwitch.flip(true);  // Output: LightBulb is turned on
        lightSwitch.flip(false); // Output: LightBulb is turned off
    }
}

Explanation:

  • Switchable: An interface that defines the turnOn and turnOff methods.
  • LightBulb: A class that implements the Switchable interface.
  • Switch: A class that depends on the Switchable interface rather than the LightBulb class.
  • Main: Demonstrates the use of DIP by creating a Switch object that depends on the Switchable interface.

5. Real-World Example

Example: Payment Processing System

Consider a payment processing system where different payment methods (e.g., credit card, PayPal) need to be processed.

Step 1: Define the PaymentProcessor Interface

interface PaymentProcessor {
    void processPayment(double amount);
}

Step 2: Implement Specific Payment Classes

class CreditCardPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

class PayPalPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }
}

Step 3: Implement the PaymentService Class

class PaymentService {
    private PaymentProcessor paymentProcessor;

    public PaymentService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void makePayment(double amount) {
        paymentProcessor.processPayment(amount);
    }
}

Step 4: Main Class to Demonstrate DIP

public class Main {
    public static void main(String[] args) {
        PaymentProcessor creditCardPayment = new CreditCardPaymentProcessor();
        PaymentService paymentService = new PaymentService(creditCardPayment);
        paymentService.makePayment(100.0); // Output: Processing credit card payment of $100.0

        PaymentProcessor paypalPayment = new PayPalPaymentProcessor();
        paymentService = new PaymentService(paypalPayment);
        paymentService.makePayment(200.0); // Output: Processing PayPal payment of $200.0
    }
}

Explanation:

  • PaymentProcessor: An interface that defines the processPayment method.
  • CreditCardPaymentProcessor and PayPalPaymentProcessor: Classes that implement the PaymentProcessor interface.
  • PaymentService: A class that depends on the PaymentProcessor interface.
  • Main: Demonstrates the use of DIP by creating a PaymentService object that depends on the PaymentProcessor interface.

6. Conclusion

The Dependency Inversion Principle (DIP) is a fundamental concept in object-oriented design that promotes loose coupling between high-level and low-level modules by depending on abstractions rather than concrete implementations. By adhering to DIP, developers can create more maintainable, flexible, and testable code. Understanding and applying DIP is essential for building robust and scalable Java applications.

Happy coding!

Comments