Tiếp nối loạt bài về SOLID, sau khi đã tìm hiểu về nguyên tắc Đơn Trách Nhiệm (SRP), hôm nay chúng ta sẽ khám phá chữ cái thứ hai: O - Open/Closed Principle (OCP). Đây là một trong những nguyên tắc mạnh mẽ nhất giúp code của bạn dễ dàng mở rộng mà không sợ “phá vỡ” những gì đang hoạt động tốt.
Nguyên tắc này được Bertrand Meyer đề xuất và sau đó được Robert C. Martin (Uncle Bob) phổ biến rộng rãi:
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
Tức là, các thực thể phần mềm nên mở để mở rộng, nhưng đóng để sửa đổi. Bạn có thể thêm tính năng mới mà không cần chạm vào code cũ đã được kiểm thử và hoạt động ổn định.
Tại Sao Điều Này Quan Trọng?
Hãy tưởng tượng bạn có một hệ thống thanh toán đang chạy production với hàng triệu giao dịch mỗi ngày. Khi cần thêm một phương thức thanh toán mới (ví dụ: Momo, ZaloPay), nếu bạn phải sửa đổi code cũ, bạn có nguy cơ làm hỏng các phương thức đang hoạt động tốt như thẻ tín dụng hay PayPal. OCP giúp bạn tránh được rủi ro này bằng cách thiết kế code có thể mở rộng mà không cần thay đổi.
Ví Dụ Thực Tế: Hệ Thống Tính Chiết Khấu
Hãy xem một ví dụ về hệ thống tính chiết khấu cho các loại khách hàng khác nhau.
Ví dụ Xấu: Vi phạm OCP
Giả sử chúng ta có một lớp `DiscountCalculator` sử dụng if-else hoặc switch để xác định chiết khấu dựa trên loại khách hàng.
// VI PHẠM: Mỗi khi thêm loại khách hàng mới, ta phải SỬA ĐỔI lớp này
class DiscountCalculator_Bad {
public double calculateDiscount(String customerType, double amount) {
if ("REGULAR".equals(customerType)) {
return amount * 0.05; // Giảm 5%
} else if ("PREMIUM".equals(customerType)) {
return amount * 0.10; // Giảm 10%
} else if ("VIP".equals(customerType)) {
return amount * 0.20; // Giảm 20%
}
// Khi cần thêm loại "GOLD", "PLATINUM"...
// ta phải thêm else-if vào đây -> VI PHẠM OCP!
return 0;
}
}
Mỗi khi business yêu cầu thêm loại khách hàng mới (GOLD, PLATINUM, ENTERPRISE…), bạn phải:
- Mở file
DiscountCalculatorra và sửa đổi. - Thêm một nhánh
else-ifmới. - Chạy lại toàn bộ test để đảm bảo không ảnh hưởng các loại khách hàng cũ.
Điều này vi phạm nguyên tắc “đóng để sửa đổi”.
Ví dụ Tốt: Tuân Thủ OCP
Giải pháp là sử dụng abstraction (trừu tượng hóa) thông qua interface hoặc abstract class. Mỗi loại khách hàng sẽ có một class riêng implement interface chung.
// Bước 1: Định nghĩa một "hợp đồng" (interface) cho việc tính chiết khấu
interface DiscountStrategy {
double calculate(double amount);
}
// Bước 2: Implement các chiến lược cụ thể cho từng loại khách hàng
class RegularCustomerDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.05; // 5%
}
}
class PremiumCustomerDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.10; // 10%
}
}
class VipCustomerDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.20; // 20%
}
}
// Bước 3: Lớp DiscountCalculator giờ đây ĐÓNG để sửa đổi
// nhưng MỞ để mở rộng thông qua các DiscountStrategy mới
class DiscountCalculator {
private final DiscountStrategy strategy;
public DiscountCalculator(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double calculate(double amount) {
return strategy.calculate(amount);
}
}
Bây giờ, khi cần thêm loại khách hàng mới như “Gold”, bạn chỉ cần tạo một class mới:
// MỞ RỘNG: Thêm tính năng mới mà KHÔNG sửa đổi bất kỳ code nào đang có
class GoldCustomerDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.15; // 15%
}
}
// Sử dụng
DiscountCalculator calculator = new DiscountCalculator(new GoldCustomerDiscount());
double discount = calculator.calculate(100000); // 15000
Lợi Ích Của Thiết Kế Này
Với thiết kế mới, chúng ta đạt được:
- Không sửa đổi code cũ: Lớp
DiscountCalculatorkhông bao giờ cần phải thay đổi khi thêm loại khách hàng mới. - Dễ dàng mở rộng: Chỉ cần tạo một class mới implement
DiscountStrategy. - An toàn hơn: Code cũ đã được test vẫn hoạt động ổn định, giảm thiểu rủi ro regression.
- Dễ test: Mỗi strategy có thể được unit test độc lập.
Các Kỹ Thuật Áp Dụng OCP
Có nhiều cách để áp dụng OCP trong thực tế:
- Strategy Pattern: Như ví dụ trên, cho phép thay đổi thuật toán tại runtime.
- Template Method Pattern: Định nghĩa “khung xương” của thuật toán trong abstract class, các bước cụ thể được override bởi subclass.
- Decorator Pattern: Thêm behavior mới bằng cách “wrap” object gốc mà không sửa đổi nó.
- Dependency Injection: Inject dependency thông qua constructor hoặc setter, giúp thay thế implementation dễ dàng.
Kết Luận
Nguyên tắc Open/Closed Principle là chìa khóa để xây dựng hệ thống có khả năng mở rộng cao. Thay vì sợ hãi mỗi khi có yêu cầu thêm tính năng mới, bạn sẽ tự tin vì biết rằng code của mình đã được thiết kế để đón nhận sự thay đổi. Hãy nhớ: “Mở để mở rộng, đóng để sửa đổi” - đây là cách bạn bảo vệ code hiện tại trong khi vẫn cho phép hệ thống phát triển.