Introduction
Dependency Inversion Principle (DIP) is the last principle in S.O.L.I.D
principles.
The principle states that: a high level code should depend on abstractions, not on concretions.
- High-level modules contain application-specific business logic. They are responsible for controlling and orchestrating the application's behavior.
- Low-level modules deal with implementation details and services that the high-level modules use. They handle tasks like data access, I/O operations, and external service integration.
DIP
encourages the use of abstract classes or interfaces to define the contracts between high-level and low-level modules. High-level modules depend on abstractions (interfaces or abstract classes) rather than concrete implementations. This allows for flexibility and the ability to swap out implementations without modifying high-level code.
What is an abstraction
An abstraction is a way to hide the implementation details of a class and show only the functionalities to the users either by using an interface or an abstract class.
What is a concretion
A concrete class is a class that implements an abstraction.
So we can conclude that an interface is called abstraction, a class that implements an interface is called concrete class.
Why we need to depend on abstractions
In nutshell, we need to decuple the high level code and level code from concrete classes, why would we do that?
Well, to make the code more flexible and easy to maintain.
How Dependency Inversion makes the code flexible
Let's take an example, we'are developing an e-commerce application, and we have a cart class that needs a payment method to process the payment.
Let's say we will use a CreditCardPayment
class.
import CreditCardPayment from './payments/credit-card-payment';
class Cart {
private payment: CreditCardPayment;
public constructor() {
this.payment = new CreditCardPayment();
}
// ...rest of the code
}
This will work normally, but what if we decided to add another payment method, let's say PayPalPayment
class.
In this case we'll have to update any code that creates the credit card payment class and replace it with paypal payment class.
How to solve this problem
We can solve this problem by depending on an abstraction instead of a concrete class.
interface PaymentMethod {
pay(): void;
}
class CreditCardPayment implements PaymentMethod {
public pay(): void {
console.log('Paying with credit card');
}
}
class PayPalPayment implements PaymentMethod {
public pay(): void {
console.log('Paying with paypal');
}
}
Now we can depend on the PaymentMethod
interface instead of the concrete classes.
import PaymentMethod from './payment-method';
class Cart {
public constructor(private payment: PaymentMethod) {
//
}
}
Now we can pass any class that implements the PaymentMethod
interface.
import Cart from './cart';
import CreditCardPayment from './payments/credit-card-payment';
const cart = new Cart(new CreditCardPayment());
Here we made two things:
- We decoupled the high level code from the low level code by depending on an abstraction instead of a concrete class.
- We passed created a new instance of payment outside the cart class and passed it to the class, this is also called
dependency injection
and makes itDecoupled
What is dependency injection
Dependency Injection is a design pattern that is primarily a way to implement Dependency Inversion Principle
.
The idea is simple, instead of creating the dependencies inside the class, we create them outside the class and pass them to the class.
Why we need to use dependency injection
For many reasons actually:
-
Decoupling
the high level code from the low level code. -
Testing
the class will be easier, we can pass a mock class instead of the real class. -
Reusability
we can reuse the class with different dependencies. -
Flexibility
we can change the dependencies without changing the class.
How to implement dependency injection
There are many ways to implement dependency injection, we can use a constructor
, a setter
or a method
.
Constructor
class Cart {
public constructor(private payment: PaymentMethod) {
//
}
}
Setter
class Cart {
private payment: PaymentMethod;
public setPayment(payment: PaymentMethod): void {
this.payment = payment;
}
}
Method
class Cart {
public pay(payment: PaymentMethod): void {
payment.pay();
}
}
Later on, we'll take more in depth look at dependency injection with Inversion Of Control (IoC) containers.
Conclusion
Dependency Inversion Principle (DIP) is the last principle in S.O.L.I.D
principles.
The principle states that: a high level code should depend on abstractions, not on concretions.
We can solve this problem by depending on an abstraction instead of a concrete class.
Following the list
You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript
Join us in our Discord Community
https://discord.gg/XDZcTuU8c8
Top comments (0)