Dependency Inversion in practice (example code in typescript)
Tags: programming, node, architecture, typescript
Published: 2022-01-23 15:33:03

## 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`. 

```typescript
// 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 };
```

```typescript
// 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:

```typescript
// file ./IUserRepo.ts
interface User {
  name: string;
  age: string;
}

interface IUserRepo {
  createUser(name: string, age: number): Promise<User>;
}

export { User, IUserRepo };
```

```typescript
// 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 };
```

```typescript
// file ./UserService.ts
import { IUserRepo } from "./IUserRepo";

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

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

export { UserService };
```

```typescript
// 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.

```typescript
// 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 };
```

```typescript
// 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`.

```typescript
// 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 };
```

```typescript
// 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();
```