Tiếng Việt

Khám phá sức mạnh của Decorator trong TypeScript cho lập trình metadata, lập trình hướng khía cạnh và nâng cao mã nguồn với các mẫu khai báo. Hướng dẫn toàn diện cho lập trình viên toàn cầu.

Decorator trong TypeScript: Làm chủ các Mẫu Lập trình Metadata cho Ứng dụng Bền vững

Trong bối cảnh phát triển phần mềm hiện đại rộng lớn, việc duy trì codebase sạch sẽ, có khả năng mở rộng và dễ quản lý là tối quan trọng. TypeScript, với hệ thống kiểu mạnh mẽ và các tính năng nâng cao, cung cấp cho các nhà phát triển công cụ để đạt được điều này. Trong số các tính năng hấp dẫn và mang tính biến đổi nhất của nó là Decorator. Mặc dù vẫn là một tính năng thử nghiệm tại thời điểm viết bài (đề xuất Giai đoạn 3 cho ECMAScript), decorator được sử dụng rộng rãi trong các framework như Angular và TypeORM, thay đổi cơ bản cách chúng ta tiếp cận các mẫu thiết kế, lập trình metadata và lập trình hướng khía cạnh (AOP).

Hướng dẫn toàn diện này sẽ đi sâu vào decorator trong TypeScript, khám phá cơ chế hoạt động, các loại khác nhau, ứng dụng thực tế và các phương pháp hay nhất. Dù bạn đang xây dựng các ứng dụng doanh nghiệp quy mô lớn, microservices, hay giao diện web phía client, việc hiểu rõ decorator sẽ giúp bạn viết mã TypeScript mang tính khai báo, dễ bảo trì và mạnh mẽ hơn.

Hiểu Khái niệm Cốt lõi: Decorator là gì?

Về cơ bản, decorator là một loại khai báo đặc biệt có thể được đính kèm vào một khai báo lớp, phương thức, accessor, thuộc tính hoặc tham số. Decorator là các hàm trả về một giá trị mới (hoặc sửa đổi một giá trị hiện có) cho mục tiêu mà chúng đang trang trí. Mục đích chính của chúng là thêm metadata hoặc thay đổi hành vi của khai báo mà chúng được đính kèm, mà không cần sửa đổi trực tiếp cấu trúc mã nguồn cơ bản. Cách bổ sung mã nguồn từ bên ngoài và mang tính khai báo này cực kỳ mạnh mẽ.

Hãy nghĩ về decorator như những chú thích hoặc nhãn mà bạn áp dụng cho các phần của mã nguồn. Những nhãn này sau đó có thể được đọc hoặc xử lý bởi các phần khác của ứng dụng hoặc bởi các framework, thường là lúc chạy (runtime), để cung cấp thêm chức năng hoặc cấu hình.

Cú pháp của một Decorator

Decorator được bắt đầu bằng ký hiệu @, theo sau là tên của hàm decorator. Chúng được đặt ngay trước khai báo mà chúng trang trí.

@MyDecorator
class MyClass {
  @AnotherDecorator
  myMethod() {
    // ...
  }
}

Kích hoạt Decorator trong TypeScript

Trước khi có thể sử dụng decorator, bạn phải kích hoạt tùy chọn trình biên dịch experimentalDecorators trong tệp tsconfig.json của mình. Ngoài ra, để có các khả năng phản chiếu metadata nâng cao (thường được các framework sử dụng), bạn cũng sẽ cần emitDecoratorMetadata và polyfill reflect-metadata.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Bạn cũng cần cài đặt reflect-metadata:

npm install reflect-metadata --save
# hoặc
yarn add reflect-metadata

Và import nó ở ngay đầu điểm vào ứng dụng của bạn (ví dụ: main.ts hoặc app.ts):

import "reflect-metadata";
// Mã ứng dụng của bạn bắt đầu từ đây

Decorator Factory: Tùy chỉnh trong tầm tay bạn

Trong khi một decorator cơ bản là một hàm, thường thì bạn sẽ cần truyền các đối số cho một decorator để cấu hình hành vi của nó. Điều này được thực hiện bằng cách sử dụng một decorator factory. Một decorator factory là một hàm trả về hàm decorator thực tế. Khi bạn áp dụng một decorator factory, bạn gọi nó với các đối số của nó, và sau đó nó trả về hàm decorator mà TypeScript sẽ áp dụng vào mã của bạn.

Tạo một ví dụ Decorator Factory đơn giản

Hãy tạo một factory cho decorator Logger có thể ghi log các thông điệp với các tiền tố khác nhau.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Lớp ${target.name} đã được định nghĩa.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Ứng dụng đang khởi động...");
  }
}

const app = new ApplicationBootstrap();
// Kết quả:
// [APP_INIT] Lớp ApplicationBootstrap đã được định nghĩa.
// Ứng dụng đang khởi động...

