Tôi nghĩ rằng tôi đã đọc cùng một cuộc phỏng vấn Bruce Eckel mà bạn đã làm - và nó luôn làm tôi khó chịu. Trên thực tế, cuộc tranh luận được đưa ra bởi người được phỏng vấn (nếu đây thực sự là bài đăng mà bạn đang nói đến) Anders Hejlsberg, thiên tài MS đứng sau .NET và C #.
http://www.artima.com/intv/handcuffs.html
Fan mặc dù tôi là Hejlsberg và công việc của anh ấy, cuộc tranh luận này luôn khiến tôi không có thật. Về cơ bản, nó sôi sùng sục xuống:
"Các trường hợp ngoại lệ được kiểm tra là xấu vì các lập trình viên chỉ lạm dụng chúng bằng cách luôn bắt chúng và loại bỏ chúng dẫn đến các vấn đề bị ẩn và bỏ qua sẽ được trình bày cho người dùng".
Bằng cách "trình bày cho người dùng" Ý tôi là nếu bạn sử dụng ngoại lệ thời gian chạy, lập trình viên lười biếng sẽ bỏ qua nó (so với việc bắt nó với một khối bắt trống) và người dùng sẽ thấy nó.
Tóm tắt tóm tắt của đối số là "Các lập trình viên sẽ không sử dụng chúng đúng cách và không sử dụng chúng đúng cách còn tệ hơn là không có chúng" .
Có một số sự thật đối với lập luận này và trên thực tế, tôi nghi ngờ động lực của Goslings về việc không đặt các toán tử ghi đè lên Java xuất phát từ một đối số tương tự - chúng gây nhầm lẫn cho lập trình viên vì chúng thường bị lạm dụng.
Nhưng cuối cùng, tôi thấy đó là một lập luận không có thật của Hejlsberg và có thể là một bài hậu-hoc được tạo ra để giải thích sự thiếu thốn hơn là một quyết định được cân nhắc kỹ lưỡng.
Tôi sẽ lập luận rằng mặc dù việc sử dụng quá mức các ngoại lệ được kiểm tra là một điều xấu và có xu hướng dẫn đến việc xử lý cẩu thả của người dùng, nhưng việc sử dụng chúng đúng cách cho phép lập trình viên API mang lại lợi ích lớn cho lập trình viên API.
Bây giờ, lập trình viên API phải cẩn thận để không ném các ngoại lệ được kiểm tra khắp nơi, hoặc đơn giản là chúng sẽ gây khó chịu cho lập trình viên máy khách. Lập trình viên khách hàng rất lười biếng sẽ dùng đến để bắt (Exception) {}
như Hejlsberg cảnh báo và tất cả lợi ích sẽ bị mất và địa ngục sẽ xảy ra. Nhưng trong một số trường hợp, không có sự thay thế nào cho một ngoại lệ được kiểm tra tốt.
Đối với tôi, ví dụ kinh điển là API mở tệp. Mọi ngôn ngữ lập trình trong lịch sử ngôn ngữ (ít nhất là trên các hệ thống tệp) đều có API ở đâu đó cho phép bạn mở tệp. Và mọi lập trình viên khách hàng sử dụng API này đều biết rằng họ phải giải quyết trường hợp tệp mà họ đang cố mở không tồn tại. Hãy để tôi nói lại rằng: Mọi lập trình viên khách hàng sử dụng API này nên biết rằng họ phải giải quyết trường hợp này. Và có một điều khó khăn: lập trình viên API có thể giúp họ biết họ nên giải quyết vấn đề đó thông qua nhận xét một mình hay họ thực sự có thể yêu cầu khách hàng giải quyết nó.
Trong C thành ngữ đi một cái gì đó như
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
trong đó fopen
chỉ ra thất bại bằng cách trả về 0 và C (một cách dại dột) cho phép bạn coi 0 là một boolean và ... Về cơ bản, bạn học thành ngữ này và bạn ổn. Nhưng điều gì sẽ xảy ra nếu bạn là một người mới và bạn không học thành ngữ này. Sau đó, tất nhiên, bạn bắt đầu với
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
và học một cách khó khăn.
Lưu ý rằng chúng ta chỉ nói về các ngôn ngữ được gõ mạnh ở đây: Có một ý tưởng rõ ràng về API là gì trong ngôn ngữ được gõ mạnh: Đó là một chức năng (phương thức) để bạn sử dụng với giao thức được xác định rõ ràng cho từng ngôn ngữ.
Giao thức được xác định rõ ràng thường được xác định bởi một chữ ký phương thức. Ở đây fopen yêu cầu bạn truyền cho nó một chuỗi (hoặc char * trong trường hợp C). Nếu bạn cung cấp cho nó một cái gì đó khác, bạn nhận được một lỗi thời gian biên dịch. Bạn đã không tuân theo giao thức - bạn không sử dụng API đúng cách.
Trong một số ngôn ngữ (tối nghĩa), kiểu trả về cũng là một phần của giao thức. Nếu bạn cố gắng gọi tương đương với fopen()
một số ngôn ngữ mà không gán nó cho một biến, bạn cũng sẽ gặp lỗi thời gian biên dịch (bạn chỉ có thể làm điều đó với các hàm void).
Điểm tôi đang cố gắng đưa ra là: Trong một ngôn ngữ được gõ tĩnh, lập trình viên API khuyến khích khách hàng sử dụng API đúng cách bằng cách ngăn không cho mã máy khách của họ biên dịch nếu nó có bất kỳ lỗi rõ ràng nào.
(Trong một ngôn ngữ được gõ động, như Ruby, bạn có thể chuyển bất kỳ thứ gì, nói nổi, như tên tệp - và nó sẽ biên dịch. Tại sao lại gây rắc rối cho người dùng với các ngoại lệ được kiểm tra nếu bạn thậm chí không kiểm soát các đối số phương thức. các đối số được thực hiện ở đây chỉ áp dụng cho các ngôn ngữ được nhập tĩnh.)
Vì vậy, những gì về ngoại lệ được kiểm tra?
Đây là một trong những API Java mà bạn có thể sử dụng để mở tệp.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
Thấy mà bắt không? Đây là chữ ký cho phương thức API đó:
public FileInputStream(String name)
throws FileNotFoundException
Lưu ý rằng đó FileNotFoundException
là một ngoại lệ được kiểm tra .
Lập trình viên API đang nói điều này với bạn: "Bạn có thể sử dụng hàm tạo này để tạo FileInputStream mới nhưng bạn
a) phải chuyển vào tên tệp dưới dạng Chuỗi
b) phải chấp nhận khả năng tệp có thể không được tìm thấy trong thời gian chạy "
Và đó là toàn bộ vấn đề mà tôi quan tâm.
Chìa khóa về cơ bản là những gì câu hỏi nêu là "Những thứ nằm ngoài tầm kiểm soát của lập trình viên". Suy nghĩ đầu tiên của tôi là anh ấy / cô ấy có nghĩa là những thứ nằm ngoài sự kiểm soát của các lập trình viên API . Nhưng trên thực tế, các trường hợp ngoại lệ được kiểm tra khi được sử dụng đúng cách thực sự phải dành cho những thứ nằm ngoài sự kiểm soát của cả lập trình viên và lập trình viên API. Tôi nghĩ rằng đây là chìa khóa để không lạm dụng các ngoại lệ được kiểm tra.
Tôi nghĩ rằng việc mở tệp minh họa điểm độc đáo. Lập trình viên API biết rằng bạn có thể cung cấp cho họ một tên tệp hóa ra không tồn tại vào thời điểm API được gọi và họ sẽ không thể trả lại cho bạn những gì bạn muốn, nhưng sẽ phải đưa ra một ngoại lệ. Họ cũng biết rằng điều này sẽ xảy ra khá thường xuyên và lập trình viên máy khách có thể mong đợi tên tệp là chính xác tại thời điểm họ viết cuộc gọi, nhưng nó có thể sai trong thời gian chạy vì những lý do ngoài tầm kiểm soát của họ.
Vì vậy, API làm cho nó rõ ràng: Sẽ có trường hợp tệp này không tồn tại tại thời điểm bạn gọi cho tôi và bạn đã giải quyết tốt hơn với nó.
Điều này sẽ rõ ràng hơn với một trường hợp phản tác dụng. Hãy tưởng tượng tôi đang viết API bảng. Tôi có mô hình bảng ở đâu đó với API bao gồm phương thức này:
public RowData getRowData(int row)
Bây giờ là một lập trình viên API, tôi biết sẽ có trường hợp một số khách hàng chuyển vào một giá trị âm cho hàng hoặc một giá trị hàng bên ngoài bảng. Vì vậy, tôi có thể bị cám dỗ ném một ngoại lệ được kiểm tra và buộc khách hàng phải đối phó với nó:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(Tất nhiên tôi sẽ không gọi nó là "Đã kiểm tra".)
Đây là sử dụng xấu của ngoại lệ được kiểm tra. Mã máy khách sẽ có đầy đủ các lệnh gọi để tìm nạp dữ liệu hàng, mỗi một trong số chúng sẽ phải sử dụng thử / bắt và để làm gì? Họ sẽ báo cáo cho người dùng rằng hàng sai đã được tìm kiếm? Có lẽ là không - bởi vì bất kể giao diện người dùng nào xung quanh chế độ xem bảng của tôi là gì, nó không nên cho phép người dùng vào trạng thái yêu cầu một hàng bất hợp pháp. Vì vậy, đó là một lỗi về phía lập trình viên máy khách.
Lập trình viên API vẫn có thể dự đoán rằng máy khách sẽ mã hóa các lỗi như vậy và sẽ xử lý nó với ngoại lệ thời gian chạy như IllegalArgumentException
.
Với một ngoại lệ được kiểm tra getRowData
, đây rõ ràng là một trường hợp sẽ dẫn đến lập trình viên lười biếng của Hejlsberg chỉ đơn giản là thêm các sản phẩm khai thác trống. Khi điều đó xảy ra, các giá trị hàng bất hợp pháp sẽ không rõ ràng ngay cả với người kiểm tra hoặc nhà phát triển máy khách gỡ lỗi, thay vào đó chúng sẽ dẫn đến các lỗi gõ cửa khó xác định chính xác nguồn. Tên lửa Arianne sẽ nổ tung sau khi phóng.
Được rồi, đây là vấn đề: Tôi đang nói rằng ngoại lệ được kiểm tra FileNotFoundException
không chỉ là một điều tốt mà còn là một công cụ thiết yếu trong hộp công cụ lập trình API để xác định API theo cách hữu ích nhất cho lập trình viên máy khách. Nhưng đó CheckedInvalidRowNumberException
là một bất tiện lớn, dẫn đến lập trình xấu và nên tránh. Nhưng làm thế nào để nói sự khác biệt.
Tôi đoán đó không phải là một khoa học chính xác và tôi đoán rằng đó là nền tảng và có lẽ biện minh cho một mức độ nhất định của Hejlsberg. Nhưng tôi không vui khi ném em bé ra ngoài bằng nước tắm ở đây, vì vậy hãy cho phép tôi trích ra một số quy tắc ở đây để phân biệt các trường hợp ngoại lệ được kiểm tra tốt và xấu:
Mất kiểm soát của khách hàng hoặc Đóng so với Mở:
Các ngoại lệ được kiểm tra chỉ nên được sử dụng khi trường hợp lỗi nằm ngoài tầm kiểm soát của cả API và lập trình viên máy khách. Điều này phải làm với cách mở hoặc đóng hệ thống. Trong một giao diện người dùng bị ràng buộc nơi lập trình viên máy khách có quyền kiểm soát, giả sử, trên tất cả các nút, lệnh bàn phím, v.v. có thêm và xóa các hàng khỏi chế độ xem bảng (một hệ thống đóng), đó là lỗi lập trình máy khách nếu nó cố gắng tìm nạp dữ liệu từ một hàng không tồn tại. Trong một hệ điều hành dựa trên tệp mà bất kỳ số lượng người dùng / ứng dụng nào cũng có thể thêm và xóa các tệp (một hệ thống mở), có thể hiểu rằng tệp mà khách hàng đang yêu cầu đã bị xóa mà họ không nên biết để xử lý .
Tính phổ biến:
Các trường hợp ngoại lệ được kiểm tra không nên được sử dụng trong lệnh gọi API được thực hiện thường xuyên bởi máy khách. Bởi thường thì tôi có nghĩa là từ rất nhiều nơi trong mã máy khách - không thường xuyên về thời gian. Vì vậy, một mã máy khách không có xu hướng cố gắng mở cùng một tệp, nhưng chế độ xem bảng của tôi xuất hiện ở RowData
mọi nơi từ các phương thức khác nhau. Đặc biệt, tôi sẽ viết rất nhiều mã như
if (model.getRowData().getCell(0).isEmpty())
và sẽ rất đau đớn khi phải thử / bắt mỗi lần.
Thông báo cho người dùng:
Các ngoại lệ được kiểm tra nên được sử dụng trong trường hợp bạn có thể tưởng tượng một thông báo lỗi hữu ích được trình bày cho người dùng cuối. Đây là "và bạn sẽ làm gì khi nó xảy ra?" câu hỏi tôi nêu ở trên. Nó cũng liên quan đến mục 1. Vì bạn có thể dự đoán rằng một cái gì đó bên ngoài hệ thống API khách của bạn có thể khiến tệp không ở đó, nên bạn có thể nói với người dùng về nó một cách hợp lý:
"Error: could not find the file 'goodluckfindingthisfile'"
Vì số hàng bất hợp pháp của bạn là do lỗi nội bộ và không phải do lỗi của người dùng, nên thực sự không có thông tin hữu ích nào bạn có thể cung cấp cho họ. Nếu ứng dụng của bạn không để ngoại lệ thời gian chạy rơi vào bảng điều khiển, có thể cuối cùng nó sẽ cung cấp cho họ một số thông báo xấu như:
"Internal error occured: IllegalArgumentException in ...."
Nói tóm lại, nếu bạn không nghĩ rằng lập trình viên khách hàng của bạn có thể giải thích ngoại lệ của bạn theo cách giúp người dùng, thì có lẽ bạn không nên sử dụng một ngoại lệ được kiểm tra.
Vì vậy, đó là những quy tắc của tôi. Một chút gì đó bị chiếm đoạt, và chắc chắn sẽ có ngoại lệ (xin vui lòng giúp tôi tinh chỉnh chúng nếu bạn muốn). Nhưng đối số chính của tôi là có những trường hợp như FileNotFoundException
ngoại lệ được kiểm tra là một phần quan trọng và hữu ích của hợp đồng API như các loại tham số. Vì vậy, chúng ta không nên phân phối với nó chỉ vì nó được sử dụng sai.
Xin lỗi, không có nghĩa là làm cho điều này quá dài và gượng gạo. Hãy để tôi kết thúc với hai gợi ý:
Trả lời: Lập trình viên API: sử dụng ngoại lệ được kiểm tra một cách tiết kiệm để duy trì tính hữu dụng của chúng. Khi nghi ngờ sử dụng một ngoại lệ không được kiểm tra.
B: Lập trình viên khách hàng: có thói quen tạo một ngoại lệ được bao bọc (google nó) ngay từ đầu trong quá trình phát triển của bạn. JDK 1.4 và sau đó cung cấp một hàm tạo RuntimeException
cho việc này, nhưng bạn cũng có thể dễ dàng tạo riêng cho mình. Đây là hàm tạo:
public RuntimeException(Throwable cause)
Sau đó, hãy tập thói quen bất cứ khi nào bạn phải xử lý một ngoại lệ được kiểm tra và bạn cảm thấy lười biếng (hoặc bạn nghĩ rằng lập trình viên API đã quá nhiệt tình khi sử dụng ngoại lệ được kiểm tra ở nơi đầu tiên), đừng nuốt ngoại lệ, hãy bọc nó và suy nghĩ lại nó.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Đặt cái này vào một trong các mẫu mã nhỏ của IDE của bạn và sử dụng nó khi bạn cảm thấy lười biếng. Bằng cách này nếu bạn thực sự cần xử lý ngoại lệ đã kiểm tra, bạn sẽ buộc phải quay lại và xử lý sau khi gặp sự cố khi chạy. Bởi vì, hãy tin tôi (và Anders Hejlsberg), bạn sẽ không bao giờ quay lại TODO đó trong bạn
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}