Dependency Injection trong TypeScript

Dependency Injection trong TypeScript

Giới thiệu

Trong nguyên tắc thiết kế phần mềm SOLID, chữ “D” đại diện cho Dependency Inversion Principle (DIP). Quy tắc này giúp tách biệt các module để giảm sự phụ thuộc giữa chúng, mang lại tính linh hoạt cao hơn trong việc mở rộng và bảo trì hệ thống. Một trong những kỹ thuật để thực hiện DIP là Dependency Injection (DI).


Dependencies là gì?

Dependency là bất kỳ module hoặc thành phần nào mà chương trình của bạn sử dụng để thực hiện các chức năng cụ thể. Ví dụ:

const getRandomInRange = (min: number, max: number): number => Math.random() * (max - min) + min;

Hàm getRandomInRange phụ thuộc vào:

  • Các tham số min và max.
  • Hàm Math.random.

Nếu Math.random không hoạt động, getRandomInRange cũng không thể chạy đúng. Do đó, Math.random là một dependency.

Truyền Dependency qua tham số

Bạn có thể truyền Math.random vào dưới dạng một dependency:

const getRandomInRange = (
  min: number,
  max: number,
  random: () => number
): number => random() * (max - min) + min;

const result = getRandomInRange(1, 10, Math.random);

Để tránh việc phải truyền Math.random mỗi lần gọi hàm, bạn có thể đặt giá trị mặc định:

const getRandomInRange = (
  min: number,
  max: number,
  random: () => number = Math.random
): number => random() * (max - min) + min;

Đây là một cách triển khai cơ bản của Dependency Injection, trong đó bạn cung cấp tất cả các dependency mà module cần từ bên ngoài.


Tại sao Dependency Injection lại cần thiết?

  1. Testability

Khi dependency được định nghĩa rõ ràng, việc kiểm thử trở nên dễ dàng hơn:

Thay thế dependency bằng mock: Giúp kiểm soát đầu vào và kết quả.

Ví dụ: Thay Math.random bằng một mock:

const mockRandom = () => 0.1;
const result = getRandomInRange(1, 10, mockRandom);

expect(result).toBe(1); // true
  1. Dễ thay thế dependency

Bạn có thể thay thế dependency bằng một phiên bản khác mà không làm thay đổi logic của module:

const otherRandom = (): number => {
  // Implementation khác của random
  return 0.5;
};

const result = getRandomInRange(1, 10, otherRandom);

Sử dụng TypeScript giúp đảm bảo rằng dependency mới tuân thủ cùng một interface hoặc kiểu, giảm thiểu lỗi khi thay thế.


Dependency Injection trong TypeScript

Dependency Injection không chỉ giới hạn ở hàm, mà còn áp dụng cho các lớp (class).

Ví dụ: Class Counter

Hãy xem một lớp Counter có thể tăng, giảm và ghi log trạng thái hiện tại:

class Counter {
  public state: number = 0;

  public increase(): void {
    this.state += 1;
    console.log(`State increased. Current state is ${this.state}.`);
  }

  public decrease(): void {
    this.state -= 1;
    console.log(`State decreased. Current state is ${this.state}.`);
  }
}

Lớp này phụ thuộc vào console để ghi log. Thay vì sử dụng console trực tiếp, ta có thể inject nó như một dependency:

interface Logger {
  log(message: string): void;
}

class Counter {
  constructor(private logger: Logger) {}

  public state: number = 0;

  public increase(): void {
    this.state += 1;
    this.logger.log(`State increased. Current state is ${this.state}.`);
  }

  public decrease(): void {
    this.state -= 1;
    this.logger.log(`State decreased. Current state is ${this.state}.`);
  }
}

Khi khởi tạo Counter, bạn cần cung cấp một dependency phù hợp:

const counter = new Counter(console);

// Hoặc thay thế bằng module khác
const alertLogger: Logger = {
  log: (message: string): void => {
    alert(message);
  },
};

const counterWithAlert = new Counter(alertLogger);

Dependency Injection Containers

Mặc dù việc inject thủ công hoạt động tốt với ít dependency, nhưng khi hệ thống trở nên phức tạp, việc quản lý các dependency sẽ khó khăn. Lúc này, ta sử dụng DI Container.

DI Container là gì?

DI Container tự động tạo và cung cấp các dependency cần thiết cho module, giúp giảm công sức cấu hình và duy trì thứ tự injection.

Cài đặt DI Container với brandi

  1. Tạo interface và implementation:
// Logger.ts
export interface Logger {
  log(message: string): void;
}

export class ConsoleLogger implements Logger {
  public log(message: string): void {
    console.log(message);
  }
}
  1. Định nghĩa Tokens:

Tokens giúp ánh xạ dependency với implementation của nó.

// tokens.ts
import { token } from 'brandi';

export const TOKENS = {
  logger: token<Logger>('logger'),
  counter: token<Counter>('counter'),
};
  1. Inject dependency vào class:
// Counter.ts
import { injected } from 'brandi';
import { TOKENS } from './tokens';
import { Logger } from './Logger';

export class Counter {
  constructor(private logger: Logger) {}

  public increase(): void {
    this.logger.log('Increased');
  }
}

injected(Counter, TOKENS.logger);
  1. Cấu hình container:
// container.ts
import { Container } from 'brandi';
import { TOKENS } from './tokens';
import { ConsoleLogger } from './Logger';
import { Counter } from './Counter';

const container = new Container();

container.bind(TOKENS.logger).toInstance(ConsoleLogger).inTransientScope();
container.bind(TOKENS.counter).toInstance(Counter).inTransientScope();

export { container };
  1. Sử dụng DI Container:
// index.ts
import { TOKENS } from './tokens';
import { container } from './container';

const counter = container.get(TOKENS.counter);

counter.increase();

Lợi ích của DI Container

  1. Dễ thay đổi implementation:

Bạn có thể thay đổi implementation trên toàn bộ hệ thống chỉ bằng cách thay đổi một dòng cấu hình trong container.

class AlertLogger implements Logger {
  public log(message: string): void {
    alert(message);
  }
}

container.bind(TOKENS.logger).toInstance(AlertLogger).inTransientScope();
  1. Quản lý dependency phức tạp:

DI Container tự động xử lý thứ tự injection và giảm thiểu lỗi cấu hình thủ công.

  1. Tăng tính linh hoạt:

Các module ít phụ thuộc vào nhau hơn, dễ kiểm thử và bảo trì.


Kết luận

Dependency Injection là một kỹ thuật mạnh mẽ giúp giảm sự phụ thuộc giữa các module, cải thiện khả năng kiểm thử và tính linh hoạt của hệ thống. Tuy nhiên, việc áp dụng DI đi kèm với việc viết thêm mã cấu hình, do đó chỉ nên sử dụng khi hệ thống đủ phức tạp hoặc cần quản lý nhiều dependency. DI không chỉ là một công cụ mà còn là một tư duy thiết kế quan trọng trong phát triển phần mềm hiện đại.