Trong ví dụ này, Logger("APP_INIT") là lời gọi decorator factory. Nó trả về hàm decorator thực tế nhận target: Function (hàm khởi tạo của lớp) làm đối số. Điều này cho phép cấu hình động hành vi của decorator.

Các loại Decorator trong TypeScript

TypeScript hỗ trợ năm loại decorator riêng biệt, mỗi loại áp dụng cho một loại khai báo cụ thể. Chữ ký của hàm decorator thay đổi tùy thuộc vào ngữ cảnh mà nó được áp dụng.

1. Class Decorator

Class decorator được áp dụng cho các khai báo lớp. Hàm decorator nhận hàm khởi tạo (constructor) của lớp làm đối số duy nhất. Một class decorator có thể quan sát, sửa đổi, hoặc thậm chí thay thế định nghĩa của một lớp.

Chữ ký:

function ClassDecorator(target: Function) { ... }

Giá trị trả về:

Nếu class decorator trả về một giá trị, nó sẽ thay thế khai báo lớp bằng hàm khởi tạo được cung cấp. Đây là một tính năng mạnh mẽ, thường được sử dụng cho mixin hoặc bổ sung lớp. Nếu không có giá trị nào được trả về, lớp ban đầu sẽ được sử dụng.

Trường hợp sử dụng:

Ví dụ Class Decorator: Tiêm một Service

Hãy tưởng tượng một kịch bản dependency injection đơn giản nơi bạn muốn đánh dấu một lớp là "injectable" và tùy chọn cung cấp một tên cho nó trong một container.

const InjectableServiceRegistry = new Map<string, Function>();

function Injectable(name?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceName = name || constructor.name;
    InjectableServiceRegistry.set(serviceName, constructor);
    console.log(`Đã đăng ký service: ${serviceName}`);

    // Tùy chọn, bạn có thể trả về một lớp mới ở đây để bổ sung hành vi
    return class extends constructor {
      createdAt = new Date();
      // Các thuộc tính hoặc phương thức bổ sung cho tất cả các service được tiêm
    };
  };
}

@Injectable("UserService")
class UserDataService {
  getUsers() {
    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
  }
}

@Injectable()
class ProductDataService {
  getProducts() {
    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
  }
}

console.log("--- Các Service đã được đăng ký ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Người dùng:", userServiceInstance.getUsers());
  // console.log("User Service Created At:", userServiceInstance.createdAt); // Nếu lớp trả về được sử dụng
}

Ví dụ này minh họa cách một class decorator có thể đăng ký một lớp và thậm chí sửa đổi hàm khởi tạo của nó. Decorator Injectable làm cho lớp có thể được phát hiện bởi một hệ thống dependency injection giả định.

2. Method Decorator

Method decorator được áp dụng cho các khai báo phương thức. Chúng nhận ba đối số: đối tượng mục tiêu (đối với thành viên tĩnh, là hàm khởi tạo; đối với thành viên instance, là prototype của lớp), tên của phương thức, và property descriptor của phương thức.

Chữ ký:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Giá trị trả về:

Một method decorator có thể trả về một PropertyDescriptor mới. Nếu có, descriptor này sẽ được sử dụng để định nghĩa phương thức. Điều này cho phép bạn sửa đổi hoặc thay thế việc triển khai của phương thức ban đầu, làm cho nó cực kỳ mạnh mẽ cho AOP.

Trường hợp sử dụng:

Ví dụ Method Decorator: Giám sát hiệu suất

