Tiếng Việt

Khám phá các lớp trừu tượng trong TypeScript, lợi ích của chúng, và các mẫu hình nâng cao để triển khai một phần, giúp tăng cường khả năng tái sử dụng và tính linh hoạt của mã trong các dự án phức tạp. Bao gồm các ví dụ thực tế và phương pháp hay nhất.

Lớp trừu tượng (Abstract Class) trong TypeScript: Làm chủ các mẫu hình triển khai một phần

Lớp trừu tượng là một khái niệm cơ bản trong lập trình hướng đối tượng (OOP), cung cấp một bản thiết kế cho các lớp khác. Trong TypeScript, các lớp trừu tượng cung cấp một cơ chế mạnh mẽ để định nghĩa chức năng chung đồng thời bắt buộc các yêu cầu triển khai cụ thể trên các lớp dẫn xuất. Bài viết này đi sâu vào sự phức tạp của các lớp trừu tượng trong TypeScript, tập trung vào các mẫu hình thực tế để triển khai một phần và cách chúng có thể tăng cường đáng kể khả năng tái sử dụng, bảo trì và tính linh hoạt của mã trong các dự án của bạn.

Lớp trừu tượng là gì?

Lớp trừu tượng trong TypeScript là một lớp không thể được khởi tạo trực tiếp. Nó đóng vai trò là một lớp cơ sở cho các lớp khác, định nghĩa một tập hợp các thuộc tính và phương thức mà các lớp dẫn xuất phải triển khai (hoặc ghi đè). Các lớp trừu tượng được khai báo bằng từ khóa abstract.

Đặc điểm chính:

Tại sao nên sử dụng lớp trừu tượng?

Các lớp trừu tượng mang lại nhiều lợi thế trong phát triển phần mềm:

Ví dụ cơ bản về lớp trừu tượng

Hãy bắt đầu với một ví dụ đơn giản để minh họa cú pháp cơ bản của một lớp trừu tượng trong TypeScript:


abstract class Animal {
 abstract makeSound(): string;

 move(): void {
 console.log("Moving...");
 }
}

class Dog extends Animal {
 makeSound(): string {
 return "Woof!";
 }
}

class Cat extends Animal {
 makeSound(): string {
 return "Meow!";
 }
}

//const animal = new Animal(); // Lỗi: Không thể tạo một thể hiện của lớp trừu tượng.

const dog = new Dog();
console.log(dog.makeSound()); // Kết quả: Woof!
dog.move(); // Kết quả: Moving...

const cat = new Cat();
console.log(cat.makeSound()); // Kết quả: Meow!
cat.move(); // Kết quả: Moving...

Trong ví dụ này, Animal là một lớp trừu tượng với một phương thức trừu tượng là makeSound() và một phương thức cụ thể là move(). Các lớp DogCat mở rộng từ Animal và cung cấp các phần triển khai cụ thể cho phương thức makeSound(). Lưu ý rằng việc cố gắng khởi tạo trực tiếp `Animal` sẽ gây ra lỗi.

Các mẫu hình triển khai một phần

Một trong những khía cạnh mạnh mẽ của lớp trừu tượng là khả năng định nghĩa các phần triển khai một phần. Điều này cho phép bạn cung cấp một phần triển khai mặc định cho một số phương thức trong khi yêu cầu các lớp dẫn xuất phải triển khai những phương thức khác. Điều này cân bằng giữa khả năng tái sử dụng mã và tính linh hoạt.

1. Phương thức trừu tượng với triển khai trong các lớp dẫn xuất

Trong mẫu hình này, lớp trừu tượng khai báo một phương thức trừu tượng mà *phải* được triển khai bởi các lớp dẫn xuất, nhưng nó không cung cấp phần triển khai cơ sở nào. Điều này buộc các lớp dẫn xuất phải cung cấp logic của riêng chúng.


