The Dependency Inversion Principle (DIP) is a software design principle that suggests high-level modules/classes should not depend on low-level modules/classes directly. Instead, they should depend on abstractions (interfaces or protocols), and the low-level modules/classes should implement those abstractions.
Before applying the Dependency Inversion Principle, the high-level module directly depends on the low-level module. This means that any changes or updates in the low-level module will affect the high-level module, leading to a tightly coupled system. Let's take an example of a payment system:
Before Dependency Inversion Principle:
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 the above example, the PaymentProcessor directly depends on the concrete implementation of PaymentGateway. If we want to change the payment gateway or add another payment gateway, we would need to modify the PaymentProcessor class, violating the principle of encapsulation and making it harder to maintain and test the code.
After applying the Dependency Inversion Principle, the high-level module depends on an abstraction (interface or protocol), which is implemented by the low-level module. This way, the high-level module becomes independent of the specific implementation details of the low-level module.
After Dependency Inversion Principle:
protocol PaymentGateway {
func processPayment(amount: Double)
}
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)
}
}
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
}
}
In the updated example, we introduce the PaymentGateway protocol, which defines the contract for processing payments. The PaymentProcessor class now depends on the abstraction (protocol) instead of a concrete implementation. We can create different payment gateways (e.g. PayPalGateway, StripeGateway) that conform to the PaymentGateway protocol and pass them to the PaymentProcessor at runtime.
This design allows us to easily swap or extend payment gateways without modifying the PaymentProcessor class. It promotes loose coupling and enhances code maintainability, testability, and flexibility.