1. Dependency Injection Pattern là gì?

Dependency Injection (DI) là một mẫu thiết kế trong đó các đối tượng được cung cấp các dependency (phụ thuộc) của chúng thay vì tự tạo ra chúng. Pattern này giúp tăng tính module hóa, dễ test và bảo trì code bằng cách giảm sự phụ thuộc chặt chẽ giữa các thành phần.

Các thành phần chính trong Dependency Injection Pattern:

  • Service: Đối tượng cần được inject
  • Client: Đối tượng nhận dependency
  • Container: Quản lý việc tạo và inject các dependency
  • Interface: Định nghĩa cách client sử dụng service

2. Triển khai trong JavaScript

2.1 Ví dụ về User Service

// Interfaces (implicit in JavaScript)
class UserRepository {
  getUsers() {
    throw new Error("getUsers() phải được triển khai");
  }

  getUserById(id) {
    throw new Error("getUserById() phải được triển khai");
  }

  saveUser(user) {
    throw new Error("saveUser() phải được triển khai");
  }
}

class Logger {
  log(message) {
    throw new Error("log() phải được triển khai");
  }

  error(message) {
    throw new Error("error() phải được triển khai");
  }
}

// Concrete implementations
class MySQLUserRepository extends UserRepository {
  constructor(connection) {
    super();
    this.connection = connection;
  }

  getUsers() {
    console.log("Lấy danh sách users từ MySQL");
    return [
      { id: 1, name: "User 1" },
      { id: 2, name: "User 2" },
    ];
  }

  getUserById(id) {
    console.log(`Lấy user với id ${id} từ MySQL`);
    return { id, name: `User ${id}` };
  }

  saveUser(user) {
    console.log("Lưu user vào MySQL:", user);
    return true;
  }
}

class ConsoleLogger extends Logger {
  log(message) {
    console.log(`[INFO] ${message}`);
  }

  error(message) {
    console.error(`[ERROR] ${message}`);
  }
}

// Service that depends on UserRepository and Logger
class UserService {
  constructor(userRepository, logger) {
    this.userRepository = userRepository;
    this.logger = logger;
  }

  getAllUsers() {
    try {
      this.logger.log("Đang lấy danh sách users...");
      const users = this.userRepository.getUsers();
      this.logger.log(`Đã lấy ${users.length} users`);
      return users;
    } catch (error) {
      this.logger.error(`Lỗi khi lấy users: ${error.message}`);
      throw error;
    }
  }

  getUserById(id) {
    try {
      this.logger.log(`Đang lấy user với id ${id}...`);
      const user = this.userRepository.getUserById(id);
      this.logger.log(`Đã lấy user: ${JSON.stringify(user)}`);
      return user;
    } catch (error) {
      this.logger.error(`Lỗi khi lấy user ${id}: ${error.message}`);
      throw error;
    }
  }

  createUser(userData) {
    try {
      this.logger.log("Đang tạo user mới...");
      const success = this.userRepository.saveUser(userData);
      if (success) {
        this.logger.log("User đã được tạo thành công");
      }
      return success;
    } catch (error) {
      this.logger.error(`Lỗi khi tạo user: ${error.message}`);
      throw error;
    }
  }
}

// Simple DI Container
class Container {
  constructor() {
    this.services = new Map();
  }

  register(name, implementation) {
    this.services.set(name, implementation);
  }

