Giảm thiểu if/else trong Java: Khi nào nên dùng Switch, Map và Strategy Pattern?
Trong quá trình phát triển Backend Java, chúng ta rất dễ gặp những đoạn code có nhiều if/else:
if ("EVN".equals(provider)) {
// EVN logic
} else if ("WATER".equals(provider)) {
// Water logic
} else if ("TUITION".equals(provider)) {
// Tuition logic
} else if ("INSURANCE".equals(provider)) {
// Insurance logic
}
Ban đầu đoạn code này hoàn toàn bình thường. Tuy nhiên, khi số lượng provider tăng lên 10, 20 hoặc nhiều hơn, conditional logic bắt đầu trở thành vấn đề về maintainability, testability và extensibility.
Vậy có phải cứ thấy nhiều if/else là phải loại bỏ?
Không.
Mục tiêu không phải là "xóa sạch if/else", mà là lựa chọn cách biểu diễn conditional logic phù hợp với bản chất của bài toán.
Trong bài viết này, chúng ta sẽ lần lượt xem xét:
- Khi nào nên giữ
if/else - Khi nào nên dùng
switch - Khi nào nên dùng
Map - Khi nào nên dùng Strategy Pattern
- Kết hợp Strategy Pattern với Spring Dependency Injection
- Những trường hợp không nên over-engineering
1. Không phải if/else nào cũng xấu
Ví dụ:
if (amount <= 0) {
throw new IllegalArgumentException("Invalid amount");
}
if (user == null) {
throw new IllegalArgumentException("User not found");
}
Đây là conditional logic rất đơn giản và dễ đọc.
Không có lý do gì để biến nó thành:
ValidationStrategy
AmountValidationStrategy
UserValidationStrategy
ValidationFactory
ValidationRegistry
Chỉ để loại bỏ hai câu if.
Trong trường hợp này, if giúp code dễ đọc hơn.
Một nguyên tắc đơn giản:
Không nên loại bỏ
if/elsechỉ vì muốn code "trông clean".
Điều cần quan tâm là conditional logic có đang làm cho code khó thay đổi hay không.
2. Khi nào nên sử dụng switch?
Giả sử chúng ta có một enum:
public enum ProviderType {
EVN,
WATER,
TUITION,
INSURANCE
}
Nếu logic chỉ đơn giản là phân nhánh theo một value cố định, switch có thể là lựa chọn tốt:
public PaymentResult process(
ProviderType provider,
PaymentRequest request
) {
return switch (provider) {
case EVN -> evnService.process(request);
case WATER -> waterService.process(request);
case TUITION -> tuitionService.process(request);
case INSURANCE -> insuranceService.process(request);
};
}
So với:
if (provider == ProviderType.EVN) {
...
} else if (provider == ProviderType.WATER) {
...
} else if (provider == ProviderType.TUITION) {
...
}
switch thể hiện rõ hơn rằng chúng ta đang thực hiện branching theo một value.
Khi nào switch phù hợp?
Thường phù hợp khi:
- Số lượng case không quá lớn
- Logic của mỗi case đơn giản
- Các case ít thay đổi
- Không có nhiều nơi phải xử lý cùng một loại conditional
Ví dụ:
switch (status) {
case SUCCESS -> handleSuccess();
case FAILED -> handleFailed();
case PENDING -> handlePending();
}
Đây hoàn toàn có thể là code tốt.
3. Khi nào nên sử dụng Map?
Có một trường hợp khác:
if (type == A) {
return handlerA;
}
if (type == B) {
return handlerB;
}
if (type == C) {
return handlerC;
}
Ở đây chúng ta thực chất đang làm một việc:
type -> handler
Vậy có thể biểu diễn trực tiếp bằng Map:
private final Map<ProviderType, PaymentStrategy> strategies;
Sau đó:
PaymentStrategy strategy = strategies.get(provider);
Ví dụ:
@Component
public class PaymentStrategyFactory {
private final Map<ProviderType, PaymentStrategy> strategies;
public PaymentStrategyFactory(
List<PaymentStrategy> strategies
) {
this.strategies = strategies.stream()
.collect(Collectors.toUnmodifiableMap(
PaymentStrategy::support,
Function.identity()
));
}
public PaymentStrategy get(ProviderType provider) {
PaymentStrategy strategy = strategies.get(provider);
if (strategy == null) {
throw new IllegalArgumentException(
"Unsupported provider: " + provider
);
}
return strategy;
}
}
Ở đây Map không nhất thiết làm hệ thống nhanh hơn if/else.
Mục đích chính là:
Biến một tập conditional logic thành một cấu trúc mapping rõ ràng.
Java Map cung cấp các operation như get, put, compute, computeIfAbsent... để quản lý mapping giữa key và value.
4. Khi business logic bắt đầu khác nhau → Strategy Pattern
Đây là trường hợp quan trọng nhất.
Giả sử payment system hỗ trợ:
EVN
WATER
TUITION
INSURANCE
AIRLINE
SECURITIES
Mỗi provider có cách xử lý khác nhau.
Ví dụ:
if (provider == EVN) {
validate();
callEvnApi();
parseEvnResponse();
saveTransaction();
} else if (provider == WATER) {
validate();
callWaterApi();
parseWaterResponse();
saveTransaction();
} else if (provider == TUITION) {
validate();
callUniversityApi();
parseUniversityResponse();
saveTransaction();
}
Đây là lúc if/else bắt đầu trở thành vấn đề.
Bởi vì mỗi branch không còn là một operation đơn giản.
Mỗi branch đang chứa một business behavior riêng.
Đây là lúc Strategy Pattern phù hợp.
5. Xây dựng PaymentStrategy
Đầu tiên tạo interface:
public interface PaymentStrategy {
ProviderType support();
PaymentResult process(PaymentRequest request);
}
Sau đó mỗi provider có một implementation riêng.
EVN
@Component
public class EvnPaymentStrategy implements PaymentStrategy {
@Override
public ProviderType support() {
return ProviderType.EVN;
}
@Override
public PaymentResult process(PaymentRequest request) {
// EVN-specific validation
// Call EVN API
// Parse EVN response
// Build payment result
return ...;
}
}
Water
@Component
public class WaterPaymentStrategy implements PaymentStrategy {
@Override
public ProviderType support() {
return ProviderType.WATER;
}
@Override
public PaymentResult process(PaymentRequest request) {
// Water-specific logic
return ...;
}
}
Tuition
@Component
public class TuitionPaymentStrategy implements PaymentStrategy {
@Override
public ProviderType support() {
return ProviderType.TUITION;
}
@Override
public PaymentResult process(PaymentRequest request) {
// Tuition-specific logic
return ...;
}
}
Bây giờ business logic đã được tách ra:
PaymentStrategy
|
+-- EvnPaymentStrategy
|
+-- WaterPaymentStrategy
|
+-- TuitionPaymentStrategy
|
+-- InsurancePaymentStrategy
6. Spring Dependency Injection giúp Strategy Pattern trở nên mạnh hơn
Spring có thể inject toàn bộ các implementation:
@Component
public class PaymentStrategyFactory {
private final Map<ProviderType, PaymentStrategy> strategies;
public PaymentStrategyFactory(
List<PaymentStrategy> strategies
) {
this.strategies = strategies.stream()
.collect(Collectors.toUnmodifiableMap(
PaymentStrategy::support,
Function.identity()
));
}
public PaymentStrategy get(ProviderType provider) {
PaymentStrategy strategy = strategies.get(provider);
if (strategy == null) {
throw new IllegalArgumentException(
"Unsupported provider: " + provider
);
}
return strategy;
}
}
Service chính lúc này rất đơn giản:
@Service
public class PaymentService {
private final PaymentStrategyFactory factory;
public PaymentService(
PaymentStrategyFactory factory
) {
this.factory = factory;
}
public PaymentResult process(
PaymentRequest request
) {
PaymentStrategy strategy =
factory.get(request.provider());
return strategy.process(request);
}
}
Điểm quan trọng ở đây:
PaymentService
|
v
PaymentStrategyFactory
|
+---- EVN --------> EvnPaymentStrategy
|
+---- WATER ------> WaterPaymentStrategy
|
+---- TUITION ----> TuitionPaymentStrategy
PaymentService không cần biết chi tiết EVN, Water hay Tuition xử lý như thế nào.
7. Khi thêm provider mới thì sao?
Giả sử hệ thống cần thêm FWD.
Với cách sử dụng if/else, chúng ta có thể phải sửa:
if (provider == EVN) {
...
} else if (provider == WATER) {
...
} else if (provider == TUITION) {
...
} else if (provider == FWD) {
...
}
Nếu logic provider xuất hiện ở nhiều service:
PaymentService
InquiryService
RefundService
NotificationService
thì có thể phải sửa rất nhiều nơi.
Với Strategy Pattern:
@Component
public class FwdPaymentStrategy implements PaymentStrategy {
@Override
public ProviderType support() {
return ProviderType.FWD;
}
@Override
public PaymentResult process(PaymentRequest request) {
// FWD logic
return ...;
}
}
Các class khác không cần thay đổi.
Spring sẽ inject implementation mới vào List<PaymentStrategy>.
Đây là một trong những lợi ích lớn của polymorphism:
Thêm behavior mới bằng cách thêm implementation thay vì liên tục sửa conditional logic ở các class hiện tại.
8. Nhưng Strategy Pattern cũng có trade-off
Không nên nghĩ:
if/else = bad
Strategy = good
Thực tế:
if/else
↓
ít code
dễ đọc
ít abstraction
Strategy
↓
nhiều class
nhiều abstraction
dễ mở rộng
dễ test từng behavior
Ví dụ chỉ có:
if (status == SUCCESS) {
return "OK";
} else {
return "FAILED";
}
Việc tạo:
StatusStrategy
SuccessStrategy
FailedStrategy
StatusFactory
StatusRegistry
là over-engineering.
Code trở nên phức tạp hơn vấn đề ban đầu.
9. Một rule thực tế để quyết định
Có thể sử dụng rule sau:
2–3 điều kiện đơn giản
↓
if/else
Nhiều value cố định
↓
switch
Key → Handler
↓
Map
Mỗi type có business behavior riêng
↓
Strategy
Strategy được quản lý bởi Spring
↓
Strategy + DI + Registry
Nhưng vẫn cần nhìn vào business context.
10. Đừng tối ưu if/else vì performance
Một hiểu lầm khá phổ biến là:
if/else chậm
Map nhanh
Strategy nhanh hơn
Không nên suy luận như vậy.
Ví dụ:
if (type == A) {
...
} else if (type == B) {
...
}
không tự nhiên trở thành performance bottleneck chỉ vì có nhiều branch.
Trong Backend thực tế, latency thường bị chi phối bởi:
HTTP call
Database
Redis
RabbitMQ
Network
External API
Serialization
Connection pool
thay vì vài conditional statements.
Vì vậy:
Dùng Strategy để cải thiện architecture và maintainability, không phải để "tăng tốc if/else".
11. Production reality: Payment System
Trong một payment system, tôi thường thiết kế theo hướng:
Payment API
|
v
PaymentService
|
v
Strategy Registry
|
+--------------+--------------+
| | |
v v v
EVN WATER FWD
| | |
v v v
External API External API External API
Mỗi Strategy có thể có:
Validation
Retry
Timeout
Circuit Breaker
Metrics
Logging
Tracing
Idempotency
External API integration
Ví dụ metric:
payment_requests_total{provider="EVN"}
payment_requests_total{provider="WATER"}
payment_latency_seconds{provider="EVN"}
payment_latency_seconds{provider="WATER"}
Khi production có vấn đề, chúng ta có thể biết:
EVN
p95 = 180ms
WATER
p95 = 320ms
FWD
p95 = 950ms
Thay vì chỉ thấy:
PaymentService p95 = 700ms
12. Một lỗi khác: Strategy quá lớn
Không nên biến:
EvnPaymentStrategy
thành một class 1.500 dòng.
Strategy chỉ nên chịu trách nhiệm cho behavior đặc thù của provider.
Có thể tiếp tục tách:
EvnPaymentStrategy
|
+-- EvnClient
+-- EvnRequestMapper
+-- EvnResponseMapper
+-- EvnValidator
Khi đó architecture sẽ rõ ràng hơn:
PaymentService
|
v
Strategy
|
+---- Domain logic
|
+---- External client
|
+---- Mapper
|
+---- Validator
13. Kết luận
Không nên đặt mục tiêu:
"Làm sao để code không còn if/else?"
Mà nên đặt câu hỏi:
"Conditional này có đại diện cho một behavior có khả năng thay đổi hoặc mở rộng không?"
Nếu không:
if (amount <= 0) {
throw ...
}
Cứ giữ if.
Nếu chỉ phân nhánh value:
switch (status) {
...
}
Nếu chỉ mapping:
Map<Type, Handler>
Nếu mỗi type có business behavior riêng:
Strategy Pattern
Đặc biệt trong các hệ thống Backend như Payment, Billing, Provider Integration, Strategy Pattern kết hợp với Spring DI là một cách rất thực tế để tránh việc if/else phát triển thành một "God method".
References
-
Martin Fowler — Replace Conditional with Polymorphism https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html
-
Oracle Java Documentation —
Maphttps://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/Map.html
Next step
Một chủ đề rất đáng đào sâu sau bài này là:
"Strategy Pattern + Factory Pattern + Spring DI khác nhau như thế nào, và tại sao không nên tạo Factory một cách máy móc?"
Đây là phần giúp phân biệt giữa việc biết design pattern và thực sự biết cách dùng chúng trong một production Spring Boot application.
All rights reserved