Hãy tạo một decorator MeasurePerformance để ghi log thời gian thực thi của một phương thức.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const start = process.hrtime.bigint();
    const result = originalMethod.apply(this, args);
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000;
    console.log(`Phương thức "${propertyKey}" đã thực thi trong ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Mô phỏng một hoạt động phức tạp, tốn thời gian
    for (let i = 0; i < 1_000_000; i++) {
      Math.sin(i);
    }
    return data.map(n => n * 2);
  }

  @MeasurePerformance
  fetchRemoteData(id: string): Promise<string> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(`Dữ liệu cho ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

Decorator MeasurePerformance bao bọc phương thức ban đầu bằng logic đo thời gian, in ra thời gian thực thi mà không làm lộn xộn logic nghiệp vụ bên trong chính phương thức đó. Đây là một ví dụ kinh điển của Lập trình Hướng khía cạnh (AOP).

3. Accessor Decorator

Accessor decorator được áp dụng cho các khai báo accessor (getset). Tương tự như method decorator, chúng nhận đối tượng mục tiêu, tên của accessor và property descriptor của nó.

Chữ ký:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Giá trị trả về:

Một accessor decorator có thể trả về một PropertyDescriptor mới, sẽ được sử dụng để định nghĩa accessor.

Trường hợp sử dụng:

Ví dụ Accessor Decorator: Cache các Getter

Hãy tạo một decorator lưu cache kết quả của một phép tính getter tốn kém.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `_cached_${String(propertyKey)}`;

  if (originalGetter) {
    descriptor.get = function() {
      if (this[cacheKey] === undefined) {
        console.log(`[Cache Miss] Đang tính toán giá trị cho ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Sử dụng giá trị đã cache cho ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Mô phỏng một phép tính tốn kém
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Đang thực hiện tính toán tóm tắt tốn kém...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("Lần truy cập đầu tiên:", generator.expensiveSummary);
console.log("Lần truy cập thứ hai:", generator.expensiveSummary);
console.log("Lần truy cập thứ ba:", generator.expensiveSummary);

Decorator này đảm bảo rằng phép tính của getter expensiveSummary chỉ chạy một lần, các lần gọi tiếp theo sẽ trả về giá trị đã được cache. Mẫu này rất hữu ích để tối ưu hóa hiệu suất khi việc truy cập thuộc tính liên quan đến tính toán nặng hoặc các cuộc gọi bên ngoài.

4. Property Decorator

Property decorator được áp dụng cho các khai báo thuộc tính. Chúng nhận hai đối số: đối tượng mục tiêu (đối với thành viên tĩnh, là hàm khởi tạo; đối với thành viên instance, là prototype của lớp), và tên của thuộc tính.

Chữ ký:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Giá trị trả về:

Property decorator không thể trả về bất kỳ giá trị nào. Công dụng chính của chúng là đăng ký metadata về thuộc tính. Chúng không thể trực tiếp thay đổi giá trị của thuộc tính hoặc descriptor của nó tại thời điểm trang trí, vì descriptor cho một thuộc tính chưa được định nghĩa đầy đủ khi property decorator được chạy.

Trường hợp sử dụng:

Ví dụ Property Decorator: Xác thực trường bắt buộc

Hãy tạo một decorator để đánh dấu một thuộc tính là "bắt buộc" và sau đó xác thực nó lúc chạy.

interface ValidationRule {
  property: string | symbol;
  validate: (value: any) => boolean;
  message: string;
}

const validationRules: Map<Function, ValidationRule[]> = new Map();

function Required(target: Object, propertyKey: string | symbol) {
  const rules = validationRules.get(target.constructor) || [];
  rules.push({
    property: propertyKey,
    validate: (value: any) => value !== null && value !== undefined && value !== "",
    message: `${String(propertyKey)} là bắt buộc.`
  });
  validationRules.set(target.constructor, rules);
}

function validate(instance: any): string[] {
  const classRules = validationRules.get(instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of classRules) {
    if (!rule.validate(instance[rule.property])) {
      errors.push(rule.message);
    }
  }
  return errors;
}

class UserProfile {
  @Required
  firstName: string;

  @Required
  lastName: string;

  age?: number;

  constructor(firstName: string, lastName: string, age?: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new UserProfile("John", "Doe", 30);
console.log("Lỗi xác thực người dùng 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Lỗi xác thực người dùng 2:", validate(user2)); // ["firstName là bắt buộc."]

const user3 = new UserProfile("Alice", "");
console.log("Lỗi xác thực người dùng 3:", validate(user3)); // ["lastName là bắt buộc."]

Decorator Required chỉ đơn giản là đăng ký quy tắc xác thực với một map trung tâm validationRules. Một hàm validate riêng biệt sau đó sử dụng metadata này để kiểm tra instance lúc chạy. Mẫu này tách logic xác thực khỏi định nghĩa dữ liệu, làm cho nó có thể tái sử dụng và sạch sẽ.

5. Parameter Decorator

Parameter decorator được áp dụng cho các tham số trong hàm khởi tạo của lớp hoặc một phương thức. Chúng nhận ba đối số: đối tượng mục tiêu (đối với thành viên tĩnh, là hàm khởi tạo; đối với thành viên instance, là prototype của lớp), tên của phương thức (hoặc undefined đối với tham số của hàm khởi tạo), và chỉ số thứ tự của tham số trong danh sách tham số của hàm.

Chữ ký:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Giá trị trả về:

Parameter decorator không thể trả về bất kỳ giá trị nào. Giống như property decorator, vai trò chính của chúng là thêm metadata về tham số.

Trường hợp sử dụng:

Ví dụ Parameter Decorator: Tiêm dữ liệu Request

Hãy mô phỏng cách một web framework có thể sử dụng parameter decorator để tiêm dữ liệu cụ thể vào một tham số phương thức, chẳng hạn như ID người dùng từ một request.

interface ParameterMetadata {
  index: number;
  key: string | symbol;
  resolver: (request: any) => any;
}

const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();

function RequestParam(paramName: string) {
  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const targetKey = propertyKey || "constructor";
    let methodResolvers = parameterResolvers.get(target.constructor);
    if (!methodResolvers) {
      methodResolvers = new Map();
      parameterResolvers.set(target.constructor, methodResolvers);
    }
    const paramMetadata = methodResolvers.get(targetKey) || [];
    paramMetadata.push({
      index: parameterIndex,
      key: targetKey,
      resolver: (request: any) => request[paramName]
    });
    methodResolvers.set(targetKey, paramMetadata);
  };
}

// Một hàm framework giả định để gọi một phương thức với các tham số đã được giải quyết
function executeWithParams(instance: any, methodName: string, request: any) {
  const classResolvers = parameterResolvers.get(instance.constructor);
  if (!classResolvers) {
    return (instance[methodName] as Function).apply(instance, []);
  }
  const methodParamMetadata = classResolvers.get(methodName);
  if (!methodParamMetadata) {
    return (instance[methodName] as Function).apply(instance, []);
  }

  const args: any[] = Array(methodParamMetadata.length);
  for (const meta of methodParamMetadata) {
    args[meta.index] = meta.resolver(request);
  }
  return (instance[methodName] as Function).apply(instance, args);
}

class UserController {
  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
    console.log(`Đang lấy người dùng với ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Đang xóa người dùng với ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Mô phỏng một request đến
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Đang thực thi getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Đang thực thi deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Ví dụ này cho thấy cách parameter decorator có thể thu thập thông tin về các tham số phương thức cần thiết. Một framework sau đó có thể sử dụng metadata đã thu thập này để tự động giải quyết và tiêm các giá trị thích hợp khi phương thức được gọi, giúp đơn giản hóa đáng kể logic của controller hoặc service.

Thành phần và Thứ tự thực thi của Decorator

Decorator có thể được áp dụng trong nhiều sự kết hợp khác nhau, và việc hiểu thứ tự thực thi của chúng là rất quan trọng để dự đoán hành vi và tránh các vấn đề không mong muốn.

Nhiều Decorator trên một Mục tiêu duy nhất

Khi nhiều decorator được áp dụng cho một khai báo duy nhất (ví dụ: một lớp, phương thức hoặc thuộc tính), chúng thực thi theo một thứ tự cụ thể: từ dưới lên trên, hoặc từ phải sang trái, cho việc đánh giá của chúng. Tuy nhiên, kết quả của chúng được áp dụng theo thứ tự ngược lại.

@DecoratorA
@DecoratorB
class MyClass {
  // ...
}

Ở đây, DecoratorB sẽ được đánh giá trước, sau đó là DecoratorA. Nếu chúng sửa đổi lớp (ví dụ: bằng cách trả về một constructor mới), sự sửa đổi từ DecoratorA sẽ bao bọc hoặc áp dụng lên trên sự sửa đổi từ DecoratorB.

Ví dụ: Nối chuỗi các Method Decorator

Hãy xem xét hai method decorator: LogCallAuthorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Đang gọi ${String(propertyKey)} với các đối số:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Phương thức ${String(propertyKey)} trả về:`, result);
    return result;
  };
  return descriptor;
}

function Authorization(roles: string[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUserRoles = ["admin"]; // Mô phỏng việc lấy vai trò người dùng hiện tại
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Truy cập bị từ chối cho ${String(propertyKey)}. Yêu cầu vai trò: ${roles.join(", ")}`);
        throw new Error("Truy cập trái phép");
      }
      console.log(`[AUTH] Quyền truy cập được cấp cho ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Đang xóa dữ liệu nhạy cảm cho ID: ${id}`);
    return `Dữ liệu ID ${id} đã được xóa.`;
  }

  @Authorization(["user"])
  @LogCall // Thứ tự thay đổi ở đây
  fetchPublicData(query: string) {
    console.log(`Đang lấy dữ liệu công khai với truy vấn: ${query}`);
    return `Dữ liệu công khai cho truy vấn: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Đang gọi deleteSensitiveData (Người dùng Admin) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Đang gọi fetchPublicData (Người dùng không phải Admin) ---");
  // Mô phỏng một người dùng không phải admin cố gắng truy cập fetchPublicData yêu cầu vai trò 'user'
  const mockUserRoles = ["guest"]; // Điều này sẽ thất bại xác thực
  // Để làm điều này động, bạn sẽ cần một hệ thống DI hoặc ngữ cảnh tĩnh cho vai trò người dùng hiện tại.
  // Để đơn giản, chúng ta giả định decorator Authorization có quyền truy cập vào ngữ cảnh người dùng hiện tại.
  // Hãy điều chỉnh decorator Authorization để luôn giả định là 'admin' cho mục đích demo, 
  // để lần gọi đầu tiên thành công và lần thứ hai thất bại để hiển thị các đường đi khác nhau.
  
  // Chạy lại với vai trò 'user' để fetchPublicData thành công.
  // Tưởng tượng currentUserRoles trong Authorization trở thành: ['user']
  // Đối với ví dụ này, hãy giữ nó đơn giản và hiển thị hiệu ứng thứ tự.
  service.fetchPublicData("search term"); // Điều này sẽ thực thi Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Kết quả dự kiến cho deleteSensitiveData:
[AUTH] Quyền truy cập được cấp cho deleteSensitiveData
[LOG] Đang gọi deleteSensitiveData với các đối số: [ 'record123' ]
Đang xóa dữ liệu nhạy cảm cho ID: record123
[LOG] Phương thức deleteSensitiveData trả về: Dữ liệu ID record123 đã được xóa.
*/

/* Kết quả dự kiến cho fetchPublicData (nếu người dùng có vai trò 'user'):
[LOG] Đang gọi fetchPublicData với các đối số: [ 'search term' ]
[AUTH] Quyền truy cập được cấp cho fetchPublicData
Đang lấy dữ liệu công khai với truy vấn: search term
[LOG] Phương thức fetchPublicData trả về: Dữ liệu công khai cho truy vấn: search term
*/

Lưu ý thứ tự: đối với deleteSensitiveData, Authorization (dưới cùng) chạy trước, sau đó LogCall (trên cùng) bao bọc nó. Logic bên trong của Authorization thực thi trước. Đối với fetchPublicData, LogCall (dưới cùng) chạy trước, sau đó Authorization (trên cùng) bao bọc nó. Điều này có nghĩa là khía cạnh LogCall sẽ nằm ngoài khía cạnh Authorization. Sự khác biệt này rất quan trọng đối với các mối quan tâm xuyên suốt như logging hoặc xử lý lỗi, nơi thứ tự thực thi có thể ảnh hưởng đáng kể đến hành vi.

Thứ tự thực thi cho các Mục tiêu khác nhau

Khi một lớp, các thành viên của nó và các tham số đều có decorator, thứ tự thực thi được định nghĩa rõ ràng:

  1. Parameter Decorator được áp dụng trước, cho mỗi tham số, bắt đầu từ tham số cuối cùng đến tham số đầu tiên.
  2. Sau đó, Method, Accessor, hoặc Property Decorator được áp dụng cho mỗi thành viên.
  3. Cuối cùng, Class Decorator được áp dụng cho chính lớp đó.

Trong mỗi loại, nhiều decorator trên cùng một mục tiêu được áp dụng từ dưới lên trên (hoặc từ phải sang trái).

Ví dụ: Thứ tự thực thi đầy đủ

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Param Decorator: ${message} trên tham số #${descriptorOrIndex} của ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Method/Accessor Decorator: ${message} trên ${String(propertyKey)}`);
      } else {
        console.log(`Property Decorator: ${message} trên ${String(propertyKey)}`);
      }
    } else {
      console.log(`Class Decorator: ${message} trên ${target.name}`);
    }
    return descriptorOrIndex; // Trả về descriptor cho phương thức/accessor, undefined cho những cái khác
  };
}