abstract class DataProcessor {
 abstract fetchData(): Promise;
 abstract processData(data: any): any;
 abstract saveData(processedData: any): Promise;

 async run(): Promise {
 const data = await this.fetchData();
 const processedData = this.processData(data);
 await this.saveData(processedData);
 }
}

class APIProcessor extends DataProcessor {
 async fetchData(): Promise {
 // Triển khai để lấy dữ liệu từ một API
 console.log("Fetching data from API...");
 return { data: "API Data" }; // Dữ liệu giả
 }

 processData(data: any): any {
 // Triển khai để xử lý dữ liệu dành riêng cho dữ liệu API
 console.log("Processing API data...");
 return { processed: data.data + " - Processed" }; // Dữ liệu đã xử lý giả
 }

 async saveData(processedData: any): Promise {
 // Triển khai để lưu dữ liệu đã xử lý vào cơ sở dữ liệu qua API
 console.log("Saving processed API data...");
 console.log(processedData);
 }
}

const apiProcessor = new APIProcessor();
apiProcessor.run();

Trong ví dụ này, lớp trừu tượng DataProcessor định nghĩa ba phương thức trừu tượng: fetchData(), processData(), và saveData(). Lớp APIProcessor mở rộng từ DataProcessor và cung cấp các phần triển khai cụ thể cho từng phương thức này. Phương thức run(), được định nghĩa trong lớp trừu tượng, điều phối toàn bộ quy trình, đảm bảo rằng mỗi bước được thực hiện theo đúng thứ tự.

2. Phương thức cụ thể với các phụ thuộc trừu tượng

Mẫu hình này liên quan đến các phương thức cụ thể trong lớp trừu tượng mà dựa vào các phương thức trừu tượng để thực hiện các tác vụ cụ thể. Điều này cho phép bạn định nghĩa một thuật toán chung trong khi ủy thác các chi tiết triển khai cho các lớp dẫn xuất.


abstract class PaymentProcessor {
 abstract validatePaymentDetails(paymentDetails: any): boolean;
 abstract chargePayment(paymentDetails: any): Promise;
 abstract sendConfirmationEmail(paymentDetails: any): Promise;

 async processPayment(paymentDetails: any): Promise {
 if (!this.validatePaymentDetails(paymentDetails)) {
 console.error("Invalid payment details.");
 return false;
 }

 const chargeSuccessful = await this.chargePayment(paymentDetails);
 if (!chargeSuccessful) {
 console.error("Payment failed.");
 return false;
 }

 await this.sendConfirmationEmail(paymentDetails);
 console.log("Payment processed successfully.");
 return true;
 }
}

class CreditCardPaymentProcessor extends PaymentProcessor {
 validatePaymentDetails(paymentDetails: any): boolean {
 // Xác thực chi tiết thẻ tín dụng
 console.log("Validating credit card details...");
 return true; // Xác thực giả
 }

 async chargePayment(paymentDetails: any): Promise {
 // Thanh toán bằng thẻ tín dụng
 console.log("Charging credit card...");
 return true; // Thanh toán giả
 }

 async sendConfirmationEmail(paymentDetails: any): Promise {
 // Gửi email xác nhận cho thanh toán bằng thẻ tín dụng
 console.log("Sending confirmation email for credit card payment...");
 }
}

const creditCardProcessor = new CreditCardPaymentProcessor();
creditCardProcessor.processPayment({ cardNumber: "1234-5678-9012-3456", expiryDate: "12/24", cvv: "123", amount: 100 });

Trong ví dụ này, lớp trừu tượng PaymentProcessor định nghĩa một phương thức processPayment() xử lý logic xử lý thanh toán tổng thể. Tuy nhiên, các phương thức validatePaymentDetails(), chargePayment(), và sendConfirmationEmail() là trừu tượng, yêu cầu các lớp dẫn xuất phải cung cấp các phần triển khai cụ thể cho từng phương thức thanh toán (ví dụ: thẻ tín dụng, PayPal, v.v.).

