Achieving Loose Coupling with Dependency Injection

Achieving Loose Coupling with Dependency Injection

Takahiro Iwasa
Takahiro Iwasa
4 min read
Architecting

Introduction

This post explores how to create loosely coupled code using Dependency Injection. Originally written for junior developers, it highlights practical approaches to improve software design and maintainability.

What is Loose Coupling

Loose coupling refers to designing software components, such as classes or modules, so they interact through interfaces rather than being tightly interconnected. Tightly coupled components are harder to modify or test.

Example of Tight Coupling

The following example demonstrates an employee management feature written in 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});
  }
}

Problems with Tight Coupling

Tightly Coupled to the Salary Class

  • The line this.salary = new Salary(this.employeeId); directly couples the Employee and Salary classes.
  • Testing Employee#notify becomes challenging because it depends on the actual Salary#calculate method, making it harder to simulate different salary calculations or handle edge cases during testing.

Tightly Coupled to the System Clock

  • The line const hour = (new Date()).getHours(); couples the Employee class with the system clock.
  • Conditional logic testing for specific times becomes difficult.

Tightly Coupled to AWS SES

  • The line (new SES()).sendEmail(...) directly couples Employee with the AWS SES service.
  • Testing the notify method results in actual email sends, which may not be feasible in development.

Improving with Dependency Injection

Using Dependency Injection (DI), we can make the code more modular and testable.

Refactored Code

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});
  }
}

Key Improvements

  1. Reduced Coupling:

    • Components like Salary, SystemDate, and EmployeeSes are now injected via interfaces.
    • The Employee class is no longer directly dependent on specific implementations.
  2. Easier Testing:

    • Mock implementations of ISalary, ISystemDate, and IMailer can be used for testing.
    • System dependencies like clocks and SES services are decoupled.
  3. Dependency Inversion Principle:

    • High-level modules (Employee) are independent of low-level modules (Salary, Date, SES).

Conclusion

Dependency Injection promotes loose coupling and enables code to be more flexible, testable, and maintainable. By using DI, you can improve the adaptability of your codebase to future changes while ensuring robust testing practices.

Happy Coding! 🚀

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.