  get(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service ${name} không tồn tại`);
    }
    return service;
  }
}

// Usage
const container = new Container();

// Register services
container.register("logger", new ConsoleLogger());
container.register(
  "userRepository",
  new MySQLUserRepository("mysql://localhost")
);
container.register(
  "userService",
  new UserService(container.get("userRepository"), container.get("logger"))
);

// Use the service
const userService = container.get("userService");

console.log("Lấy tất cả users:");
const users = userService.getAllUsers();

console.log("\nLấy user theo id:");
const user = userService.getUserById(1);

console.log("\nTạo user mới:");
userService.createUser({ name: "New User" });

2.2 Ví dụ về Notification System

// Interfaces
class NotificationSender {
  send(message, recipient) {
    throw new Error("send() phải được triển khai");
  }
}

class NotificationFormatter {
  format(message, type) {
    throw new Error("format() phải được triển khai");
  }
}

// Concrete implementations
class EmailSender extends NotificationSender {
  constructor(smtpConfig) {
    super();
    this.smtpConfig = smtpConfig;
  }

  send(message, recipient) {
    console.log(`Gửi email đến ${recipient}:`, message);
    return true;
  }
}

class SMSSender extends NotificationSender {
  constructor(smsGateway) {
    super();
    this.smsGateway = smsGateway;
  }

  send(message, recipient) {
    console.log(`Gửi SMS đến ${recipient}:`, message);
    return true;
  }
}

class HTMLFormatter extends NotificationFormatter {
  format(message, type) {
    return `<div class="notification ${type}">${message}</div>`;
  }
}

class PlainTextFormatter extends NotificationFormatter {
  format(message, type) {
    return `[${type.toUpperCase()}] ${message}`;
  }
}

// Service using dependency injection
class NotificationService {
  constructor(sender, formatter) {
    this.sender = sender;
    this.formatter = formatter;
  }

  notify(message, recipient, type = "info") {
    const formattedMessage = this.formatter.format(message, type);
    return this.sender.send(formattedMessage, recipient);
  }

  notifyMany(message, recipients, type = "info") {
    return recipients.map((recipient) => this.notify(message, recipient, type));
  }
}

// Decorators for notification senders
class RetryDecorator extends NotificationSender {
  constructor(sender, maxRetries = 3) {
    super();
    this.sender = sender;
    this.maxRetries = maxRetries;
  }

  async send(message, recipient) {
    for (let i = 0; i < this.maxRetries; i++) {
      try {
        return await this.sender.send(message, recipient);
      } catch (error) {
        if (i === this.maxRetries - 1) throw error;
        console.log(`Retry ${i + 1}/${this.maxRetries}`);
      }
    }
  }
}

// Factory for creating notification services
class NotificationServiceFactory {
  static createEmailService(smtpConfig, useHTML = true) {
    const sender = new RetryDecorator(new EmailSender(smtpConfig));
    const formatter = useHTML ? new HTMLFormatter() : new PlainTextFormatter();
    return new NotificationService(sender, formatter);
  }

  static createSMSService(smsGateway) {
    const sender = new RetryDecorator(new SMSSender(smsGateway));
    const formatter = new PlainTextFormatter();
    return new NotificationService(sender, formatter);
  }
}

// Usage
const emailService = NotificationServiceFactory.createEmailService({
  host: "smtp.example.com",
  port: 587,
});

const smsService = NotificationServiceFactory.createSMSService({
  apiKey: "your-api-key",
});

// Send notifications
console.log("Gửi email:");
emailService.notify(
  "Chào mừng bạn đến với hệ thống!",
  "[email protected]",
  "welcome"
);

console.log("\nGửi SMS:");
smsService.notifyMany(
  "Khuyến mãi đặc biệt!",
  ["0123456789", "0987654321"],
  "promotion"
);

3. Triển khai trong TypeScript

3.1 Ví dụ về Authentication System

// Interfaces
interface IUserRepository {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

interface IPasswordHasher {
  hash(password: string): Promise<string>;
  compare(password: string, hash: string): Promise<boolean>;
}

interface ITokenGenerator {
  generate(payload: object): string;
  verify(token: string): object;
}

// Types
interface User {
  id: string;
  email: string;
  password: string;
  roles: string[];
}

interface LoginResult {
  user: Omit<User, "password">;
  token: string;
}

// Implementations
class MongoUserRepository implements IUserRepository {
  constructor(private connection: any) {}

  async findByEmail(email: string): Promise<User | null> {
    console.log(`Tìm user với email ${email} từ MongoDB`);
    return {
      id: "1",
      email,
      password: "hashed_password",
      roles: ["user"],
    };
  }

  async save(user: User): Promise<void> {
    console.log("Lưu user vào MongoDB:", user);
  }
}

class BCryptPasswordHasher implements IPasswordHasher {
  constructor(private rounds: number = 10) {}

  async hash(password: string): Promise<string> {
    console.log("Mã hóa mật khẩu với BCrypt");
    return `hashed_${password}`;
  }

  async compare(password: string, hash: string): Promise<boolean> {
    console.log("So sánh mật khẩu với BCrypt");
    return hash === `hashed_${password}`;
  }
}

class JWTTokenGenerator implements ITokenGenerator {
  constructor(private secret: string) {}

  generate(payload: object): string {
    console.log("Tạo JWT token với payload:", payload);
    return "jwt_token";
  }

  verify(token: string): object {
    console.log("Xác thực JWT token:", token);
    return { userId: "1", roles: ["user"] };
  }
}

// Service
class AuthenticationService {
  constructor(
    private userRepository: IUserRepository,
    private passwordHasher: IPasswordHasher,
    private tokenGenerator: ITokenGenerator
  ) {}

  async register(email: string, password: string): Promise<User> {
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new Error("Email đã tồn tại");
    }

    const hashedPassword = await this.passwordHasher.hash(password);
    const user: User = {
      id: Date.now().toString(),
      email,
      password: hashedPassword,
      roles: ["user"],
    };

    await this.userRepository.save(user);
    return user;
  }

  async login(email: string, password: string): Promise<LoginResult> {
    const user = await this.userRepository.findByEmail(email);
    if (!user) {
      throw new Error("User không tồn tại");
    }

    const isValid = await this.passwordHasher.compare(password, user.password);
    if (!isValid) {
      throw new Error("Mật khẩu không đúng");
    }

    const token = this.tokenGenerator.generate({
      userId: user.id,
      roles: user.roles,
    });

    const { password: _, ...userWithoutPassword } = user;
    return {
      user: userWithoutPassword,
      token,
    };
  }

  verifyToken(token: string): object {
    return this.tokenGenerator.verify(token);
  }
}

// DI Container
class Container {
  private services: Map<string, any> = new Map();

  register<T>(name: string, implementation: T): void {
    this.services.set(name, implementation);
  }

  get<T>(name: string): T {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service ${name} không tồn tại`);
    }
    return service;
  }
}

