Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

đừng cứ if-else mãi như thế nữa!

Tung Bui Thanh (TungDaDev)10 mins read

If else.webp

Lăn vào mấy dự án cũ chạy từ đời nào, bạn sẽ phải bơi trong hàng ngàn dòng code già cỗi từ trên xuống dưới. Xui thì còn gặp mấy đoạn logic toàn if-else là if-else. Thử tưởng tượng vài chục cái if-else nối nhau, chẳng khác gì đi trong hẻm, rẽ xong lại phải chọn rẽ tiếp ...

Bài này gom vài cách để bớt if-else, giảm bớt rẽ nhánh cho code dễ đọc, dễ hiểu hơn hẳn.

# có quá nhiều câu lệnh if-else

Trước khi vào từng cách, lấy một đoạn Java mẫu có cả chùm if-else, rồi mình sửa nó theo nhiều kiểu khác nhau:

public class ShippingCostCalculator {
    public double calculateShippingCost(String shippingType, double weight) {
        if (shippingType.equals("STANDARD")) {
            return weight * 5.0;
        } else if (shippingType.equals("EXPRESS")) {
            return weight * 10.0;
        } else if (shippingType.equals("SAME_DAY")) {
            return weight * 20.0;
        } else if (shippingType.equals("INTERNATIONAL")) {
            return weight * 50.0;
        } else if (shippingType.equals("OVERNIGHT")) {
            return weight * 30.0;
        }
        return 0;
    }
}

Đoạn code trên tính phí ship theo loại hình vận chuyển.

# sử dụng Enum

Giờ thử dùng Enum thay cho đống if-else.

public enum ShippingType {
    STANDARD {
        @Override
        public double getCost(double weight) {
            return weight * 5.0;
        }
    },
    EXPRESS {
        @Override
        public double getCost(double weight) {
            return weight * 10.0;
        }
    },
    SAME_DAY {
        @Override
        public double getCost(double weight) {
            return weight * 20.0;
        }
    },
    INTERNATIONAL {
        @Override
        public double getCost(double weight) {
            return weight * 50.0;
        }
    },
    OVERNIGHT {
        @Override
        public double getCost(double weight) {
            return weight * 30.0;
        }
    };
 
    public abstract double getCost(double weight);
}

Hàm tính phí giờ chỉ cần nhận ShippingType và weight:

public class ShippingCostCalculator {
 
    public double calculateShippingCost(ShippingType shippingType, double weight) {
        return shippingType.getCost(weight);
    }
}

Rồi ... chương trình chính nhìn gọn và dễ đọc hơn hẳn.

public class MainCost {
    public static void main(String[] args) {
        var calculator = new ShippingCostCalculator();
        var cost = calculator.calculateShippingCost(ShippingType.EXPRESS, 2.5);
        System.out.println("Shipping cost: " + cost);
    }
}

Cả chùm if-else giờ gọn lại còn hai dòng. Chạy hàm main thử xem kết quả.

Nếu đầu vào vẫn là chuỗi (từ request, từ file cấu hình) thì đổi sang enum bằng ShippingType.valueOf("EXPRESS"). Chuỗi không khớp tên nào thì valueOf ném IllegalArgumentException, rõ ràng hơn kiểu lặng lẽ trả về 0 như bản if-else ban đầu.

# ưu điểm:

  • Dễ mở rộng: thêm loại vận chuyển mới chỉ cần khai báo thêm vào enum và viết hàm xử lý tương ứng.
  • Dễ bảo trì, dễ hiểu: logic của từng loại vận chuyển nằm riêng một chỗ, nhìn là hiểu.

# nhược điểm:

  • Mở rộng có giới hạn: thêm loại vận chuyển thì dễ, nhưng khi cần thêm tham số thì Enum bắt đầu không hợp, code rối lên.
  • Khó thêm tham số mới: tham số càng nhiều, code càng cồng kềnh, khó quản lý.
  • Không kế thừa được: Enum không extends class khác được, nên khó tái sử dụng logic. Bù lại enum vẫn implements được interface, nên có thể cho nó implement luôn ShippingCostStrategy ở phần dưới.
  • Nói chung dùng Enum hợp với điều kiện đơn giản, ít tham số.

Với ví dụ này, các loại chỉ khác nhau ở đơn giá, nên còn một cách gọn hơn nữa là đưa đơn giá vào constructor của enum, khỏi override từng hằng:

public enum ShippingType {
    STANDARD(5.0), EXPRESS(10.0), SAME_DAY(20.0), INTERNATIONAL(50.0), OVERNIGHT(30.0);
 
    private final double ratePerKg;
 
    ShippingType(double ratePerKg) {
        this.ratePerKg = ratePerKg;
    }
 
    public double getCost(double weight) {
        return weight * ratePerKg;
    }
}

Override từng hằng như ở trên chỉ đáng khi công thức của mỗi loại thật sự khác nhau.

# dùng Factory Pattern

Vẫn đoạn code ở trên, lần này mình làm thế này:

Tạo interface ShippingCostStrategy.

public interface ShippingCostStrategy {
    double calculate(double weight);
}

