Nguyên tắc thiết kế API: Đừng cám dỗ người dùng chia cho số không

Khi thiết kế API, có những quyết định tưởng chừng vô hại nhưng lại có thể dẫn đến những lỗi nghiêm trọng mà người phát triển khó lường trước. Một trong những lỗi kinh điển là phép chia cho số không (divide‑by‑zero). Bài viết này kể về một tình huống thực tế trong đó API đã vô tình “mời gọi” người dùng thực hiện phép chia cho zero, và cách khắc phục nó.

Bài toán: Biểu diễn tập giá trị hỗ trợ

Một nhóm phát triển đưa ra một API cho phép biểu diễn một tập các giá trị được hỗ trợ dưới dạng ba con số:

  • Giá trị tối thiểu (minimum).
  • Bước nhảy (increment).
  • Giá trị tối đa (maximum).

Các giá trị hợp lệ là: giá trị tối thiểu, rồi cộng thêm bội số nguyên lần bước nhảy, cho đến và bao gồm cả giá trị tối đa.

Ví dụ: nếu minimum = 5, increment = 10, maximum = 30, các giá trị hợp lệ là:

  • 5 (minimum)
  • 15 (minimum + 1 × increment)
  • 25 (minimum + 2 × increment)
  • 30 (maximum)

Nhóm cũng lưu ý rằng nếu increment bằng 0, thì tập giá trị hỗ trợ chỉ gồm minimum và maximum.

Vấn đề: Mời gọi chia cho zero

Thiết kế này vô tình tạo ra cái bẫy: người dùng có thể viết một hàm tìm giá trị hỗ trợ gần nhất với một giá trị mong muốn, chẳng hạn:


int closestSupportedValue(int desired) {
int nearest = minimum +
((desired - minimum + increment/2) / increment) * increment;
return std::clamp(nearest, minimum, maximum);
}

Hàm này hoạt động tốt cho đến khi increment bằng 0. Lúc đó, biểu thức increment/2 và phép chia (desired - minimum + increment/2) / increment sẽ gây lỗi chia cho zero.

Trong thực tế, có thể nhóm phát triển chưa bao giờ gặp trường hợp increment = 0 trong quá trình kiểm thử, vì tất cả phần cứng họ thử nghiệm đều hỗ trợ nhiều giá trị. Họ không nhận ra rằng có những thiết bị chỉ hỗ trợ 1 hoặc 2 giá trị, và khi đó increment có thể được đặt bằng 0.

Giải pháp: Loại bỏ zero khỏi API

Tôi đề nghị nhóm loại bỏ giá trị 0 khỏi API. Nếu chỉ có hai giá trị được hỗ trợ, hãy đặt increment bằng maximum – minimum. Nếu chỉ có một giá trị duy nhất, đặt increment = 1 (hoặc bất kỳ giá trị dương nào khác, miễn là không gây tràn số).

Ý tưởng là: đừng để người dùng rơi vào tình huống phải chia cho zero. Hãy chọn một giá trị increment mà dù có dùng trong phép chia thì cũng không gây lỗi.

Bonus chatter: Câu chuyện từ đội ngũ lưới điện

Đội ngũ phát triển hệ thống dự báo lưới điện cũng mắc phải bẫy tương tự. Lớp PowerGridForecast chứa một vector các đối tượng PowerGridData, bắt đầu từ StartTime, mỗi phần tử đại diện cho một khoảng thời gian BlockDuration. Phần tử thứ n mô tả dự báo bắt đầu tại StartTime + n × BlockDuration và kéo dài BlockDuration.

Để tìm block tương ứng với một thời điểm cụ thể, người ta có thể viết:


PowerGridData FindForecastForDateTime(
PowerGridForecast forecast, DateTime time) {
var elapsed = time - forecast.StartTime;
var index = elapsed / forecast.BlockDuration;
if (index < 0 || index >= forecast.Forecast.Count) {
return null; // không có dự báo cho index này
}
return forecast.Forecast[index];
}

Nhóm quyết định rằng nếu không có dự báo, thuộc tính Forecast trả về vector rỗng (điều này tốt), còn BlockDuration được đặt bằng 0 (điều này không tốt). Họ nghĩ rằng khi không có dự báo thì kích thước block không quan trọng, nhưng họ quên mất rằng phép tính index sẽ thực hiện chia cho zero.

Giải pháp: thay vì đặt BlockDuration = 0, hãy chọn một giá trị khác không, ví dụ một giờ. Dù giá trị này không có ý nghĩa thực tế khi không có dự báo, nó sẽ tránh được lỗi chia cho zero.

Kết luận

Thiết kế API cần phải “thân thiện” với người dùng, không được tạo ra những tình huống dễ gây lỗi. Việc cho phép increment = 0 (hay bất kỳ tham số nào có thể dẫn đến chia cho zero) là một cái bẫy tiềm ẩn. Hãy loại bỏ những giá trị “nguy hiểm” khỏi miền giá trị hợp lệ của API, hoặc chọn giá trị mặc định an toàn. Như vậy, người dùng API sẽ không phải đau đầu với những lỗi chia cho zero bất ngờ, và code của họ sẽ bền vững hơn.

Bài viết dựa trên chia sẻ của Raymond Chen từ blog The Old New Thing.

Chỉ mục