
Có một sự thật là: Khi mới bắt đầu một dự án, code của chúng ta luôn đẹp đẽ và ngăn nắp. Nhưng theo thời gian, khi các tính năng mới (features) được yêu cầu dồn dập, các câu lệnh điều kiện if-else hoặc switch-case bắt đầu mọc lên như nấm sau mưa.
Tương tự như cách chúng ta giải quyết bài toán thắt cổ chai dữ liệu bằng giải pháp bộ nhớ đệm (bạn có thể tham khảo thêm tại bài viết về Redis Caching trên Ant NCC), việc tối ưu cấu trúc mã nguồn ngay từ đầu cũng quan trọng không kém để hệ thống không bị “phình to” vô kiểm soát. Đến một ngày, bạn mở một file service ra và đối mặt với một “quái vật” if-else lồng nhau dài 500 dòng. Bạn đã chính thức rơi vào “If-Else Hell” (Địa ngục If-Else)!
Bài viết này sẽ hướng dẫn bạn cách dùng Strategy Design Pattern để đập tan đống if-else hỗn độn đó, đưa code về trạng thái Clean Code.
Vấn Đề: Khi If-Else Vi Phạm Nguyên Lý OCP (Open/Closed Principle)
Hãy tưởng tượng bạn đang viết module tính toán chi phí vận chuyển cho một sàn Thương mại điện tử (E-commerce) dựa trên các đơn vị vận chuyển: Giao Hàng Nhanh (GHN), Giao Hàng Tiết Kiệm (GHTK), và ViettelPost.
Code “sơ khai” của bạn sẽ trông như thế này:
TypeScript
class ShippingCalculator {
calculateFee(provider: string, weight: number): number {
if (provider === 'GHN') {
return weight * 12000 + 5000;
} else if (provider === 'GHTK') {
return weight * 10000 + 8000;
} else if (provider === 'ViettelPost') {
return weight * 15000;
} else {
throw new Error("Đơn vị vận chuyển không hợp lệ!");
}
}
}
Tại sao đoạn code trên lại là “bad code”?
- Vi phạm OCP (Open/Closed Principle): Nếu tháng sau, PM yêu cầu tích hợp thêm GrabExpress hoặc Shopee Xpress, bạn lại phải nhảy vào class này, viết thêm
else if. Việc sửa đổi trực tiếp vào một class đang chạy ổn định rất dễ sinh ra bug cho các tính năng cũ. - Khó bảo trì và test: Càng nhiều đơn vị vận chuyển, hàm
calculateFeecàng phình to. Việc viết Unit Test cho một hàm chứa quá nhiều logic rẽ nhánh sẽ trở thành một cơn ác mộng.
Giải Pháp: Strategy Design Pattern Là Gì?
Strategy Pattern là một mẫu thiết kế thuộc nhóm Behavioral (Hành vi). Thay vì thực hiện một hành vi bằng nhiều cách khác nhau trong cùng một Class (bằng cách dùng if-else), Strategy Pattern định nghĩa một tập hợp các thuật toán riêng biệt, đóng gói từng thuật toán lại thành các Class độc lập, và giúp chúng có thể hoán đổi cho nhau linh hoạt lúc runtime.
Alt text: Mô hình hóa cách tách logic If-Else thành các Strategy Class độc lập
Từng Bước Refactor Code Thực Chiến
Hãy cùng áp dụng Strategy Pattern để “giải cứu” đoạn code trên qua 3 bước đơn giản:
Bước 1: Định nghĩa Interface chung cho tất cả Chiến lược (Strategy)
Mọi đơn vị vận chuyển đều phải tuân thủ một “bản hợp đồng” chung là hàm tính phí.
TypeScript
interface IShippingStrategy {
calculate(weight: number): number;
}
Bước 2: Tạo các Class cụ thể cho từng Đơn vị vận chuyển
Mỗi bên sẽ tự quản lý logic tính toán của riêng mình. Bạn cần thêm bao nhiêu bên thì chỉ cần tạo thêm bấy nhiêu class, hoàn toàn độc lập!
TypeScript
class GHNStrategy implements IShippingStrategy {
calculate(weight: number): number {
return weight * 12000 + 5000;
}
}
class GHTKStrategy implements IShippingStrategy {
calculate(weight: number): number {
return weight * 10000 + 8000;
}
}
class ViettelPostStrategy implements IShippingStrategy {
calculate(weight: number): number {
return weight * 15000;
}
}
Bước 3: Thiết lập Context để sử dụng linh hoạt
Class ShippingContext sẽ nhận vào chiến lược phù hợp lúc runtime và thực thi nó mà không cần quan tâm bên trong chiến lược đó làm gì.
TypeScript
class ShippingOrderContext {
private strategy!: IShippingStrategy;
// Cho phép thay đổi chiến lược linh hoạt
setStrategy(strategy: IShippingStrategy) {
this.strategy = strategy;
}
executeCalculation(weight: number): number {
if (!this.strategy) {
throw new Error("Vui lòng chọn phương thức vận chuyển trước!");
}
return this.strategy.calculate(weight);
}
}
Cách vận hành trong thực tế:
Bây giờ, khi client gọi API, chúng ta chỉ cần map từ provider sang class tương ứng (có thể dùng một Map object đơn giản thay vì if-else).
TypeScript
const strategyMap: Record<string, IShippingStrategy> = {
'GHN': new GHNStrategy(),
'GHTK': new GHTKStrategy(),
'ViettelPost': new ViettelPostStrategy(),
// Muốn thêm GrabExpress? Chỉ cần thêm 1 dòng ở đây sau khi tạo class mới!
};
// Sử dụng
const order = new ShippingOrderContext();
const userChoice = 'GHTK'; // Giả sử user chọn GHTK trên UI
order.setStrategy(strategyMap[userChoice]);
const finalFee = order.executeCalculation(5); // Tính phí cho gói hàng 5kg
console.log(`Phí vận chuyển là: ${finalFee} VND`);
So Sánh: Khi Nào Nên Và Không Nên Dùng Strategy Pattern?
Mặc dù rất “xịn xò”, nhưng không phải lúc nào áp dụng Design Pattern cũng tốt. Hãy cân nhắc bảng so sánh sau:
| Ưu điểm (Nên dùng) | Nhược điểm (Cần cân nhắc) |
| Code cực sạch: Tách biệt hoàn toàn phần code xử lý logic khỏi code điều khiển. | Tăng số lượng File/Class: Dự án sẽ có nhiều file nhỏ hơn, đôi khi gây rối cho người mới. |
| Dễ dàng mở rộng: Thêm tính năng mới không sợ ảnh hưởng đến tính năng cũ (Chuẩn SOLID). | Overengineering: Nếu hệ thống của bạn chỉ có 2 nhánh cố định và không bao giờ tăng thêm, dùng if-else truyền thống vẫn nhanh và tối ưu hơn. |
| Unit Test dễ dàng: Có thể test độc lập từng class Strategy một cách mượt mà. | Client buộc phải biết và hiểu rõ sự khác biệt giữa các Strategy để truyền vào cho đúng. |
Kết Luận và Takeaway
Việc lạm dụng if-else là một trong những nguyên nhân hàng đầu tạo ra Technical Debt (Nợ kỹ thuật) cho dự án. Bằng cách áp dụng Strategy Design Pattern, bạn đã biến những khối code rối rắm, dễ vỡ thành một kiến trúc linh hoạt, dễ bảo trì và mở rộng – tố chất cần có của một Senior Developer tương lai tại NCC.