// Usage
const container = new Container();

// Register services
container.register(
  "userRepository",
  new MongoUserRepository("mongodb://localhost")
);
container.register("passwordHasher", new BCryptPasswordHasher(12));
container.register("tokenGenerator", new JWTTokenGenerator("your-secret-key"));
container.register(
  "authService",
  new AuthenticationService(
    container.get("userRepository"),
    container.get("passwordHasher"),
    container.get("tokenGenerator")
  )
);

// Use the service
async function demo() {
  const authService = container.get<AuthenticationService>("authService");

  console.log("Đăng ký user mới:");
  const user = await authService.register("[email protected]", "password123");
  console.log("User đã đăng ký:", user);

  console.log("\nĐăng nhập:");
  const loginResult = await authService.login(
    "[email protected]",
    "password123"
  );
  console.log("Kết quả đăng nhập:", loginResult);

  console.log("\nXác thực token:");
  const tokenPayload = authService.verifyToken(loginResult.token);
  console.log("Token payload:", tokenPayload);
}

demo().catch(console.error);

3.2 Ví dụ về Payment Processing

// Interfaces
interface IPaymentGateway {
  processPayment(amount: number, currency: string): Promise<PaymentResult>;
  refundPayment(paymentId: string): Promise<boolean>;
}

interface IPaymentValidator {
  validate(amount: number, currency: string): boolean;
}

interface IPaymentLogger {
  logPayment(payment: Payment): void;
  logError(error: Error): void;
}

// Types
interface Payment {
  id: string;
  amount: number;
  currency: string;
  status: PaymentStatus;
  timestamp: Date;
}

interface PaymentResult {
  success: boolean;
  paymentId: string;
  error?: string;
}

enum PaymentStatus {
  PENDING = "PENDING",
  COMPLETED = "COMPLETED",
  FAILED = "FAILED",
  REFUNDED = "REFUNDED",
}

// Implementations
class StripeGateway implements IPaymentGateway {
  constructor(private apiKey: string) {}

  async processPayment(
    amount: number,
    currency: string
  ): Promise<PaymentResult> {
    console.log(`Xử lý thanh toán ${amount} ${currency} qua Stripe`);
    return {
      success: true,
      paymentId: `stripe_${Date.now()}`,
    };
  }

  async refundPayment(paymentId: string): Promise<boolean> {
    console.log(`Hoàn tiền cho thanh toán ${paymentId} qua Stripe`);
    return true;
  }
}

class PaymentValidator implements IPaymentValidator {
  validate(amount: number, currency: string): boolean {
    if (amount <= 0) {
      throw new Error("Số tiền phải lớn hơn 0");
    }
    if (!["USD", "EUR", "VND"].includes(currency)) {
      throw new Error("Loại tiền tệ không được hỗ trợ");
    }
    return true;
  }
}

class DatabaseLogger implements IPaymentLogger {
  constructor(private dbConnection: any) {}

  logPayment(payment: Payment): void {
    console.log("Lưu log thanh toán vào database:", payment);
  }

  logError(error: Error): void {
    console.error("Lưu log lỗi vào database:", error.message);
  }
}

// Service
class PaymentService {
  constructor(
    private gateway: IPaymentGateway,
    private validator: IPaymentValidator,
    private logger: IPaymentLogger
  ) {}

  async processPayment(amount: number, currency: string): Promise<Payment> {
    try {
      this.validator.validate(amount, currency);

      const payment: Payment = {
        id: `payment_${Date.now()}`,
        amount,
        currency,
        status: PaymentStatus.PENDING,
        timestamp: new Date(),
      };

      const result = await this.gateway.processPayment(amount, currency);

      if (result.success) {
        payment.status = PaymentStatus.COMPLETED;
      } else {
        payment.status = PaymentStatus.FAILED;
      }

      this.logger.logPayment(payment);
      return payment;
    } catch (error) {
      this.logger.logError(error as Error);
      throw error;
    }
  }

  async refundPayment(paymentId: string): Promise<boolean> {
    try {
      const success = await this.gateway.refundPayment(paymentId);

      if (success) {
        const refundPayment: Payment = {
          id: `refund_${paymentId}`,
          amount: 0, // Số tiền hoàn lại sẽ được lấy từ payment gốc
          currency: "USD",
          status: PaymentStatus.REFUNDED,
          timestamp: new Date(),
        };

        this.logger.logPayment(refundPayment);
      }

      return success;
    } catch (error) {
      this.logger.logError(error as Error);
      throw error;
    }
  }
}

// Usage
const paymentService = new PaymentService(
  new StripeGateway("stripe-api-key"),
  new PaymentValidator(),
  new DatabaseLogger("db-connection")
);

async function demo() {
  console.log("Xử lý thanh toán:");
  const payment = await paymentService.processPayment(100, "USD");
  console.log("Kết quả thanh toán:", payment);

  console.log("\nHoàn tiền:");
  const refunded = await paymentService.refundPayment(payment.id);
  console.log("Kết quả hoàn tiền:", refunded);
}

demo().catch(console.error);

3.5 DI trong hệ sinh thái JS/TS 2025 — bản khảo sát nhanh

Viết container tay (mục 2, 3) giúp hiểu cơ chế, nhưng dự án thực tế hầu như dùng framework/lib có sẵn. Dưới đây là các lựa chọn phổ biến nhất và điểm mạnh.

3.5.1 NestJS — DI framework-level cho Node backend

Inspired từ Angular, Nest tích hợp DI với module system, middleware, GraphQL…

import { Injectable, Module, Controller, Get, Inject } from "@nestjs/common";

@Injectable()
class UserRepository {
  findAll() {
    return [{ id: 1, name: "Alice" }];
  }
}

@Injectable()
class UserService {
  constructor(private readonly repo: UserRepository) {} // auto-injected
  list() {
    return this.repo.findAll();
  }
}

@Controller("users")
class UserController {
  constructor(private readonly users: UserService) {}
  @Get() index() {
    return this.users.list();
  }
}

@Module({
  controllers: [UserController],
  providers: [UserService, UserRepository],
})
class AppModule {}

3.5.2 tsyringe — DI container nhẹ của Microsoft

Hợp cho TS/Node, API ngắn, chỉ cần reflect-metadata:

import "reflect-metadata";
import { container, injectable, inject } from "tsyringe";

@injectable()
class HttpClient {
  get(url: string) {
    return fetch(url).then((r) => r.json());
  }
}

@injectable()
class UserApi {
  constructor(@inject(HttpClient) private http: HttpClient) {}
  me() {
    return this.http.get("/api/me");
  }
}

const api = container.resolve(UserApi);

3.5.3 InversifyJS — container DI mature, type-rich

Được dùng rộng rãi trước khi Nest ra đời. Mạnh về feature (scopes, middleware, multi-inject) nhưng verbose hơn:

import "reflect-metadata";
import { Container, injectable, inject } from "inversify";

const TYPES = { Logger: Symbol("Logger"), Service: Symbol("Service") };

@injectable()
class Logger {
  log(m: string) {
    console.log(m);
  }
}

@injectable()
class Service {
  constructor(@inject(TYPES.Logger) private logger: Logger) {}
  run() {
    this.logger.log("running");
  }
}

const c = new Container();
c.bind<Logger>(TYPES.Logger).to(Logger).inSingletonScope();
c.bind<Service>(TYPES.Service).to(Service);

c.get<Service>(TYPES.Service).run();

3.5.4 Awilix — DI không cần decorator

Awilix dùng function-based resolution → chạy ngon cả plain JS, Fastify, Koa, Express. Không cần reflect-metadata:

import { createContainer, asClass, Lifetime, InjectionMode } from "awilix";

