Loại bỏ if-else trong Java một cách tinh tế

Loại bỏ if-else trong Java một cách tinh tế
Đôi khi, việc có quá nhiều điều kiện if-else trong code không phải lúc nào cũng là cách tốt nhất để mọi người hiểu rõ code của bạn. Vì vậy, như là một người mới bắt đầu, hoặc ngay cả khi bạn đã khá giỏi trong công việc của mình, việc làm cho code của bạn trở nên "phức tạp" hơn nhưng vẫn dễ đọc là rất quan trọng.

Đôi khi, việc có quá nhiều điều kiện if-else trong code không phải lúc nào cũng là cách tốt nhất để mọi người hiểu rõ code của bạn. Vì vậy, như là một người mới bắt đầu, hoặc ngay cả khi bạn đã khá giỏi trong công việc của mình, việc làm cho code của bạn trở nên "phức tạp" hơn nhưng vẫn dễ đọc là rất quan trọng.

PHƯƠNG PHÁP THỨ 1: EARLY RETURN, EARLY EXIT

Nếu bạn dịch tên phương pháp này ra tiếng Việt, thì bạn sẽ nhận được ý nghĩa là "Trả về sớm", "Thoát sớm" thì bạn cũng có thể hiểu rằng phương pháp này sẽ làm gì đúng không.

Hãy xem một ví dụ sau đây, dưới đây là một đoạn code mà chúng ta thường gặp, chắc hẳn ai cũng đã từng rơi vào trường hợp như thế này:

Ví dụ 1:

public boolean isValid(String condition) {  
  boolean result;  
  if (condition != null){      
    if (condition.equals("hello") {        
      result = true;      
    } else {         
      result = false;      
    }  
  } else {    
    result= false;  
  }  
  return result;
}

Thoạt nhìn hàm isValid ở trên rất bình thường, nhưng nếu chúng ta phân tích kỹ 1 chút, thì chúng ta dễ dàng nhận ra hàm này sẽ trả về giá trị true chỉ khi tham số condition truyền vào có giá trị là "hello". Nên chúng ta có thể viết lại hàm này như sau

public boolean isValid(String condition) {  
  if (condition == null) {    
    return false;  
  }  
  if (condition.equals("hello") {
    return true  
  }  
  return false;
}

Có phải trông nó ổn hơn nhiều không?

Ví dụ 2:

bool ShouldFireNuclearWeapon(PossibleTarget target){    
  if (target != null) {
    if (target.IsFromEnemyNation) {
      if (target.IsCarryingWeapon) {                
        if (!target.IsCivillian) {                    
          return true;                
        } else {                    
          return false;                
        }            
      }            
      if (target.LivingBeingInformation.NumberOfLegs == 2) {
        if (target.LivingBeingInformation.Height < 1.50) {
          return false;                
        } else {                    
          return true;                
        }            
      }        
    }            
  }    
  return false;
}

Giả sử chúng ta đang phát triển hệ thống nhắm mục tiêu của robot, và hiện tại chúng ta đang làm việc với cách nó chọn mục tiêu của mình. Nếu nhìn vào phương thức ở trên, chúng ta không thể rõ ràng hiểu khi nào nó trả về true hoặc false. Và hãy tưởng tượng rằng đến một số thời điểm, chúng ta sẽ phải sửa đổi nó. Điều này sẽ không dễ dàng. Nhưng nếu nó được viết dưới dạng 1 cấu trúc đơn giản, chúng ta có thể nâng cấp robot dễ dàng hơn nhiều.

bool ShouldFireNuclearWeapon(PossibleTarget target){    
  if (target == null)        
    return false;    
  if (!target.IsFromEnemyNation)        
    return false;    
  if (target.IsCarryingWeapon)        
    return !target.IsCivillian;    
  if (target.LivingBeingInformation.NumberOfLegs == 2)        
    return target.LivingBeingInformation.Height > 1.50;    
  return false;    
}

Phương pháp này thường chỉ phù hợp với các cấu trúc đơn giản và chúng ta có thể kết thúc sớm để loại bỏ một số phần if-else không cần thiết

PHƯƠNG PHÁP THỨ 2: SỬ DỤNG BẢNG LIỆT KÊ (ENUMERATION)

Dưới đây tiếp tục là những đoạn code rất chi là bình thường

public String getLabel(int status) {    
  String label;    
  if (1 == status) {        
    label = "Pending";    
  } else if (2 == status) {        
    label = "Paid";    
  } else if (3 == status) {        
    label = "Success";    
  } else if (4 == status) {        
    label = "Failed";    
  }    
  return label;
}

Tất nhiên, nếu bạn là một "pro" thì có thể bạn sẽ nói rằng chẳng có "đứa" nào viết như thế này. Nhưng thực tế, nó rất phổ biến. Với phong cách cũng như đề bài này, thì rất tuyệt vời để sử dụng enum để giải quyết.

Trước tiên chúng ta sẽ định nghĩa ra 1 enum, hãy đặt tên nó là StatusLabelEnum.

@Getter
@AllArgsConstructorpublic enum StatusLabelEnum {    
  Padding(1, "Pending"),    
  Paid(2, "Paid"),    
  Success(3, "Success"),    
  Failed(4, "Failed"),;    
  private int status;    
  private String label;    
  public static String getLabelByStatus(int status) {        
    for (StatusLabelEnum labelEnum : StatusLabelEnum.values()) {            
      if (labelEnum.getStatus() == status) {                
        return labelEnum.getLabel();            
      }        
    }        
    return "Unknown";    
  }
}

Với cách viết này, chúng ta có thể dễ dàng, hàm getLabel ban đầu của chúng ta có thể được viết gọn chỉ với 1 dòng đơn giản như sau:

public String getLabel(int status) {  
  return StatusLabelEnum.getLabelByStatus(status);
}

Tất nhiên, trong dự án thực tế, cách xử lý này không phải là tốt nhất. Cách tốt nhất có thể là trong cơ sở dữ liệu có một bảng cấu hình key-value. Cho nên dùng Enum(Bảng liệt kê) cũng là một giải pháp đơn giản nếu bạn đã xác định rõ những giá trị cần xử lý(giá trị cố định)

PHƯƠNG PHÁP THỨ 3: SỬ DỤNG OPTIONAL

Tôi chắc chắn rằng những người đọc bài này, đều đã từng phải thực hiện việc kiểm tra 1 giá trị nào đó có null hay không null, và nếu nó là null, một giá trị trả về hoặc một ngoại lệ được ném ra.

Dưới đây sẽ mô phỏng lại 1 trường hợp như thế

public int getOrderStatus(UUID id) {  
  Order order = getOrderById(id);  
  if (order == null) {      
    return 1;  
  } else {      
    return order.getOrderStatus();  
  }
}

Đối với loại mã này, chúng ta có thể sử dụng Optional để giải quyết một cách rất thanh lịch

public int getOrderStatus(UUID id) {  
  Order order = getOrderById(id);  
  return Optional.ofNullable(order).map(Order::getOrderStatus).orElse(1);
}

PHƯƠNG PHÁP THỨ 4: TABLE-DRIVEN

Phương pháp Table-driven là một phương thức cho phép bạn tra cứu thông tin trong một bảng mà không cần sử dụng quá nhiều if-else để tìm ra chúng. Dưới đây là một đoạn code bình thường khi chưa sử dụng phương pháp nào.

if ("code1".equals(action)) {    
  doAction1();
} else if ("code2".equals(action)) {    
  doAction2();
} else if ("code3".equals(action)) {    
  doAction3();
} else if ("code4".equals(action)) {    
  doAction4();
} else if ("code5".equals(action)) {    
  doAction5();
}

Còn đây là đoạn code được cải tiến sử dụng phương pháp Table-driven

//Definition
Map<String, Function<?> action> actionMap = new HashMap<>();
action.put("code1",() -> {doAction1()});
action.put("code2",() -> {doAction2()});
action.put("code3",() -> {doAction3()});
action.put("code4",() -> {doAction4()});
action.put("code5",() -> {doAction5()});
//use case
actionMap.get(action).apply();

Nhưng đối với cách thực hiện ở trên, thì đây cũng chưa không phải là cách tốt nhất, vì nó có thể dẫn đến các đoạn code bạn viết sẽ khá là rối. Để cải thiện, chúng ta có thể dựa trên tính trừu tượng của OOP bằng cách trừu tượng hóa doAction() thành một Class

Dưới đây là cách thực hiện

//1. Define interface
public interface ActionService {    
  void doAction();
}
//2. Define implementations
public class ActionService1 implements ActionService{    
  public void doAction() {        
    //do something    
  }}
//3. add to table
Map<String, ActionService> actionMap = new HashMap<>();
action.put("code1",new ActionService1());
action.put("code2",new ActionService2());
action.put("code3",new ActionService3());
action.put("code4",new ActionService4());
action.put("code5",new ActionService5());
//4. use itaction
Map.get(action).doAction();

Có phải nhìn nó đã rất "sáng sủa" và chuyên nghiệp hơn rồi đúng không.

Dưới đây là 4 cách giúp bạn loại bỏ if-else trong code của bạn. Hãy cân nhắc để áp dụng một cách hợp lý nhất nhé.