@log("Class Level D")
@log("Class Level C")
class MyDecoratedClass {
  @log("Static Property A")
  static staticProp: string = "";

  @log("Instance Property B")
  instanceProp: number = 0;

  @log("Method D")
  @log("Method C")
  myMethod(
    @log("Parameter Z") paramZ: string,
    @log("Parameter Y") paramY: number
  ) {
    console.log("Phương thức myMethod đã thực thi.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Constructor đã thực thi.");
  }
}

new MyDecoratedClass();
// Gọi phương thức để kích hoạt method decorator
new MyDecoratedClass().myMethod("hello", 123);

/* Thứ tự đầu ra dự đoán (gần đúng, tùy thuộc vào phiên bản TypeScript cụ thể và quá trình biên dịch):
Param Decorator: Parameter Y trên tham số #1 của myMethod
Param Decorator: Parameter Z trên tham số #0 của myMethod
Property Decorator: Static Property A trên staticProp
Property Decorator: Instance Property B trên instanceProp
Method/Accessor Decorator: Getter/Setter F trên myAccessor
Method/Accessor Decorator: Method C trên myMethod
Method/Accessor Decorator: Method D trên myMethod
Class Decorator: Class Level C trên MyDecoratedClass
Class Decorator: Class Level D trên MyDecoratedClass
Constructor đã thực thi.
Phương thức myMethod đã thực thi.
*/

