Featured image of post Dependency Inversion

Dependency Inversion

Learn many things about DIP and how to decouple high and low-level components, creating a flexible and maintainable codebase.

Understanding Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is based on two key points:

  1. High level modules should not depend upon low level modules. both should depend upon abstractions.

  2. Abstractions should not depend upon details. details should depend upon abstractions.

These two points may sound a bit abstract, so let’s break them down.

  • The first point emphasizes the importance of decoupling in software systems. It implies that the high-level components, which contain the complex logic, should not depend on the low-level components, such as databases or servers. Instead, both should depend on abstract classes, such as interfaces or protocols, act as a mediator between high-level and low-level components.

  • The second point suggests that the details of implementation (low-level components) should depend on the abstract classes or interfaces. This approach ensures that changes in the low-level modules do not affect the high-level ones.

Before Applying DIP

Consider a scenario without applying the Dependency Inversion Principle, illustrated through a payment system example. In this setup, a PaymentProcessor class directly depends on the concrete implementation of a PaymentGateway:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class PaymentProcessor {
    private let paymentGateway: PaymentGateway

    init() {
        paymentGateway = PaymentGateway()
    }

    func processPayment(amount: Double) {
        // Process the payment using the payment gateway
        paymentGateway.processPayment(amount)
    }
}

class PaymentGateway {
    func processPayment(amount: Double) {
        // Actual implementation of processing the payment
    }
}

In this code snippet, any modifications or updates in the PaymentGateway class will affect the PaymentProcessor class, creating a tightly coupled system. This violates the principle of encapsulation and makes the codebase challenging to maintain and test.

Embracing Dependency Inversion Principle

In the updated example, we create a PaymentGateway protocol:

1
2
3
protocol PaymentGateway {
    func processPayment(amount: Double)
}

Now, the PaymentProcessor class depends on this abstraction rather than a concrete implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class PaymentProcessor {
    private let paymentGateway: PaymentGateway

    init(paymentGateway: PaymentGateway) {
        self.paymentGateway = paymentGateway
    }

    func processPayment(amount: Double) {
        // Process the payment using the provided payment gateway
        paymentGateway.processPayment(amount)
    }
}

Additionally, we can create different payment gateways, such as PayPalGateway and StripeGateway, that conform to the PaymentGateway protocol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PayPalGateway: PaymentGateway {
    func processPayment(amount: Double) {
        // Implementation of processing the payment using PayPal
    }
}

class StripeGateway: PaymentGateway {
    func processPayment(amount: Double) {
        // Implementation of processing the payment using Stripe
    }
}

Now, the PaymentProcessor is decoupled from specific implementations, allowing for easy swapping or extension of payment gateways without modifying the core logic. This adherence to Dependency Inversion Principle results in a loosely coupled, more maintainable, and flexible codebase.

Benefits of Dependency Inversion Principle

  1. Enhanced Maintainability: Changes to low-level modules do not impact high-level modules, simplifying maintenance.

  2. Improved Testability: Abstractions facilitate easier testing of high-level modules in isolation.

  3. Flexibility and Extensibility: The system becomes more adaptable, allowing for the addition of new functionalities without major code modifications.

Conclusion

By embracing the Dependency Inversion Principle, developers pave the way for scalable and resilient software architecture. The use of abstractions fosters a modular and extensible design, setting the stage for robust and future-proof applications. As we navigate the complexities of modern software development, the Dependency Inversion Principle stands as a guiding beacon toward code that withstands the test of time.

Reference 🥳

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy