Dependency Injection for Maintainable, Testable Code

Dependency Injection for Maintainable, Testable Code

Refactoring a tightly coupled TypeScript class into one that's testable with mocks, using Dependency Injection for the salary calculator, the system clock, and SES.

Takahiro Iwasa
4 min read

Dependency Injection is one way to keep code loosely coupled and easier to test, illustrated below with a small TypeScript example.

Example of Tight Coupling

The following example demonstrates an employee management feature written in TypeScript. For simplicity, this example assumes that mocks cannot be dynamically configured.

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

The problems are:

  • 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.

Refactoring with Dependency Injection

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

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:

  • Reduced Coupling:
    • Components like Salary, SystemDate, and EmployeeSes are now injected via interfaces.
    • The Employee class is no longer directly dependent on specific implementations.
  • Easier Testing:
    • Mock implementations of ISalary, ISystemDate, and IMailer can be used for testing.
    • System dependencies like clocks and SES services are decoupled.
  • Dependency Inversion Principle:
    • High-level modules (Employee) are independent of low-level modules (Salary, Date, SES).

Conclusion

Refactoring the Employee class to accept ISalary, ISystemDate, and IMailer through its constructor and methods turned a class tightly coupled to Salary, the system clock, and SES into one that can be tested with mocks for all three. Introducing ISystemDate for something as ordinary as new Date() might look like overkill at first glance, but it’s exactly this kind of small, easy-to-miss dependency that makes time-sensitive logic hard to test reliably — without it, a test asserting on the “Good morning” branch only passes if it happens to run in the morning. The pattern scales the same way regardless of what’s being injected, whether it’s a class, a system dependency, or an external service like SES: define the interface at the point of use, and let the concrete implementation be swapped in by the caller rather than constructed inside.

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.