Tiếp theo, mỗi loại giao hàng một class riêng, implement interface vừa tạo.

public class StandardShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 5.0;
    }
}
 
public class ExpressShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 10.0;
    }
}
 
public class SameDayShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 20.0;
    }
}
 
public class InternationalShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 50.0;
    }
}
 
public class OvernightShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 30.0;
    }
}

Giờ tạo một class Factory để chọn đúng Strategy theo loại vận chuyển.

public class ShippingCostFactory {
    private static final Map<String, ShippingCostStrategy> strategies = new HashMap<>();
 
    static {
        strategies.put("STANDARD", new StandardShipping());
        strategies.put("EXPRESS", new ExpressShipping());
        strategies.put("SAME_DAY", new SameDayShipping());
        strategies.put("INTERNATIONAL", new InternationalShipping());
        strategies.put("OVERNIGHT", new OvernightShipping());
    }
 
    public static ShippingCostStrategy getStrategy(String shippingType) {
        ShippingCostStrategy strategy = strategies.get(shippingType);
        if (strategy == null) {
            throw new IllegalArgumentException("Invalid shipping type: " + shippingType);
        }
        return strategy;
    }
}

Còn lại chỉ việc gọi ra dùng.

public class ShippingCostCalculator {
    public double calculateShippingCost(String shippingType, double weight) {
        ShippingCostStrategy strategy = ShippingCostFactory.getStrategy(shippingType);
        return strategy.calculate(weight);
    }
}

# ưu điểm của Factory Pattern:

  • Dễ mở rộng: thêm loại giao hàng mới chỉ cần viết thêm class và đăng ký vào Factory, không phải đụng vào code chính.
  • Tách logic rõ ràng: logic tính phí nằm riêng, dễ quản lý, dễ bảo trì.
  • Linh hoạt: Factory có thể trả về cách xử lý khác nhau tuỳ theo các yếu tố khác nữa.

# dùng Strategy Pattern

Nói trước: cách cài đặt nhìn khá giống Factory, nhưng mục đích dùng thì hơi khác.

public interface ShippingCostStrategy {
    double calculate(double weight);
}
public class StandardShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 5.0;
    }
}
 
public class ExpressShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 10.0;
    }
}
 
public class SameDayShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 20.0;
    }
}
 
public class InternationalShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 50.0;
    }
}
 
public class OvernightShipping implements ShippingCostStrategy {
    @Override
    public double calculate(double weight) {
        return weight * 30.0;
    }
}

Giờ tạo class ShippingContext để quản lý các Strategy.

public class ShippingCostContext {
    private ShippingCostStrategy strategy;
 
    public void setStrategy(ShippingCostStrategy strategy) {
        this.strategy = strategy;
    }
 
    public double calculateShippingCost(double weight) {
        return strategy.calculate(weight);
    }
}

Lúc này hàm tính phí trông như sau:

public class ShippingCostCalculator {
 
    private static final Map<String, ShippingCostStrategy> strategies = new HashMap<>();
 
    static {
        strategies.put("STANDARD", new StandardShipping());
        strategies.put("EXPRESS", new ExpressShipping());
        strategies.put("SAME_DAY", new SameDayShipping());
        strategies.put("INTERNATIONAL", new InternationalShipping());
        strategies.put("OVERNIGHT", new OvernightShipping());
    }
 
    private final ShippingCostContext context = new ShippingCostContext();
 
    public double calculateShippingCost(String shippingType, double weight) {
        ShippingCostStrategy strategy = strategies.get(shippingType);
        if (strategy == null) {
            throw new IllegalArgumentException("Invalid shipping type: " + shippingType);
        }
        context.setStrategy(strategy);
        return context.calculateShippingCost(weight);
    }
}
public class MainCost {
 
    public static void main(String[] args) {
        ShippingCostCalculator calculator = new ShippingCostCalculator();
 
        double weight = 10.0;
 
        String shippingType1 = "STANDARD";
        double cost1 = calculator.calculateShippingCost(shippingType1, weight);
        System.out.println("Shipping cost for " + shippingType1 + ": " + cost1);
 
        String shippingType2 = "EXPRESS";
        double cost2 = calculator.calculateShippingCost(shippingType2, weight);
        System.out.println("Shipping cost for " + shippingType2 + ": " + cost2);
 
        String shippingType3 = "SAME_DAY";
        double cost3 = calculator.calculateShippingCost(shippingType3, weight);
        System.out.println("Shipping cost for " + shippingType3 + ": " + cost3);
 
        String shippingType4 = "INTERNATIONAL";
        double cost4 = calculator.calculateShippingCost(shippingType4, weight);
        System.out.println("Shipping cost for " + shippingType4 + ": " + cost4);
 
        String shippingType5 = "OVERNIGHT";
        double cost5 = calculator.calculateShippingCost(shippingType5, weight);
        System.out.println("Shipping cost for " + shippingType5 + ": " + cost5);
    }
}

Tóm lại hai cách trên: Strategy Pattern lo phần tính phí ra sao, còn Factory Pattern lo chọn cách tính nào theo loại vận chuyển.