Thời gian ghi log chính xác trên console có thể thay đổi một chút tùy thuộc vào thời điểm một constructor hoặc phương thức được gọi, nhưng thứ tự mà các hàm decorator tự thực thi (và do đó các tác dụng phụ hoặc giá trị trả về của chúng được áp dụng) tuân theo các quy tắc trên.

Ứng dụng thực tế và Mẫu thiết kế với Decorator

Decorator, đặc biệt khi kết hợp với polyfill reflect-metadata, mở ra một lĩnh vực mới của lập trình dựa trên metadata. Điều này cho phép các mẫu thiết kế mạnh mẽ giúp trừu tượng hóa boilerplate và các mối quan tâm xuyên suốt.

1. Dependency Injection (DI)

Một trong những ứng dụng nổi bật nhất của decorator là trong các framework Dependency Injection (như @Injectable(), @Component() của Angular, v.v., hoặc việc sử dụng DI rộng rãi của NestJS). Decorator cho phép bạn khai báo các phụ thuộc trực tiếp trên các constructor hoặc thuộc tính, cho phép framework tự động khởi tạo và cung cấp các service chính xác.

Ví dụ: Tiêm Service đơn giản hóa

import "reflect-metadata"; // Cần thiết cho emitDecoratorMetadata

const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");

