DEV Community

Thy Pham
Thy Pham

Posted on • Updated on

Dependency Inversion in practice (example code in typescript)

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 };
Enter fullscreen mode Exit fullscreen mode
// file ./UserService.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";

class UserService {
  createUser() {
    return new UserRepoMongoDB(). createUser("Max Mustermann", 20);
  }
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode
// 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 };
Enter fullscreen mode Exit fullscreen mode
// file ./UserService.ts
import { IUserRepo } from "./IUserRepo";

class UserService {
  constructor(private userRepo: IUserRepo) {}

  createUser() {
    return this.userRepo.createUser("Max Mustermann", 20);
  }
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode
// file ./main.ts
import { UserRepoMongoDB } from "./UserRepoMongoDB";
import { UserService } from "./UserService";

function executeCode() {
  new UserService(new UserRepoMongoDB()).createUser();
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode
// 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 };
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode
// 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();
Enter fullscreen mode Exit fullscreen mode

Discussion (0)