Elevating Code Flexibility: Dependency Inversion with DI Containers
Tags: typescript, cleancode, programming, coding
Published: 2023-07-03 12:05:02

Having tight-coupling between classes and objects is consistently detrimental, as it diminishes code flexibility and reusability. Each modification to a single component necessitates the vigorous task of dealing with its dependencies, causing developers to face recurring challenges. 
In the realm of software development, the following design patterns serve as fundamental pillars extensively employed to eliminate tight coupling, especially when adhering to the [SOLID](https://en.wikipedia.org/wiki/SOLID) principles.
- **Inversion-of-Control ([IoC](https://en.wikipedia.org/wiki/Inversion_of_control))**
- **Dependency-Injection ([DI](https://en.wikipedia.org/wiki/Dependency_injection))**
- **Dependency-Inversion-Principle ([DIP](https://en.wikipedia.org/wiki/Dependency_inversion_principle))**

These patterns offer developers the means to decouple dependencies and achieve code that is flexible, maintainable, and extensible.

Furthermore, the modern landscape of development practices has witnessed the emergence of highly acclaimed **"DI Containers"** frameworks. These frameworks have revolutionized the implementation of IoC and DI, empowering developers to construct software architectures that are robust, modular, and scalable.

Lets review a practical example to employ the powerful Dependency Inversion Principle that results in modular and clean design. Examples are written for a TypeScript-based codebase utilizing [InversifyJS](https://inversify.io/) as the DI Container.

## Preliminary Setup
Lets expect that your application has a DI container set up, with each class appropriately attached to it using the necessary procedures. In InversifyJS, creating an application container is as straightforward as shown below:

#### **`AppContainer.ts`**
```TypeScript
import {Container} from 'inversify';
import {ServiceOne, ServiceTwo} from '../services';

export class AppContainer {
  private static container: Container;

  public static build(){
    const container = new Container();
    
    container.bind<ServiceOne>('ServiceOne').to(ServiceOne);
    container.bind<ServiceTwo>('ServiceTwo').to(ServiceTwo);
  }
}
```
## Practical Use Case
Lets take a fictional Authentication module in a codebase that solely relies on Google as the Identity Provider for managing authentication. However, a new requirement has emerged to incorporate Amazon as an additional Identity Provider.

#### 👇 Current Code Structure

Imagine a service named `Authenticator` that primarily relies on Google as the identity provider, exemplified in the code snippet below.

#### **`Authenticator.ts`**
```TypeScript
export class Authenticator {

  public initiateUserAuth(){
    // Google specific operations    
    .
  }

  public logoutUser(){
    // Google specific operations    
    .
  }
  .
  .
}
```
#### ⭕ Tight Coupling found
In this scenario, the code exhibits tight coupling as the `Authenticator` service directly relies on the Google as the Identity Provider. To accommodate new Identity Providers, the code requires the introduction of if/else statements and as the number of Identity Providers grows, maintaining this code becomes increasingly complex and challenging. Now, it becomes evident that a significant restructuring is necessary to accommodate the integration of a new Identity Provider.

> Looks like an exhastive task? 🙂

#### 🍯 Desired Objective
The seamless integration of new Identity Providers should be accomplished without requiring any significant or even zero modifications to the `Authenticator` service.

#### 🔍 Analysis
By leveraging the **Dependency Inversion Principle**, we can decouple the `Authenticator` service from the specific Google implementation. This involves introducing an abstract class or interface as a protocol for all Identity Providers to adhere to. Such decoupling ensures that changes or additions to low-level modules, like Google or Amazon Identity Provider, won't directly affect the high-level `Authenticator` module, promoting modularity and maintainability in the codebase.

### Resolution
We require a substantial yet straightforward restructuring, outlined as follows:

1. Introduce a new module called IdentityProvider. Within this module, 
    - Create an interface named `IIdentityProvider` that encompasses essential methods and properties universally applicable to all Identity Providers in your system.
    - Create an enum `EIdentityProvider` for the Identity Provider types.

    #### **`IIdentityProvider.ts`**
    ```TypeScript
    export interface IIdentityProvider {
      fetchTokens(): string[],
      revokeTokens(): void
    }

    export enum EIdentityProvider {
      GOOGLE = 'GOOGLE',
      AMAZON = 'AMAZON'
    }
    ```
2. Create a new `GoogleIdentityProvider` class which implements the `IIdentityProvider` interface.
    #### **`GoogleIdentityProvider.ts`**
    ```TypeScript
    export class GoogleIdentityProvider implements IIdentityProvider{
        fetchTokens(): string[] {
           // google specific operations
        }
        revokeTokens(): void {
          // google specific operations
        }
    }
    ```
3. Similar to `GoogleIdentityProvider`, Create a new `AmazonIdentityProvider` class. 
    #### **`AmazonIdentityProvider.ts`**
    ```TypeScript
    export class AmazonIdentityProvider implements IIdentityProvider{
        fetchTokens(): string[]{
          // Amazon specific operations
        }
        revokeTokens(): void{
          // Amazon specific operations
        }
    }
    ```
4. Refactor the `Authenticator` class.
     - Introduce a new property called `identityProvider`, which stores the relevant Identity Provider instance based on the use case. Utilize this property to carry out the authentication process.

    #### _**`Authenticator.ts`**_
    ```TypeScript
    import {inject} from 'inversify';
    
    export class Authenticator {
      protected identityProvider: IIdentityProvider;

      constructor(identityProvider: IIdentityProvider) {
        this.identityProvider = identityProvider;
      }
    
      /* Authentication-related methods that are independent
         of any specific Identity Provider. */
      public initiaUserAuth(){
        .
        .
        return this.identityProvider.fetchTokens();
      }
    
      public logoutUser(){
        .
        .
        return this.identityProvider.revokeTokens();
      }
      .
      .
      .
    }
    ```
5. Configure multiple instances of the `Authenticator` class in the DI Container. Each instance is initialized using a specific `IdentityProvider` type. To accomplish this, we will leverage the service identification methods offered by your DI Container framework.

    See InversifyJS example below.

    #### **`AppContainer.ts`**
    ```TypeScript
    import {Container} from 'inversify';
    import {Authenticator, GoogleIdentityProvider, AmazonIdentityProvider, IdentityProvider} from '../services';
    
    export class AppContainer {
      private static container: Container;
    
      public static build(){
        const container = new Container();
    
        /* Initialize an instance of each Identity Provider 
           class. */
        container.bind<GoogleIdentityProvider('GoogleIdentityProvider')
          .to(GoogleIdentityProvider);
        container.bind<AmazonIdentityProvider>('AmazonIdentityProvider')
          .to(AmazonIdentityProvider);

        /* Initialize multiple Authenticator instances based on 
           each Identity Provider type. */
        container.bind<Authenticator>('Authenticator')
          .toDynamicValue(context => {
            return new Authenticator(context.container.get('GoogleIdentityProvider');
          })
          .whenTargetTagged('IdentityProvider', EIdentityProvider.GOOGLE);
        container.bind<Authenticator>('Authenticator')
          .toDynamicValue(context => {
            return new Authenticator(context.container.get('AmazonIdentityProvider);
          })
          .whenTargetTagged('IdentityProvider', EIdentityProvider.AMAZON);
      }
    }
    ```
6. Refactor the code, specifically in the sections where you want to load the suitable `Authenticator` instance based on the required Identity Provider type in the use case.
    
    Take a look at the provided InversifyJS examples in a hypothetical Authentication Controller scenario.

    #### **`AuthController.ts`**
    ```TypeScript
    import {AppContainer} from '../AppContainer.ts';
    import {Authenticator} from '../Authenticator.ts';

    export class AuthController{
      protected authenticator: Authenticator;

      @Post('/auth/google')
      public async initiateGoogleAuth(){

        // Loading Authenticator instance with GOOGLE Identity Provider
        this.authenticator = AppContainer.getTagged(
            'Authenticator',
            'IdentityProvider',
            EIdentityProvider.GOOGLE
        );
        .
        .
        .
      }

      @Post('/auth/amazon')
      public async initiateAmazonAuth(){

        // Loading Authenticator instance with AMAZON Identity Provider
        this.authenticator = AppContainer.getTagged(
            'Authenticator',
            'IdentityProvider',
            EIdentityProvider.AMAZON
        );
        .
        .
        .
      }
    }
    ```

---

## Default Implementation with Decorator Pattern
If 📜default behavior is necessary for your Identity Providers, considering an **abstract class** instead of an **interface** is a viable option. Moreover, if that default behavior serves as a valid form of an identity provider, it may be appropriate for the class to be **concrete**.
Other specific Identity Providers can inherit from this class, allowing them to modify or override the functionality as required. This particular scenario is a great application of the [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern).
- Create a component Interface.
- Create a default-identity-provider/component class which implements component interface.
- Create decorator/specific-identity-provider classes that inherit from the component class.

A good Decorator Pattern example [here](https://refactoring.guru/design-patterns/decorator/typescript/example).

----
I hope, this helped you:), happy learning🚀

**_refs_** [1](https://martinfowler.com/articles/dipInTheWild.html#YouMeanDependencyInversionRight), [2](https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/)