function Injectable() {
  return function (target: Function) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
  };
}

function Inject(token: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
  };
}

class Container {
  private static instances = new Map<any, any>();

  static resolve<T>(target: { new (...args: any[]): T }): T {
    if (Container.instances.has(target)) {
      return Container.instances.get(target);
    }

    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
    if (!isInjectable) {
      throw new Error(`Lớp ${target.name} không được đánh dấu là @Injectable.`);
    }

    // Lấy các kiểu của tham số constructor (yêu cầu emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Sử dụng token @Inject rõ ràng nếu được cung cấp, nếu không thì suy ra kiểu
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Không thể giải quyết tham số ở chỉ số ${index} cho ${target.name}. Có thể là một phụ thuộc vòng tròn hoặc kiểu nguyên thủy không có @Inject rõ ràng.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Định nghĩa các service
@Injectable()
class DatabaseService {
  connect() {
    console.log("Đang kết nối đến cơ sở dữ liệu...");
    return "DB Connection";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Đang xác thực bằng ${this.db.connect()}`);
    return "Người dùng đã đăng nhập";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Ví dụ về tiêm qua thuộc tính bằng decorator tùy chỉnh hoặc tính năng framework

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log("UserService: Đang lấy hồ sơ người dùng...");
    return { id: 1, name: "Global User" };
  }
}

// Giải quyết service chính
console.log("--- Đang giải quyết UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Đang giải quyết AuthService (nên được cache) ---");
const authService = Container.resolve(AuthService);
authService.login();

Ví dụ phức tạp này minh họa cách các decorator @Injectable@Inject, kết hợp với reflect-metadata, cho phép một Container tùy chỉnh tự động giải quyết và cung cấp các phụ thuộc. Metadata design:paramtypes được TypeScript tự động phát ra (khi emitDecoratorMetadata là true) là rất quan trọng ở đây.

2. Lập trình Hướng khía cạnh (AOP)

AOP tập trung vào việc modular hóa các mối quan tâm xuyên suốt (ví dụ: logging, bảo mật, giao dịch) mà cắt ngang qua nhiều lớp và module. Decorator là một sự phù hợp tuyệt vời để triển khai các khái niệm AOP trong TypeScript.

Ví dụ: Logging với Method Decorator

Quay lại với decorator LogCall, đó là một ví dụ hoàn hảo của AOP. Nó thêm hành vi logging vào bất kỳ phương thức nào mà không cần sửa đổi mã gốc của phương thức. Điều này tách biệt "làm gì" (logic nghiệp vụ) khỏi "làm như thế nào" (logging, giám sát hiệu suất, v.v.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Bắt đầu phương thức: ${String(propertyKey)} với các đối số:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Kết thúc phương thức: ${String(propertyKey)} với kết quả:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Lỗi trong phương thức ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Số tiền thanh toán phải là số dương.");
    }
    console.log(`Đang xử lý thanh toán ${amount} ${currency}...`);
    return `Thanh toán ${amount} ${currency} đã được xử lý thành công.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Đang hoàn tiền cho giao dịch ID: ${transactionId}...`);
    return `Hoàn tiền đã được bắt đầu cho ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Đã bắt lỗi:", error.message);
}

Cách tiếp cận này giữ cho lớp PaymentProcessor tập trung hoàn toàn vào logic thanh toán, trong khi decorator LogMethod xử lý mối quan tâm xuyên suốt về logging.

3. Xác thực và Chuyển đổi

Decorator cực kỳ hữu ích để định nghĩa các quy tắc xác thực trực tiếp trên các thuộc tính hoặc để chuyển đổi dữ liệu trong quá trình tuần tự hóa/giải tuần tự hóa.

Ví dụ: Xác thực dữ liệu với Property Decorator

Ví dụ @Required trước đó đã minh họa điều này. Đây là một ví dụ khác với xác thực phạm vi số.

interface FieldValidationRule {
  property: string | symbol;
  validator: (value: any) => boolean;
  message: string;
}

const fieldValidationRules = new Map<Function, FieldValidationRule[]>();

function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
  const rules = fieldValidationRules.get(target.constructor) || [];
  rules.push({ property: propertyKey, validator, message });
  fieldValidationRules.set(target.constructor, rules);
}

function IsPositive(target: Object, propertyKey: string | symbol) {
  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} phải là một số dương.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} không được dài quá ${maxLength} ký tự.`);
  };
}

class Product {
  @MaxLength(50)
  name: string;

  @IsPositive
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  static validate(instance: any): string[] {
    const errors: string[] = [];
    const rules = fieldValidationRules.get(instance.constructor) || [];
    for (const rule of rules) {
      if (!rule.validator(instance[rule.property])) {
        errors.push(rule.message);
      }
    }
    return errors;
  }
}

const product1 = new Product("Laptop", 1200);
console.log("Lỗi sản phẩm 1:", Product.validate(product1)); // []

const product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);
console.log("Lỗi sản phẩm 2:", Product.validate(product2)); // ["name không được dài quá 50 ký tự."]

const product3 = new Product("Book", -10);
console.log("Lỗi sản phẩm 3:", Product.validate(product3)); // ["price phải là một số dương."]

Thiết lập này cho phép bạn định nghĩa các quy tắc xác thực một cách khai báo trên các thuộc tính mô hình của mình, làm cho các mô hình dữ liệu của bạn tự mô tả về các ràng buộc của chúng.

Các phương pháp hay nhất và Lưu ý

Mặc dù decorator rất mạnh mẽ, chúng nên được sử dụng một cách thận trọng. Lạm dụng chúng có thể dẫn đến mã nguồn khó gỡ lỗi hoặc khó hiểu hơn.

Khi nào nên sử dụng Decorator (và khi nào không)

Ảnh hưởng đến hiệu suất

Decorator thực thi tại thời điểm biên dịch (hoặc thời điểm định nghĩa trong môi trường chạy JavaScript nếu được chuyển mã). Việc chuyển đổi hoặc thu thập metadata xảy ra khi lớp/phương thức được định nghĩa, chứ không phải trên mỗi lần gọi. Do đó, tác động hiệu suất lúc chạy của việc *áp dụng* decorator là rất nhỏ. Tuy nhiên, *logic bên trong* decorator của bạn có thể có tác động đến hiệu suất, đặc biệt nếu chúng thực hiện các hoạt động tốn kém trên mỗi lần gọi phương thức (ví dụ: các phép tính phức tạp trong một method decorator).

Khả năng bảo trì và Dễ đọc

Decorator, khi được sử dụng đúng cách, có thể cải thiện đáng kể khả năng đọc bằng cách di chuyển mã boilerplate ra khỏi logic chính. Tuy nhiên, nếu chúng thực hiện các phép biến đổi phức tạp, ẩn giấu, việc gỡ lỗi có thể trở nên khó khăn. Hãy đảm bảo decorator của bạn được ghi chú đầy đủ và hành vi của chúng có thể dự đoán được.

Trạng thái thử nghiệm và Tương lai của Decorator

Điều quan trọng cần nhắc lại là decorator trong TypeScript dựa trên một đề xuất Giai đoạn 3 của TC39. Điều này có nghĩa là đặc tả phần lớn ổn định nhưng vẫn có thể trải qua những thay đổi nhỏ trước khi trở thành một phần của tiêu chuẩn ECMAScript chính thức. Các framework như Angular đã chấp nhận chúng, đặt cược vào việc chúng sẽ được tiêu chuẩn hóa cuối cùng. Điều này ngụ ý một mức độ rủi ro nhất định, mặc dù với sự chấp nhận rộng rãi của chúng, các thay đổi lớn gây phá vỡ là không có khả năng xảy ra.

Đề xuất của TC39 đã phát triển. Việc triển khai hiện tại của TypeScript dựa trên một phiên bản cũ hơn của đề xuất. Có một sự phân biệt giữa "Legacy Decorators" và "Standard Decorators". Khi tiêu chuẩn chính thức ra mắt, TypeScript có khả năng sẽ cập nhật việc triển khai của mình. Đối với hầu hết các nhà phát triển sử dụng framework, quá trình chuyển đổi này sẽ được quản lý bởi chính framework đó. Đối với các tác giả thư viện, việc hiểu những khác biệt tinh tế giữa decorator cũ và decorator tiêu chuẩn trong tương lai có thể trở nên cần thiết.

Tùy chọn trình biên dịch emitDecoratorMetadata

Tùy chọn này, khi được đặt thành true trong tsconfig.json, chỉ thị cho trình biên dịch TypeScript phát ra một số metadata kiểu tại thời điểm thiết kế vào JavaScript đã biên dịch. Metadata này bao gồm kiểu của các tham số constructor (design:paramtypes), kiểu trả về của các phương thức (design:returntype), và kiểu của các thuộc tính (design:type).

Metadata được phát ra này không phải là một phần của môi trường chạy JavaScript tiêu chuẩn. Nó thường được tiêu thụ bởi polyfill reflect-metadata, sau đó làm cho nó có thể truy cập được thông qua các hàm Reflect.getMetadata(). Điều này hoàn toàn quan trọng đối với các mẫu nâng cao như Dependency Injection, nơi một container cần biết các loại phụ thuộc mà một lớp yêu cầu mà không cần cấu hình rõ ràng.

Các mẫu nâng cao với Decorator

Decorator có thể được kết hợp và mở rộng để xây dựng các mẫu phức tạp hơn nữa.

1. Trang trí Decorator (Higher-Order Decorators)

Bạn có thể tạo các decorator sửa đổi hoặc kết hợp các decorator khác. Điều này ít phổ biến hơn nhưng minh họa bản chất hàm của decorator.

// Một decorator đảm bảo một phương thức được ghi log và cũng yêu cầu vai trò admin
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Áp dụng Authorization trước (bên trong)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Sau đó áp dụng LogCall (bên ngoài)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Trả về descriptor đã được sửa đổi
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Đang xóa tài khoản người dùng: ${userId}`);
    return `Người dùng ${userId} đã bị xóa.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Kết quả đầu ra dự kiến (giả sử có vai trò admin):
[AUTH] Quyền truy cập được cấp cho deleteUserAccount
[LOG] Đang gọi deleteUserAccount với các đối số: [ 'user007' ]
Đang xóa tài khoản người dùng: user007
[LOG] Phương thức deleteUserAccount trả về: Người dùng user007 đã bị xóa.
*/

Ở đây, AdminAndLoggedMethod là một factory trả về một decorator, và bên trong decorator đó, nó áp dụng hai decorator khác. Mẫu này có thể đóng gói các thành phần decorator phức tạp.

2. Sử dụng Decorator cho Mixin

Mặc dù TypeScript cung cấp các cách khác để triển khai mixin, decorator có thể được sử dụng để tiêm các khả năng vào các lớp một cách khai báo.

function ApplyMixins(constructors: Function[]) {
  return function (derivedConstructor: Function) {
    constructors.forEach(baseConstructor => {
      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
        Object.defineProperty(
          derivedConstructor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
        );
      });
    });
  };
}

class Disposable {
  isDisposed: boolean = false;
  dispose() {
    this.isDisposed = true;
    console.log("Đối tượng đã được giải phóng.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Các thuộc tính/phương thức này được tiêm bởi decorator
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Tài nguyên ${this.name} đã được tạo.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Tài nguyên ${this.name} đã được dọn dẹp.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Đã giải phóng: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Đã giải phóng: ${resource.isDisposed}`);

