SSV
+1

Clean code - P4 : Error Handling

Xử lý lỗi là một trong những điều chúng ta phải làm khi lập trình. Vậy xử lý lỗi liệu có quan trọng?

Use Exceptions Rather Than Return Codes

Hãy cùng xem đoạn code dưới đây:

public class DeviceController {
   ...
   public void sendShutDown() {
      DeviceHandle handle = getHandle(DEV1);
     // Check the state of the device
      if (handle != DeviceHandle.INVALID) {
         // Save the device status to the record field
         DeviceRecord record = retrieveDeviceRecord(handle);
         // If not suspended, shut down
         if (record.getStatus() != DEVICE_SUSPENDED) {
            pauseDevice(handle);
            clearDeviceWorkQueue(handle);
            closeDevice(handle);
         } else {
            logger.log("Device suspended. Unable to shut down");
         }
      } else {
         logger.log("Invalid handle for: " + DEV1.toString());
      }
   }
   ...
}

Một sự lộn xộn đến khó chịu, nó gây sự khó hiểu với người đọc code. Tiếp, chúng ta cùng xem đoạn code dưới đây sau khi được refactor.

public class DeviceController {
   ...
   public void sendShutDown() {
      try {
         tryToShutDown();
      } catch (DeviceShutDownError e) {
         logger.log(e);
      }
   }

   private void tryToShutDown() throws DeviceShutDownError {
      DeviceHandle handle = getHandle(DEV1);
      DeviceRecord record = retrieveDeviceRecord(handle);

      if (record.getStatus() != DEVICE_SUSPENDED) {
            pauseDevice(handle);
            clearDeviceWorkQueue(handle);
            closeDevice(handle);
      } else {
            logger.log("Device suspended. Unable to shut down");
      }
   }

   private DeviceHandle getHandle(DeviceID id) {
      DeviceHandle handle = ... xử lý
      if (handle == DeviceHandle.INVALID) {
          throw new DeviceShutDownError("Invalid handle for: " + id.toString());
      } else {
          return handle;
      }
   }
   ...
}

Rõ ràng đoạn code dưới đã tốt hơn rất nhiều khi tách biệt được quan hệ lộn xộn giữa thuật toán cho thiết bị tắt máy và xử lý lỗi.

Write Your Try-Catch-Finally Statement First

Try - tương tự Transactions trong khi làm việc với Databases
Catch - tương tự rollback, tức đoạn mã trong try không xảy ra, không được thực thi.

Sử dụng try-catch-finally là một cách tốt để bắt đầu viết code khi bạn đang viết code và ném đi những ngoại lệ. Điều này giúp chúng ta định nghĩa được những gì mà người dùng nên mong đợi, không vấn đề gì xảy ra đối với các đoạn code ở trong try.

Ví dụ: Viết một đoạn code với mục đích truy cập file và đọc một số đối tượng theo sắp xếp theo thứ tự.
Chúng ta sẽ viết một Unit test cho trường hợp bắt ngoại lệ khi file không tồn tại.

@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
   sectionStore.retrieveSection("invalid - file");
}

Với ý định test case như trên chúng ta sẽ viết một bản nháp business sơ khai như sau:

public List<RecordedGrip> retrieveSection(String sectionName) {
   // dummy return until we have a real implementation
   return new ArrayList<RecordedGrip>();
}

Khi này chạy test trên sẽ thất bại do nó chưa ném ra ngoại lệ. Tiếp theo chúng ta thay đổi implements business trên, do đó mà nó cố gắng truy cập vào một tập tin không hợp lệ. Thao tác này thows một exception:

public List<RecordedGrip> retrieveSection(String sectionName) {
   try {
      FileInputStream stream = new FileInputStream(sectionName)
   } catch (Exception e) {
      throw new StorageException("retrieval error", e);
   }
   return new ArrayList<RecordedGrip>();
}

Khi này chạy test đã passes bởi vì nó đã bắt lấy một exception. Tái cấu trúc lại:

public List<RecordedGrip> retrieveSection(String sectionName) {
   try {
      FileInputStream stream = new FileInputStream(sectionName);
   } catch (FileNotFoundException e) {
      throw new StorageException("retrieval error”, e);
   } finally {
      stream.close();
   }
   return new ArrayList<RecordedGrip>();
}

Use Unchecked Exceptions

Checked : là các ngoại lệ được kiểm tra tại thời điểm biên dịch (compile).
Unchecked : là các ngoại lệ không được kiểm tra tại thời điểm biên dịch (compile).
Khi nào nên sử dụng Checked or Unchecked tham khảo tại : https://www.geeksforgeeks.org/checked-vs-unchecked-exceptions-in-java/

Provide Context with Exceptions

Mỗi trường hợp ngoại lệ ta nên cung cấp ngữ cảnh để xác định nguồn gốc và vị trí của lỗi.
Trong Java, ta sẽ nhận được một stack trace từ bất kỳ ngoại lệ nào để truy tìm lỗi. Tuy nhiên stack trace đó không cho ta biết ý nghĩ của các ngoại lệ không thành công.
Ta nên tạo một thông điệp báo lỗi và truyền cho chúng với ngoại lệ của chương trình bắn ra. Đề cập đến các hành động gây ra lỗi và loại lỗi.

Don't Return Null

Cùng xem đoạn code dưới:

public void registerItem(Item item) {
   if (item != null) {
      ItemRegistry registry = peristentStore.getItemRegistry();
      if (registry != null) {
         Item existing = registry.getItem(item.getID());
         if (existing.getBillingPeriod().hasRetailOwner()) {
            existing.register(item);
         }
      }
   }
}

Khi chúng ta trả về null, chúng ta tự tạo công việc cho bản thân và thêm vấn đề cho người gọi nó. Tất cả vấn đề chính là thiếu mất một kiểm tra null để rồi gửi ứng dụng bắn ra exception, một exception ra khỏi tầm kiểm soát.

Thực tế là kiểm tra null ở if lồng thứ hai có được kiểm tra chưa? Điều gì xảy ra trong thời gian chạy nếu persistentStore là null? Chúng ta sẽ có một NullPointerException trong thời gian chạy, và một ai đó liệu có đang bắt NullPointerException ở cấp cao nhất hay không. Và điều nào thì cũng tồi tệ cả.

Tiếp tục, giả sử ta có đoạn code sau:

List<Employee> employees = getEmployees();
if (employees != null) {
   for(Employee e : employees) {
      totalPay += e.getPay();
   }
}

Bây giờ getEmployees có thể trả về null, nhưng nó có cần thiết phải làm như vậy không? Nếu chúng ta thay đổi getEmployees rằng nó trả về một danh sách rỗng, chúng ta có thể clean code như sau:

for(Employee e : employees) {
   totalPay += e.getPay();
}

public List<Employee> getEmployees() {
   if( .. there are no employees .. ) {
      return Collections.emptyList(); 
   }
   return // Trả về một danh sách xác định trước không thay đổi trong Java
}

Nếu code như vậy sẽ giảm thiểu nguy cơ NullPointerExceptions và code sẽ sạch hơn.

Don't Pass Null

Trả về null từ phương thức đã là không tốt, nhưng vượt qua null còn tồi tệ hơn. Ta nên tránh passing null trong code bất cứ khi nào có thể.

public class MetricsCalculator
{
   public double xProjection(Point p1, Point p2) {
      return (p2.x – p1.x) * 1.5;
   }
   …
}

Điều gì xảy ra nếu đi qua một tham số gán null? calculator.xProjection(null, new Point(12, 13));
Chúng ta sẽ có một NullPointerException. Sửa chữa lại:

public class MetricsCalculator
{
   public double xProjection(Point p1, Point p2) {
      if (p1 == null || p2 == null) {
         throw InvalidArgumentException(
            "Invalid argument for MetricsCalculator.xProjection");
      }
      return (p2.x – p1.x) * 1.5;
   }
}

Có một thay thế khác tốt hơn:

public class MetricsCalculator
{
   public double xProjection(Point p1, Point p2) {
      assert p1 != null : "p1 should not be null";
      assert p2 != null : "p2 should not be null";
      return (p2.x – p1.x) * 1.5;
   }
}

Trong hầu hết các ngôn ngữ lập trình không có cách nào đối phó với việc thông qua một Null với một lời gọi vô tình. Bởi vì đây là trường hợp tiêp cận với cấm truyền Null theo mặc định. Khi làm ta có thể code với sự hiểu biết rằng một Null xuất hiện trong danh sách tham số là dấu hiệu của vấn đề và kết thúc nó với ít lỗi bất cẩn nhất,

Conclusion

Code sạch là code có thể đọc được, nhưng nó cũng cần phải mạnh mẽ. Đây không phải là điều mâu thuẫn. Chúng ta có thể viết code sạch và mạnh mẽ nếu chúng ta thấy được xử lý lỗi là một mối quan tâm riêng, đôi khi có thể xem nó như không phụ thuộc với logic cơ bản của chúng ta. Để đến mức độ chúng ta làm được điều đó, chúng ta cần lý giải về nó một cách độc lập, và chúng ta có thể tiến tới bước tiến lớn trong việc bảo trì code của chúng ta.


All Rights Reserved