Post

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

Khi dấng thân vào các dự án thực tế cũ kỹ đã tồn tại từ rất lâu. Bạn có thể sẽ đắm chìm vào hàng ngàn dòng code già nua từ trên xuống dưới, và đôi khi, thật không may khi gặp phải những đoạn code cứ if-else để xử lý các logic. Cứ tưởng tượng mà xem, vài chục cái if-else liên tục như thế, khác nào đi chặt hẻm, cứ rẽ rồi lại phải lựa chọn để rẽ tiếp …

Trong bài viết này, có vài cách tối ưu hóa việc sử dụng if-else, hạn chế code có quá nhiều điều kiện rẽ nhánh và giúp code trở nên dễ đọc và dễ hiểu hơn nhiều.

Đoạn code có quá nhiều câu lệnh if-else

Trước khi đi sâu vào chi tiết các phương pháp tối ưu hóa, hãy bắt đầu với một đoạn code demo Java mẫu có chứa nhiều điều kiện if-else để sau đó tối ưu hóa nó bằng nhiều cách khác nhau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
    }
}

Như bạn có thể thấy, đoạn mã trên tính phí vận chuyển dựa trên loại hình vận chuyển.

Sử dụng Enum

Bây giờ, chúng ta sẽ sử dụng Enum để thay thế các câu lệnh if-else.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);
}

Một hàm tính toán, chỉ cần truyền vào ShippingTypeweight:

1
2
3
4
5
6
public class ShippingCostCalculator {

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

Rồi … trông chương trình chính có vẻ ngắn gọn và dễ đọc hơn nhiều.

1
2
3
4
5
6
7
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);
    }
}

Như bạn có thể thấy, câu lệnh if-else phức tạp đã được đơn giản hóa thành hai dòng mã ngắn gọn và dễ hiểu. Hãy chạy hàm main để xem kết quả.

Ưu điểm:

  • Khả năng mở rộng: Có thể dễ dàng thêm các loại hình vận chuyển và giá trị mới, chỉ cần liệt kê chúng và định nghĩa phương thức xử lý tương ứng.
  • Code dễ bảo trì và dễ hiểu: Lý do xử lý cho từng phương thức vận chuyển được cô lập và rất dễ nắm bắt.

Nhược điểm:

  • Khả năng mở rộng hạn chế: Mặc dù có thể thêm các loại vận chuyển mới, nhưng khi cần nhiều tham số hơn, Enum trở nên không phù hợp và làm mã phức tạp.
  • Khó thêm tham số mới: Khi cần thêm nhiều tham số, mã sẽ trở nên cồng kềnh và khó quản lý.
  • Hạn chế kế thừa: Enum không thể kế thừa từ các lớp khác, làm giảm khả năng tái sử dụng logic.
  • Việc sử dụng Enum để tối ưu hóa thường phù hợp với các điều kiện đơn giản và ít tham số.

Tối ưu hóa với Factory Pattern

Vẫn với đoạn mã phức tạp ở trên, chúng ta sẽ tối ưu hóa nó theo cách sau:

Tạo một interface ShippingCostStrategy.

1
2
3
public interface ShippingCostStrategy {
    double calculate(double weight);
}

Tiếp theo, chúng ta sẽ tạo các class cụ thể cho từng loại hình giao hàng, triển khai interface đã tạo ở trên.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
    }
}

Bây giờ, chúng ta sẽ tạo một lớp Factory để xử lý việc định tuyến đến các Strategy dựa trên loại hình vận chuyển.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
    }
}

Bây giờ chỉ cần gọi và sử dụng nó.

1
2
3
4
5
6
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ễ dàng mở rộng: Thêm loại giao hàng mới chỉ cần phát triển thêm các lớp và cập nhật Factory mà không phải thay đổi mã lõi.
  • Phân tách logic rõ ràng: Logic tính phí được tách biệt, dễ quản lý và bảo trì.
  • Linh hoạt: Factory có thể trả về nhiều giải pháp khác nhau dựa trên các yếu tố khác, tăng tính linh hoạt.

Tối ưu hóa bằng Strategy Pattern

Trước khi đi vào chi tiết, hãy lưu ý rằng việc triển khai sẽ tương tự với Factory, nhưng mục đích sử dụng sẽ hơi khác biệt.

1
2
3
public interface ShippingCostStrategy {
    double calculate(double weight);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
    }
}

Bây giờ, chúng ta sẽ tạo lớp ShippingContext để quản lý các Strategy.

1
2
3
4
5
6
7
8
9
10
11
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 toán sẽ trông như thế này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
    }
}

Trong hai trường hợp trước, Strategy Pattern quản lý cách tính phí vận chuyển, trong khi Factory Pattern quyết định chiến lược nào sẽ được sử dụng dựa trên loại hình vận chuyển.

Tối ưu hóa bằng Stream API và Map

Bây giờ, chúng ta sẽ sử dụng Stream API và Map để tối ưu hóa quá trình này, giúp mã dễ đọc và ngắn gọn hơn. Thay vì sử dụng các cấu trúc điều kiện phức tạp, ta có thể tận dụng Map để ánh xạ các loại hình vận chuyển tới các Strategy tính phí.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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:

  • Đơn giản hóa code: Dễ dàng thêm hoặc thay đổi strategy tính phí mà không cần thay đổi cấu trúc điều kiện.
  • Hiệu suất tốt hơn: Quản lý các strategy một cách rõ ràng và nhanh chóng thông qua Map.
  • Dễ bảo trì: Mã trở nên dễ hiểu và bảo trì hơn vì các strategy được quản lý tập trung.

Stream API và Map cũng khá tiện lợi; mặc dù khả năng mở rộng không tốt bằng FactoryStrategy, nhưng nó vẫn là một lựa chọn khả thi cho các tình huống đơn giản.

Lời kết

Việc áp dụng các phương pháp code mới để tối ưu đôi khi cần quan tâm đến performance của hệ thống và khả năng mở rộng cũng như bảo trì khi làm việc trong môi trường dự án thực tế nhiều phức tạp.

Sử dụng các cách tối ưu phần nào góp phần làm code trông xịn xò con bò cười hơn nhưng thay vì chỉ cần if-else cái là xong, bạn phải define thêm vài class và implement nhiều hơn. Anw, cái gì cũng có cái giá của nó mà :))))

Bài viết mang tính chất “ghi chú, lưu trữ, chia sẻ và phi lợi nhuận”.
Nếu bạn thấy hữu ích, đừng quên chia sẻ với bạn bè và đồng nghiệp của mình nhé!

Happy coding! 😎 👍🏻 🚀 🔥

Reference:

This post is licensed under CC BY 4.0 by the author.