Decorator @ApplyMixins này tự động sao chép các phương thức và thuộc tính từ các constructor cơ sở vào prototype của lớp dẫn xuất, thực sự "trộn vào" các chức năng.

Kết luận: Trao quyền cho Phát triển TypeScript Hiện đại

Decorator trong TypeScript là một tính năng mạnh mẽ và biểu cảm cho phép một mô hình lập trình mới dựa trên metadata và hướng khía cạnh. Chúng cho phép các nhà phát triển nâng cao, sửa đổi và thêm các hành vi khai báo vào các lớp, phương thức, thuộc tính, accessor và tham số mà không làm thay đổi logic cốt lõi của chúng. Sự tách biệt các mối quan tâm này dẫn đến mã nguồn sạch hơn, dễ bảo trì hơn và có khả năng tái sử dụng cao.

Từ việc đơn giản hóa dependency injection và triển khai các hệ thống xác thực mạnh mẽ đến việc thêm các mối quan tâm xuyên suốt như logging và giám sát hiệu suất, decorator cung cấp một giải pháp thanh lịch cho nhiều thách thức phát triển phổ biến. Mặc dù trạng thái thử nghiệm của chúng cần được lưu ý, sự chấp nhận rộng rãi của chúng trong các framework lớn cho thấy giá trị thực tế và sự phù hợp trong tương lai của chúng.

Bằng cách làm chủ decorator trong TypeScript, bạn có được một công cụ quan trọng trong kho vũ khí của mình, cho phép bạn xây dựng các ứng dụng mạnh mẽ, có khả năng mở rộng và thông minh hơn. Hãy sử dụng chúng một cách có trách nhiệm, hiểu rõ cơ chế hoạt động của chúng và mở khóa một cấp độ sức mạnh khai báo mới trong các dự án TypeScript của bạn.