Dependency Inversion in practice (example code in typescript)
Thy Pham
Posted on January 23, 2022
Summary
- Dependency Inversion is a technique (as the name suggests) to invert the dependency from one module to another.
- Without DI:
A -> B
(A depends on B) - With DI:
(A -> I) <- B
(B depends on A through interface I) - We can use DI to protect our core business logic module (and its tests) from depending on other modules (Database, HTTP API, etc...)
What is Dependency Inversion (DI)?
Imagine a class called UserService
that needs to interact with MongoDB to save new user data. UserService
will call createUser()
function provided by UserRepoMongoDB
class to serve this purpose. That means UserService
depends on UserRepoMongoDB
.
// file ./UserRepoMongoDB.ts
interface MongoDBUserSchema {
// MongoDB user schema fields
}
class UserRepoMongoDB {
createUser(name: string, age: number): Promise<MongoDBUserSchema> {
// Some MongoDB specific logic to store user data in MongoDB
}
}
export { UserRepoMongoDB };
// file ./UserService.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";
class UserService {
createUser() {
return new UserRepoMongoDB(). createUser("Max Mustermann", 20);
}
}
export { UserService };
To make UserRepoMongoDB
depends on UserService
, we can create an interface IUserRepo
with the createUser()
function. UserService
will use this function from IUserRepo
instead. And UserRepoMongoDB
will implement this interface:
// file ./IUserRepo.ts
interface User {
name: string;
age: string;
}
interface IUserRepo {
createUser(name: string, age: number): Promise<User>;
}
export { User, IUserRepo };
// file ./UserRepoMongoDB.ts
import { IUserRepo, User } from "./IUserRepo";
interface MongoDBUserSchema {
// MongoDB user schema fields
}
class UserRepoMongoDB implements IUserRepo {
createUser(name: string, age: number): Promise<User> {
// 1. Store user into MongoDB.
// 2. Convert result from MongoDBUserSchema type to User type and return.
}
}
export { UserRepoMongoDB };
// file ./UserService.ts
import { IUserRepo } from "./IUserRepo";
class UserService {
constructor(private userRepo: IUserRepo) {}
createUser() {
return this.userRepo.createUser("Max Mustermann", 20);
}
}
export { UserService };
// file ./main.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";
import { UserService } from "./UserService";
function executeCode() {
new UserService(new UserRepoMongoDB()).createUser();
}
When DI comes to the rescue
One day the team decides to use DynamoDB instead of MongoDB, so we must change the code to make it works. Without DI, we need to create a new class UserRepoDynamoDB
with the createUser()
function and change our UserService
to use this new function. That means we have to change our core business logic code (UserService
) every time there is an update on the database module.
// file ./UserRepoDynamoDB.ts
interface DynamoDBUserSchema {
// DynamoDB user schema fields
}
class UserRepoDynamoDB {
createUser(
Id: string, // DynamoDB needs this field in user Table
name: string,
age: number
): Promise<DynamoDBUserSchema> {
// store user data in DynamoDB
}
}
export { UserRepoDynamoDB };
// file ./UserService.ts
import { randomUUID } from "crypto";
import { UserRepoDynamoDB } from "./UserRepoDynamoDB";
class UserService {
// This function is updated to adapt DynamoDB
createUser() {
const Id = randomUUID();
return new UserRepoDynamoDB().createUser(Id, "Max Mustermann", 20);
}
}
export { UserService };
But if we use DI, all we need to do is make the new class UserRepoDynamoDB
implement IUserRepo
, and that's it!
There is no need to change anything in UserService
.
// file ./UserRepoDynamoDB.ts
import { IUserRepo, User } from "./IUserRepo";
interface DynamoDBUserSchema {
// DynamoDB user schema fields
}
class UserRepoDynamoDB implements IUserRepo {
createUser(name: string, age: number): Promise<User> {
// 1. Generate new Id and Store user into DynamoDB.
// 2. Convert result from DynamoDBUserSchema type to User type and return.
}
}
export { UserRepoDynamoDB };
// file ./main.ts
import { UserRepoDynamoDB } from "./UserRepoDynamoDB";
import { UserService } from "./UserService";
function executeCode() {
// Use UserRepoDynamoDB instead of old UserRepoMongoDB
new UserService(new UserRepoDynamoDB()).createUser();
}
executeCode();
Posted on January 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.