0

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/else chỉ 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

  1. Martin Fowler — Replace Conditional with Polymorphism https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html

  2. Oracle Java Documentation — Map https://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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí