Making Your Codes Loosely Coupled using Dependency Injection

Making Your Codes Loosely Coupled using Dependency Injection

Takahiro Iwasa
Takahiro Iwasa
4 min read
Architecting

This post describes how to make your codes loosely coupled using Dependency Injection. This was originally written for junior developers in my workplace.

Loose Coupling

Software components, such as classes, modules, libraries, cloud platforms and so on, should be loosely coupled together through interfaces. Tightly coupled components are often difficult to be changed or tested.

Example of Tight Coupling

The example here is an employee management feature using TypeScript.

export class Salary {
  readonly employeeId: number;

  constructor(employeeId: number) {
    this.employeeId = employeeId;
  }

  calculate(): number {
    let salary = 0;
    // ...
    salary = 200000;
    return salary;
  }
}

export class Employee {
  private employeeId: number;
  private name: string;
  private salary: Salary;

  constructor(employeeId: number, name: string) {
    this.employeeId = employeeId;
    this.name = name;
    this.salary = new Salary(this.employeeId);
  }

  // Send an email by Amazon SES. Message text depends on time.
  notify(): void {
    const hour = (new Date()).getHours();
    let title = `Hi ${this.name}`;
    const body = `Current Salary: ${this.salary.calculate()}`;

    if (6 <= hour && hour <= 9) {
      title = `Good morning ${this.name}`;
    } else if (10 <= hour && hour <= 18) {
      title = `How's it going, ${this.name}?`;
    }
    (new SES()).sendEmail({title: title, body: body});
  }
}

Analysis

The sample code above uses the new keyword to create instances, leading to tight coupling between the components.

Tightly Coupled with Salary Class

The line this.salary = new Salary(this.employeeId); creates tight coupling between Employee class and the Salary class. In Employee#notify, testing Salary#calculate in case of returning undefined should be difficult.

Tightly Coupled with System Clock

The line const hour = (new Date()).getHours(); creates tight coupling between Employee class and the system clock. Testing if in Employee#notify should be difficult.

Tightly Coupled with AWS

The line (new SES()).sendEmail({title: title, body: body}); creates tight coupling between Employee class and the Amazon SES service. Testing in Employee#notify will always send actual emails. In addition, you may not be able to use AWS in your development environment.

Decomposing Approach

One of the solutions to decompose tightly coupled components is to use dependency injection.

Example of Loose Coupling

Let’s decompose the example codes using dependency injection.

export interface ISalary {
  readonly employeeId: number;
  calculate(): number;
}

export interface ISystemDate {
  now(): Date;
}

export interface IMailer {
  send(config: any): void;
}

export class Salary implements ISalary {
  readonly employeeId: number;

  constructor(employeeId: number) {
    this.employeeId = employeeId;
  }

  calculate(): number {
    let salary = 0;
    // ...
    salary = 200000;
    return salary;
  }
}

export class SystemDate implements ISystemDate {
  now(): Date {
    return new Date();
  }
}

export class EmployeeSes implements IMailer {
  send(config: any): void {
    (new SES()).sendEmail(config);
  }
}

export class Employee {
  private employeeId: number;
  private name: string;
  private salary: ISalary;

  constructor(employeeId: number, name: string, salary: ISalary) {
    this.employeeId = employeeId;
    this.name = name;
    this.salary = salary;
  }

  // Send an email by Amazon SES. Message text depends on time.
  notify(systemDate: ISystemDate, mailer: IMailer): void {
    const hour = systemDate.now().getHours();
    let title = `Hi ${this.name}`;
    const body = `Current Salary: ${this.salary.calculate()}`;

    if (6 <= hour && hour <= 9) {
      title = `Good morning ${this.name}`;
    } else if (10 <= hour && hour <= 18) {
      title = `How's it going, ${this.name}?`;
    }
    mailer.send({title: title, body: body});
  }
}

Using dependency injection, we were able to make the code loosely coupled. Furthermore, it is important that dependency direction has been inverted, meaning that Employee class does not depend directly on Salary, Date and SES classes. This approach is commonly known as the Dependency Inversion Principle (DIP).

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.