class UserRepository {
  /* ... */
}
class UserService {
  constructor({ userRepository }: { userRepository: UserRepository }) {
    this.repo = userRepository;
  }
  repo: UserRepository;
}

const container = createContainer({ injectionMode: InjectionMode.PROXY });
container.register({
  userRepository: asClass(UserRepository).singleton(),
  userService: asClass(UserService).scoped(),
});

const service = container.resolve<UserService>("userService");

3.5.5 React / Vue — DI theo “Context + hooks”

Không có DI container chính thức; cộng đồng đồng thuận dùng Context provider làm container, custom hook làm resolver:

// React
import { createContext, useContext, ReactNode } from "react";

interface Services {
  logger: { log: (m: string) => void };
  api: { get: (u: string) => Promise<unknown> };
}

const ServicesContext = createContext<Services | null>(null);

export function ServicesProvider({ value, children }: { value: Services; children: ReactNode }) {
  return <ServicesContext.Provider value={value}>{children}</ServicesContext.Provider>;
}

export function useServices() {
  const s = useContext(ServicesContext);
  if (!s) throw new Error("useServices outside ServicesProvider");
  return s;
}

Vue 3 có API chuyên biệt cho DI: provide() / inject():

// Root component
import { provide } from "vue";
provide("logger", { log: (m: string) => console.log(m) });

// Child
import { inject } from "vue";
const logger = inject<{ log(m: string): void }>("logger")!;

3.5.6 Request-scoped DI với AsyncLocalStorage (Node)

Trong backend, một số dependency cần scope theo request (user hiện tại, trace-id, transaction DB). Node core có AsyncLocalStorage (từ Node 14+, stable Node 16+) giữ state xuyên qua async/await mà không phải truyền context:

import { AsyncLocalStorage } from "node:async_hooks";

interface RequestCtx {
  requestId: string;
  userId?: string;
}

export const ctxStorage = new AsyncLocalStorage<RequestCtx>();

// middleware
export function requestContextMw(req, res, next) {
  ctxStorage.run({ requestId: crypto.randomUUID() }, next);
}

// đâu đó sâu trong service, không cần truyền req
export function logger(msg: string) {
  const ctx = ctxStorage.getStore();
  console.log(`[${ctx?.requestId}] ${msg}`);
}

Kết hợp với container tay: lưu request-scoped container trong AsyncLocalStorage, resolve() tự lấy đúng scope.

3.5.7 Bảng quyết định nhanh

StackLựa chọn gợi ý
Backend Node lớn, nhiều moduleNestJS
Backend Node gọn, ít frameworktsyringe hoặc Awilix
Backend Node không dùng decoratorAwilix
Frontend ReactContext + hooks (hoặc không DI gì cả)
Frontend Vueprovide/inject
Frontend AngularDI built-in (không cần thêm lib)
Request-scoped data (Node)+ AsyncLocalStorage

4. Ưu điểm và Nhược điểm

4.1 Ưu điểm

  1. Tách biệt dependencies: Giảm sự phụ thuộc giữa các thành phần
  2. Dễ test: Dễ dàng mock các dependencies trong testing
  3. Linh hoạt: Dễ dàng thay đổi implementation mà không ảnh hưởng code
  4. Tái sử dụng: Tăng khả năng tái sử dụng code

4.2 Nhược điểm

  1. Phức tạp: Có thể phức tạp hóa code với nhiều dependencies
  2. Khó debug: Khó debug khi có nhiều layer của injection
  3. Overhead: Tăng thời gian khởi tạo và tài nguyên
  4. Học tập: Đòi hỏi thời gian học và hiểu pattern

5. Khi nào nên sử dụng Dependency Injection Pattern?

  1. Ứng dụng lớn: Khi làm việc với ứng dụng có nhiều thành phần
  2. Testing: Khi cần viết unit test với mock dependencies
  3. Thay đổi implementation: Khi cần thay đổi implementation thường xuyên
  4. Tái sử dụng: Khi muốn tăng khả năng tái sử dụng code
  5. Quản lý phụ thuộc: Khi cần quản lý phụ thuộc một cách hiệu quả

6. Kết luận

Dependency Injection Pattern là một mẫu thiết kế mạnh mẽ giúp quản lý sự phụ thuộc giữa các thành phần trong ứng dụng. Pattern này đặc biệt hữu ích trong JavaScript / TypeScript khi làm việc với các ứng dụng lớn và cần tính module hóa cao. Tuy nhiên, cần cân nhắc về tính phức tạp và overhead khi sử dụng pattern này.