strategy pattern
Tung Bui Thanh (TungDaDev)9 mins read

Strategy Pattern là một trong những design pattern được dùng nhiều nhất. Có điều ở nhiều dự án bây giờ, người ta áp dụng nó khá máy móc: đẻ ra cả tá class chỉ chứa đúng một method 3 dòng, project nhìn như mê cung class, bảo trì mệt. Từ khi Java 8 có function là "công dân hạng nhất" (First-Class Functions) và Spring Boot lo phần Dependency Injection, Strategy Pattern đã có cách viết khác hẳn: gọn hơn, ít class hơn mà vẫn type-safe.
Từ tính chiết khấu đơn hàng trong E-Commerce, chọn thuật toán nén dữ liệu, tới tự chọn thuật toán mã hoá trong FinTech, Strategy Pattern giúp tách thuật toán ra khỏi luồng điều phối nghiệp vụ.
Dưới đây là ranh giới giữa Strategy, State và Template Method, cách Java Lambdas cắt bớt phần lớn code khuôn mẫu, rồi dựng một Dynamic Pricing Engine (công cụ tính giá khuyến mãi động) đủ dùng cho production.
# Strategy vs state vs template method
Ba behavioral pattern này hay bị nhầm vì nhìn Class Diagram thì na ná nhau:
# bảng so sánh chi tiết
| Tiêu chí | Strategy Pattern | State Pattern | Template Method Pattern |
|---|---|---|---|
| Cách cài đặt | Composition (uỷ quyền) | Composition (uỷ quyền) | Inheritance (kế thừa) |
| Ai quyết định đổi? | Client hoặc Service Context chọn strategy phù hợp | Các State tự chuyển qua lại với nhau | Chốt cứng lúc biên dịch (Compile-time) |
| Dùng để làm gì | Đổi qua lại giữa các thuật toán độc lập | Quản lý vòng đời trạng thái của object | Dùng lại khung logic chung |
| Có giữ trạng thái không | Strategy thường Stateless (không giữ trạng thái) | State thường giữ ngữ cảnh để chuyển đổi | Tuỳ vào cây kế thừa |
# từ Java 8 trở đi: lambda thay cho đống class thừa
Trước Java 8, muốn có 3 cách định dạng ngày tháng thì phải tạo 1 Interface và 3 Class riêng:
# viết gọn bằng functional interfaces
Không cần tạo class RoadShipping, SeaShipping nữa, chỉ cần dùng BiFunction<BigDecimal, BigDecimal, BigDecimal> hoặc tự định nghĩa một @FunctionalInterface. Tự định nghĩa thường dễ đọc hơn: tên ShippingCalculator.calculate(weightKg, distanceKm) nói rõ ý nghĩa hơn BiFunction.apply(a, b). Và với tiền thì dùng BigDecimal, đừng dùng Double vì sai số dấu phẩy động (0.1 + 0.2 ra 0.30000000000000004).
@FunctionalInterface
public interface ShippingCalculator {
BigDecimal calculate(BigDecimal weightKg, BigDecimal distanceKm);
}
// Bảng biểu diễn chiến lược dưới dạng Lambda hoặc Static Method References:
public class ShippingStrategies {
public static final ShippingCalculator ROAD =
(weight, distance) -> weight.multiply(new BigDecimal("1.5")).add(distance.multiply(new BigDecimal("0.5")));
public static final ShippingCalculator SEA =
(weight, distance) -> weight.multiply(new BigDecimal("1.0")).add(distance.multiply(new BigDecimal("0.2")));
public static final ShippingCalculator AIR =
(weight, distance) -> weight.multiply(new BigDecimal("2.5")).add(distance.multiply(new BigDecimal("1.0")));
}Ở tầng Controller / Service, đổi strategy giờ nhẹ nhàng hẳn:
public class ShippingService {
public BigDecimal computeCost(BigDecimal weight, BigDecimal dist, ShippingCalculator calculator) {
return calculator.calculate(weight, dist); // Nhận trực tiếp bất kỳ Lambda nào!
}
}
// Gọi thực thi:
shippingService.computeCost(w, d, ShippingStrategies.AIR);
// Hoặc truyền trực tiếp inline lambda cho chương trình khuyến mãi đặc biệt:
shippingService.computeCost(w, d, (weight, dist) -> BigDecimal.ZERO); // Miễn phí vận chuyển# ví dụ cỡ enterprise: dynamic promotion & pricing engine
Ở các hệ thống thương mại điện tử lớn, tính giá khuyến mãi (Discount & Pricing) chẳng bao giờ là một phép tính đơn giản. Một đơn hàng có thể dính cùng lúc nhiều strategy:
- Giảm giá theo hạng thành viên (VIP Tier Discount).
- Voucher, coupon giảm giá.
- Flash Sale theo khung giờ vàng.
# viết promotion Strategy với Spring Boot
public interface PromotionStrategy {
boolean isApplicable(OrderContext context);
DiscountResult applyDiscount(OrderContext context);
int getExecutionOrder(); // Thứ tự ưu tiên áp dụng (Order precedence)
}
@Component
@Order(1)
public class VipCustomerPromotionStrategy implements PromotionStrategy {
@Override
public boolean isApplicable(OrderContext context) {
return context.customer().isVip();
}
@Override
public DiscountResult applyDiscount(OrderContext context) {
BigDecimal discount = context.currentTotal().multiply(new BigDecimal("0.10"));
return new DiscountResult("VIP_10_PERCENT", discount);
}
@Override
public int getExecutionOrder() { return 1; }
}
@Component
@Order(2)
public class FirstOrderPromotionStrategy implements PromotionStrategy {
@Override
public boolean isApplicable(OrderContext context) {
return context.customer().totalOrders() == 0;
}
@Override
public DiscountResult applyDiscount(OrderContext context) {
return new DiscountResult("FIRST_ORDER_FIXED", new BigDecimal("50000"));
}
@Override
public int getExecutionOrder() { return 2; }
}# engine tự gom hết strategy (Spring autowiring)
@Service
public class PromotionExecutionEngine {
private final List<PromotionStrategy> strategies;
// Spring tự inject mọi bean PromotionStrategy, và List được inject đã được sắp theo @Order.
// Ở đây sắp lại theo getExecutionOrder(): trong code thật chỉ nên giữ MỘT nguồn thứ tự,
// hai nguồn lệch nhau là bug khó thấy.
public PromotionExecutionEngine(List<PromotionStrategy> strategies) {
this.strategies = strategies.stream()
.sorted(Comparator.comparingInt(PromotionStrategy::getExecutionOrder))
.toList();
}
public OrderPricingResult calculateFinalPrice(OrderContext context) {
BigDecimal runningTotal = context.initialTotal();
List<DiscountResult> appliedDiscounts = new ArrayList<>();
for (PromotionStrategy strategy : strategies) {
if (strategy.isApplicable(context)) {
DiscountResult result = strategy.applyDiscount(context);
runningTotal = runningTotal.subtract(result.amount()).max(BigDecimal.ZERO); // không để giá âm
appliedDiscounts.add(result);
// Cập nhật giá trị đơn hàng sau khi giảm để chiến lược tiếp theo tính toán
context = context.withCurrentTotal(runningTotal);
}
}
return new OrderPricingResult(runningTotal, appliedDiscounts);
}
}Thứ tự ở đây không chỉ là chuyện gọn gàng mà ảnh hưởng thẳng tới số tiền. Đơn 1.000.000đ, giảm 10% trước rồi trừ 50.000đ thì còn 850.000đ; trừ 50.000đ trước rồi giảm 10% thì còn 855.000đ. Vì vậy thứ tự áp dụng phải là quyết định nghiệp vụ được ghi rõ, không phải thứ tự ngẫu nhiên Spring tìm thấy bean. Hệ thống thật thường còn cần thêm luật loại trừ (voucher này không dùng chung với flash sale) và trần giảm giá tối đa cho mỗi đơn.
# cái bẫy thread-safety khi chạy đa luồng
Một trong những lỗi nặng nhất khi viết Strategy trong Spring Boot là để strategy giữ trạng thái (Stateful Strategy):
// NGUY HIỂM: Rò rỉ trạng thái giữa các request và các luồng!
@Component
public class BrokenDiscountStrategy implements PromotionStrategy {
private BigDecimal accumulatedDiscount = BigDecimal.ZERO; // BIẾN MUTABLE TRÊN SPRING SINGLETON!
@Override
public boolean isApplicable(OrderContext context) { return true; }
@Override
public int getExecutionOrder() { return 99; }
@Override
public DiscountResult applyDiscount(OrderContext context) {
// Nhiều requests đồng thời cùng sửa đổi accumulatedDiscount -> RACE CONDITION!
this.accumulatedDiscount = this.accumulatedDiscount.add(new BigDecimal("10000"));
return new DiscountResult("PROMO", this.accumulatedDiscount);
}
}Ngay cả khi chỉ có một thread, đoạn code này vẫn sai: bean là singleton nên accumulatedDiscount sống qua mọi request, đơn sau được giảm 20.000, đơn sau nữa 30.000... Chạy đa luồng thì tệ hơn, hai request cùng đọc rồi cùng ghi đè nhau nên con số còn không đoán trước được.
Nguyên tắc không được phá: mọi Strategy bean trong Spring Boot phải Stateless (không giữ trạng thái). Cần dữ liệu gì thì truyền qua tham số method (
OrderContext context), còn kết quả trả về phải là một DTO/Record immutable mới.
# checklist trước khi lên production (architect's checklist)
- 1. Stateless 100%: soát lại các class Strategy, đảm bảo không có biến instance mutable nào có thể gây race condition khi chạy đa luồng.
- 2. Tận dụng Functional Interface: strategy chỉ có 1 hàm tính toán đơn giản thì dùng Lambda hoặc
Function<T, R>, đừng tạo class riêng. - 3. Chốt thứ tự áp dụng (Ordering): khi kết hợp nhiều Strategy (Composite Pipeline), luôn ghi rõ độ ưu tiên bằng
@Orderhoặc methodgetOrder(). - 4. Fallback Strategy: luôn có một strategy mặc định (No-Op Strategy:
return input;) cho trường hợp không strategy nào khớp điều kiện. - 5. Đo thời gian chạy: gắn metric đo thời gian chạy của từng Strategy, để bắt được thuật toán nào đang kéo P99 Latency của đơn hàng lên.
# lời kết
Strategy Pattern không chỉ là bài tập đổi if-else trong sách giáo khoa. Dùng đúng chỗ, nó cho phép thêm một thuật toán mới mà không phải sửa luồng điều phối cũ.
Strategy chỉ có một hàm đơn giản thì dùng lambda; strategy cần dependency (repository, config) hoặc cần test riêng thì để thành bean Spring. Còn nếu chỉ có hai nhánh và không có dấu hiệu sẽ thêm, một câu if vẫn dễ đọc hơn cả một bộ interface.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.