3. Mẫu Template Method

Mẫu Template Method là một mẫu thiết kế hành vi, định nghĩa bộ khung của một thuật toán trong lớp trừu tượng nhưng cho phép các lớp con ghi đè các bước cụ thể của thuật toán mà không thay đổi cấu trúc của nó. Mẫu này đặc biệt hữu ích khi bạn có một chuỗi các hoạt động cần được thực hiện theo một thứ tự cụ thể, nhưng việc triển khai một số hoạt động có thể thay đổi tùy thuộc vào ngữ cảnh.


abstract class ReportGenerator {
 abstract generateHeader(): string;
 abstract generateBody(): string;
 abstract generateFooter(): string;

 generateReport(): string {
 const header = this.generateHeader();
 const body = this.generateBody();
 const footer = this.generateFooter();

 return `${header}\n${body}\n${footer}`;
 }
}

class PDFReportGenerator extends ReportGenerator {
 generateHeader(): string {
 return "PDF Report Header";
 }

 generateBody(): string {
 return "PDF Report Body";
 }

 generateFooter(): string {
 return "PDF Report Footer";
 }
}

class CSVReportGenerator extends ReportGenerator {
 generateHeader(): string {
 return "CSV Report Header";
 }

 generateBody(): string {
 return "CSV Report Body";
 }

 generateFooter(): string {
 return "CSV Report Footer";
 }
}

const pdfReportGenerator = new PDFReportGenerator();
console.log(pdfReportGenerator.generateReport());

const csvReportGenerator = new CSVReportGenerator();
console.log(csvReportGenerator.generateReport());

Ở đây, `ReportGenerator` định nghĩa quy trình tạo báo cáo tổng thể trong `generateReport()`, trong khi các bước riêng lẻ (header, body, footer) được để lại cho các lớp con cụ thể là `PDFReportGenerator` và `CSVReportGenerator`.

4. Thuộc tính trừu tượng

Các lớp trừu tượng cũng có thể định nghĩa các thuộc tính trừu tượng, là các thuộc tính phải được triển khai trong các lớp dẫn xuất. Điều này hữu ích để bắt buộc sự hiện diện của một số phần tử dữ liệu nhất định trong các lớp dẫn xuất.


abstract class Configuration {
 abstract apiKey: string;
 abstract apiUrl: string;

 getFullApiUrl(): string {
 return `${this.apiUrl}/${this.apiKey}`;
 }
}

class ProductionConfiguration extends Configuration {
 apiKey: string = "prod_api_key";
 apiUrl: string = "https://api.example.com/prod";
}

class DevelopmentConfiguration extends Configuration {
 apiKey: string = "dev_api_key";
 apiUrl: string = "http://localhost:3000/dev";
}

const prodConfig = new ProductionConfiguration();
console.log(prodConfig.getFullApiUrl()); // Kết quả: https://api.example.com/prod/prod_api_key

const devConfig = new DevelopmentConfiguration();
console.log(devConfig.getFullApiUrl()); // Kết quả: http://localhost:3000/dev/dev_api_key

Trong ví dụ này, lớp trừu tượng Configuration định nghĩa hai thuộc tính trừu tượng: apiKeyapiUrl. Các lớp ProductionConfigurationDevelopmentConfiguration mở rộng từ Configuration và cung cấp các giá trị cụ thể cho các thuộc tính này.

Các cân nhắc nâng cao

Mixin với lớp trừu tượng

TypeScript cho phép bạn kết hợp các lớp trừu tượng với mixin để tạo ra các thành phần phức tạp và có khả năng tái sử dụng cao hơn. Mixin là một cách xây dựng các lớp bằng cách kết hợp các mảng chức năng nhỏ hơn, có thể tái sử dụng.


// Định nghĩa một kiểu cho hàm khởi tạo của một lớp
type Constructor = new (...args: any[]) => T;

// Định nghĩa một hàm mixin
function Timestamped(Base: TBase) {
 return class extends Base {
 timestamp = new Date();
 };
}

// Một hàm mixin khác
function Logged(Base: TBase) {
 return class extends Base {
 log(message: string) {
 console.log(`${this.constructor.name}: ${message}`);
 }
 };
}

abstract class BaseEntity {
 abstract id: number;
}

// Áp dụng các mixin vào lớp trừu tượng BaseEntity
const TimestampedEntity = Timestamped(BaseEntity);
const LoggedEntity = Logged(TimestampedEntity);

class User extends LoggedEntity {
 id: number = 123;
 name: string = "John Doe";

 constructor() {
 super();
 this.log("User created");
 }
}

const user = new User();
console.log(user.id); // Kết quả: 123
console.log(user.timestamp); // Kết quả: Current timestamp
user.log("User updated"); // Kết quả: User: User updated

Ví dụ này kết hợp các mixin TimestampedLogged với lớp trừu tượng BaseEntity để tạo ra một lớp User kế thừa chức năng của cả ba.

Dependency Injection (Tiêm phụ thuộc)

Các lớp trừu tượng có thể được sử dụng hiệu quả với dependency injection (DI) để giảm sự phụ thuộc giữa các thành phần và cải thiện khả năng kiểm thử. Bạn có thể định nghĩa các lớp trừu tượng như là giao diện cho các phụ thuộc của mình và sau đó tiêm các triển khai cụ thể vào các lớp của bạn.


abstract class Logger {
 abstract log(message: string): void;
}

class ConsoleLogger extends Logger {
 log(message: string): void {
 console.log(`[Console]: ${message}`);
 }
}

class FileLogger extends Logger {
 log(message: string): void {
 // Triển khai để ghi log vào một tệp tin
 console.log(`[File]: ${message}`);
 }
}

class AppService {
 private logger: Logger;

 constructor(logger: Logger) {
 this.logger = logger;
 }

 doSomething() {
 this.logger.log("Doing something...");
 }
}

// Tiêm ConsoleLogger
const consoleLogger = new ConsoleLogger();
const appService1 = new AppService(consoleLogger);
appService1.doSomething();

// Tiêm FileLogger
const fileLogger = new FileLogger();
const appService2 = new AppService(fileLogger);
appService2.doSomething();

Trong ví dụ này, lớp AppService phụ thuộc vào lớp trừu tượng Logger. Các triển khai cụ thể (ConsoleLogger, FileLogger) được tiêm vào lúc chạy, cho phép bạn dễ dàng chuyển đổi giữa các chiến lược ghi log khác nhau.

Các phương pháp hay nhất

Kết luận

Các lớp trừu tượng trong TypeScript là một công cụ mạnh mẽ để xây dựng các ứng dụng mạnh mẽ và dễ bảo trì. Bằng cách hiểu và áp dụng các mẫu hình triển khai một phần, bạn có thể tận dụng lợi ích của các lớp trừu tượng để tạo ra mã linh hoạt, có thể tái sử dụng và có cấu trúc tốt. Từ việc định nghĩa các phương thức trừu tượng với triển khai mặc định đến việc sử dụng các lớp trừu tượng với mixin và dependency injection, các khả năng là rất lớn. Bằng cách tuân theo các phương pháp hay nhất và xem xét cẩn thận các lựa chọn thiết kế của mình, bạn có thể sử dụng hiệu quả các lớp trừu tượng để nâng cao chất lượng và khả năng mở rộng của các dự án TypeScript của mình.

Dù bạn đang xây dựng một ứng dụng doanh nghiệp quy mô lớn hay một thư viện tiện ích nhỏ, việc làm chủ các lớp trừu tượng trong TypeScript chắc chắn sẽ cải thiện kỹ năng phát triển phần mềm của bạn và cho phép bạn tạo ra các giải pháp tinh vi và dễ bảo trì hơn.