Có một chỗ cần cẩn thận trong ShippingCostCalculator ở trên: context là field dùng chung, mỗi lần gọi lại setStrategy rồi mới tính. Nếu calculator này là singleton (ví dụ một Spring bean) và hai thread gọi cùng lúc, thread A vừa set STANDARD thì thread B set EXPRESS, thread A tính ra giá của EXPRESS. Trường hợp này cứ gọi thẳng strategy.calculate(weight) là an toàn, Context có setStrategy chỉ hợp khi mỗi context thuộc về một đối tượng riêng (một đơn hàng, một phiên làm việc).

# dùng Stream API và Map

Cách cuối là dùng Stream API với Map cho code ngắn và dễ đọc hơn. Thay vì một đống điều kiện, mình dùng Map để map từng loại vận chuyển sang cách tính phí.

public class ShippingCostCalculator {
 
    private static final Map<String, Double> shippingCosts = new HashMap<>();
 
    static {
        shippingCosts.put("STANDARD", 5.0);
        shippingCosts.put("EXPRESS", 10.0);
        shippingCosts.put("SAME_DAY", 20.0);
        shippingCosts.put("INTERNATIONAL", 50.0);
        shippingCosts.put("OVERNIGHT", 30.0);
    }
 
    public double calculateShippingCost(String shippingType, double weight) {
        return shippingCosts.entrySet().stream()
            .filter(entry -> entry.getKey().equalsIgnoreCase(shippingType))
            .map(Map.Entry::getValue)
            .findFirst()
            .orElse(0.0)
            * weight;
    }
 
    public static void main(String[] args) {
        ShippingCostCalculator calculator = new ShippingCostCalculator();
 
        double weight = 10.0;
 
        String shippingType1 = "STANDARD";
        double cost1 = calculator.calculateShippingCost(shippingType1, weight);
        System.out.println("Shipping cost for " + shippingType1 + ": " + cost1);
 
        String shippingType2 = "EXPRESS";
        double cost2 = calculator.calculateShippingCost(shippingType2, weight);
        System.out.println("Shipping cost for " + shippingType2 + ": " + cost2);
 
        String shippingType3 = "SAME_DAY";
        double cost3 = calculator.calculateShippingCost(shippingType3, weight);
        System.out.println("Shipping cost for " + shippingType3 + ": " + cost3);
 
        String shippingType4 = "INTERNATIONAL";
        double cost4 = calculator.calculateShippingCost(shippingType4, weight);
        System.out.println("Shipping cost for " + shippingType4 + ": " + cost4);
 
        String shippingType5 = "OVERNIGHT";
        double cost5 = calculator.calculateShippingCost(shippingType5, weight);
        System.out.println("Shipping cost for " + shippingType5 + ": " + cost5);
 
        String invalidType = "INVALID";
        double invalidCost = calculator.calculateShippingCost(invalidType, weight);
        System.out.println("Shipping cost for " + invalidType + ": " + invalidCost);
    }
}

# ưu điểm:

  • Code gọn hơn: thêm hay sửa cách tính phí mà không phải đụng tới cấu trúc điều kiện.
  • Hiệu suất tốt hơn: các strategy nằm hết trong Map, rõ ràng và tra nhanh. Có điều code ở trên dùng stream duyệt hết entrySet() (để so equalsIgnoreCase), tức là bỏ mất cái lợi tra O(1) của HashMap. Muốn tra nhanh thật thì chuẩn hoá key rồi tra thẳng: shippingCosts.getOrDefault(shippingType.toUpperCase(), 0.0) * weight.
  • Dễ bảo trì: strategy quản lý tập trung một chỗ nên dễ hiểu, dễ sửa.

Stream API và Map khá tiện. Mở rộng thì không bằng Factory hay Strategy, nhưng với trường hợp đơn giản thì vẫn dùng ngon. Chỉ lưu ý orElse(0.0) sẽ âm thầm tính phí 0 cho loại vận chuyển gõ sai, nhiều khi ném exception như bản Factory lại an toàn hơn.

# lời kết

Áp dụng mấy cách viết này vào dự án thật thì vẫn phải cân nhắc performance, khả năng mở rộng và chuyện bảo trì, nhất là khi dự án đã phức tạp sẵn.

Còn nếu đang dùng Java 14 trở lên mà chỉ có vài nhánh, switch expression (case STANDARD -> weight * 5.0;) cũng gọn chẳng kém. Đi kèm enum thì compiler còn báo lỗi khi bạn quên xử lý một hằng, khỏi lo sót nhánh. Thêm một chuyện ngoài lề: ví dụ dùng double cho dễ đọc, tính tiền thật thì nên dùng BigDecimal để khỏi dính sai số làm tròn.

Mấy cách này làm code nhìn xịn xò con bò cười hơn thật, nhưng thay vì if-else một phát là xong thì giờ bạn phải define thêm vài class, implement nhiều hơn. Anw, cái gì cũng có giá của nó mà :))))

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 😎 👍🏻 🚀 🔥.

Reference: