Các loại ngoại lệ được sử dụng lại nên được ưa chuộng hơn các loại sử dụng một lần?


8

Hãy nói rằng tôi có Doors được quản lý bởi a DoorService. Các DoorServicechịu trách nhiệm về việc mở, đóng cửa và khóa cửa mà được lưu trữ trên cơ sở dữ liệu.

public interface DoorService
{
    void open(Door door) throws DoorLockedException, DoorAlreadyOpenedException;

    void close(Door door) throws DoorAlreadyClosedException;

    /**
     * Closes the door if open
     */
    void lock(Door door) throws DoorAlreadyLockedException;
}

Đối với phương pháp khóa, có một DoorAlreadyLockedExceptionvà cho phương pháp mở có một DoorLockedException. Đây là một tùy chọn nhưng có các tùy chọn khác có thể:

1) Sử dụng DoorLockedExceptioncho mọi thứ, hơi khó xử khi bắt ngoại lệ trong lock()cuộc gọi

try
{
    doorService.lock(myDoor);
}
catch(DoorLockedException ex) // door ALREADY locked
{
    //error handling...
}

2) Có 2 loại ngoại lệ DoorLockedExceptionDoorAlreadyLockedException

3) Có 2 loại ngoại lệ nhưng hãy để DoorAlreadyLockedException extends DoorLockedException

Đó là giải pháp tốt nhất và tại sao?


1
Làm thế nào để bạn xác định "tốt nhất?" Trong C #, chúng tôi thậm chí không có ngoại lệ được kiểm tra. Chúng tôi tin rằng điều này làm cho sự phát triển của chúng tôi đơn giản và đáng tin cậy hơn.
Robert Harvey

2
Độ phân giải của Khóa bị khóa và Khóa khác nhau như thế nào?
whatsisname

@RobertHarvey Tôi coi ngoại lệ là một phần của logic của phương thức. Giải pháp tốt nhất là giải pháp rõ ràng và hợp lý nhất. Không giống như trong C #, chúng tôi đang thúc đẩy sự khám phá về sự thuận tiện bằng cách buộc bắt trường hợp đặc biệt và đảm bảo không ai gọi một phương thức mà không có sự hiểu biết đầy đủ về những gì đang diễn ra.
Mùa đông

1
Các phương thức này chỉ đơn giản là trả về một mã trạng thái. Bạn có thể thông báo cho người dùng của mình dựa trên mã bạn nhận được và được tăng hiệu suất miễn phí (ngoại lệ đắt tiền, đắt hơn khoảng 1000 đến 10000 lần so với trả lại mã trạng thái).
Robert Harvey

1
@RobertHarvey "ngoại lệ đắt tiền, đắt hơn khoảng 1000 đến 10000 lần so với trả lại mã trạng thái" Tôi đã thực hiện một số phân tích về điều này trong Java và chi phí ngoại lệ thường được nêu quá mức. Chúng 'có thể đắt tiền nếu bạn có ngăn xếp rất sâu. IIRC, tôi đã phải tăng kích thước ngăn xếp tối đa đáng kể để có thể tạo các ngăn xếp đủ lớn để làm cho các ngoại lệ đủ chậm để lo lắng. Ngay cả những con lợn xếp như mùa xuân cũng không làm điều đó. Miễn là bạn không ném và bắt trong các vòng lặp chặt chẽ, đó là tối ưu hóa sớm để tránh ngoại lệ.
JimmyJames

Câu trả lời:


21

Tôi biết mọi người kiếm được nhiều tiền từ việc sử dụng ngoại lệ để kiểm soát dòng chảy nhưng đó không phải là vấn đề lớn nhất ở đây. Tôi thấy một vi phạm lệnh truy vấn tách lớn . Nhưng ngay cả đó không phải là tồi tệ nhất.

Không, điều tồi tệ nhất ở ngay đây:

/**
 * Closes the door if open
 */

Tại sao mọi thứ khác nổ tung nếu giả định của bạn về trạng thái cửa là sai nhưng lock()chỉ sửa nó cho bạn? Quên rằng điều này làm cho không thể khóa cửa mở, điều này là hoàn toàn có thể và đôi khi hữu ích. Không có vấn đề ở đây là bạn đã trộn lẫn hai triết lý khác nhau để xử lý các giả định không chính xác. Điều đó thật khó hiểu. Đừng làm vậy. Không cùng mức độ trừu tượng với cùng một kiểu đặt tên. Ow! Lấy một trong những ý tưởng bên ngoài. Các phương pháp dịch vụ cửa nên hoạt động theo cùng một cách.

Đối với vi phạm Phân tách truy vấn lệnh, tôi không cần phải cố gắng đóng cửa để tìm hiểu xem nó đóng hay mở. Tôi có thể chỉ cần hỏi. Dịch vụ cửa không cung cấp cách để làm điều đó mà không thể thay đổi trạng thái của cửa. Điều đó làm cho điều này trở nên tồi tệ hơn nhiều sau đó các lệnh cũng xảy ra để trả về các giá trị (một sự hiểu lầm phổ biến về những gì CQS nói về). Ở đây, các lệnh thay đổi trạng thái là cách duy nhất để thực hiện các truy vấn! Ow!

Đối với các trường hợp ngoại lệ đắt hơn mã trạng thái, đó là cuộc nói chuyện tối ưu hóa. Đủ nhanh là đủ nhanh. Không có vấn đề thực sự là con người không mong đợi ngoại lệ cho các trường hợp điển hình. Bạn có thể tranh luận về những gì điển hình tất cả những gì bạn thích. Đối với tôi câu hỏi lớn là làm thế nào bạn có thể đọc được mã bằng cách đọc.

ensureClosed(DoorService service, Door door){
  // Need door closed and unlocked. No idea of its state. What to do?
  try {
    service.open(door)
    service.close(door)
  } 
  catch( DoorLockedException e ){
    //Have no way to unlock the door so give up and die
    log(e);
    throw new NoOneGaveMeAKeyException(e);      
  }
  catch( DoorAlreadyOpenedException e ){
    try { 
      service.close(door);
    }
    catch( DoorAlreadyClosedException e ){
      //Some multithreaded goof has been messing with our door.
      //Oh well, this is what we wanted anyway.
      //Hope they didn't lock it.
    }
  }
}

Xin đừng bắt tôi viết mã như thế này. Xin vui lòng cho chúng tôi isLocked()isClosed()phương pháp. Với những cái đó tôi có thể tự viết ensureClosed()ensureUnlocked()phương pháp dễ đọc. Những người chỉ ném nếu điều kiện bài của họ bị vi phạm. Tôi chỉ muốn tìm thấy bạn đã viết và kiểm tra chúng tất nhiên. Chỉ cần không trộn chúng lại với những thứ ném khi chúng không thể thay đổi trạng thái. Ít nhất là cho họ phân biệt tên.

Dù bạn làm gì, xin đừng gọi bất cứ điều gì tryClose(). Đó là một cái tên khủng khiếp.

Đối với DoorLockedExceptionmột mình và cũng có DoorAlreadyLockedExceptionbệnh nói điều này: tất cả là về việc sử dụng mã. Vui lòng không thiết kế các dịch vụ như thế này mà không viết mã bằng cách sử dụng và nhìn vào mớ hỗn độn bạn đang tạo. Tái cấu trúc và thiết kế lại cho đến khi sử dụng mã ít nhất có thể đọc được. Trong thực tế, xem xét việc viết mã bằng cách sử dụng đầu tiên.

ensureClosed(DoorService service, Door door){
  if( !service.isClosed(door) ){
    try{
      service.close(door);
    }
    catch( DoorAlreadyClosedException e ){
      //Some multithreaded goof has been messing with our door.
      //Oh well, this is what we wanted anyway.
      //Hope they didn't lock it.
    }
  } else {
    //This is what you wanted, so quietly do nothing. 
    //Why are you even here? Who bothers to write empty else conditions?
  }
}

ensureUnlocked(DoorService service, Door door){
  if( service.islocked(door) ){
    throw new NoOneGaveMeAKeyException(); 
  }
}

Các lock()phương pháp được gọi close()là chỉ có tôi là lười biếng khi viết câu hỏi, nhưng nhờ làm cho tôi nhận thức được rằng! Tôi sẽ không rơi vào nó khi viết mã thực sự. Trong trường hợp của tôi, 3 phương pháp không bao giờ thực sự được gọi là liên tiếp. Đó chỉ là: gọi một cái, nếu không có ngoại lệ nào tốt, nếu không thì hiển thị hộp thoại đã dịch và có thể đăng xuất (nếu UnlimitedTokenException). Tôi có isLocked()isOpen()các phương thức và chúng được sử dụng trong luồng điều khiển của máy chủ nhưng chúng không nên được sử dụng như một cách để kiểm tra xem hộp thoại lỗi có được hiển thị hay không, đó là một công việc ngoại lệ.
Mùa đông

Đối với điểm cuối cùng của bạn, vâng, việc sử dụng mã có vẻ tốt hơn một chút DoorAlreadyLockedExceptionnhưng nó tạo ra các lớp sinh đôi.
Mùa đông

2
Nếu theo lớp sinh đôi, bạn có nghĩa là ngoại lệ, hãy để tôi chỉ cho bạn cách sử dụng kế thừa yêu thích của tôi : public class DoorAlreadyLockedException inherits DoorLockedException {}. Xác định một lớp trong một dòng chỉ vì mục đích cho nó một cái tên hay. Chọn những gì bạn thừa hưởng từ một cách khôn ngoan. Tôi rất thích thừa kế từ càng cao càng tốt, trong khi không bị thừa một cách vô nghĩa, để tránh một chuỗi thừa kế dài.
candied_orange 17/07/18

Tôi cảm thấy như đổi tên NoOneGaveMeAKeythành NoOneGaveMeAKeyExceptionchỉ để trở thành mặt dây chuyền. Mặt khác, tôi đồng ý với điểm chính là trước khi xử lý, chúng ta cần có khả năng biết trạng thái của đối tượng.
Walfrat

2
Người ủng hộ của quỷ - chỉ cần có isOpen/ isClosedkhông đủ tốt. Hãy suy nghĩ về hệ thống tập tin. Bạn có thể kiểm tra tập tin không / không tồn tại, nhưng điều đó vẫn có thể thay đổi theo thời gian bạn cố gắng đọc hoặc viết nó, và IOExceptionkết quả sẽ. Có thể trong bối cảnh này thực sự có khả năng nhà nước không hợp lệ khi bạn mở hoặc đóng cửa.
Tom G

8

Phân biệt giữa DoorLockedExceptionDoorAlreadyLockedExceptiongợi ý rằng các loại ngoại lệ khác nhau đang được sử dụng để kiểm soát dòng chảy, một thực tế đã được coi là một phản mẫu.

Có nhiều loại ngoại lệ cho một cái gì đó lành tính này là vô cớ. Sự phức tạp bổ sung không vượt trội so với các lợi ích bổ sung.

Chỉ cần sử dụng DoorLockedExceptioncho tất cả mọi thứ.

Tốt hơn nữa, đơn giản là không làm gì cả. Nếu cửa đã bị khóa, thì nó vẫn sẽ ở trạng thái chính xác khi lock()phương thức hoàn thành. Đừng ném ngoại lệ cho các điều kiện không đáng kể.

Nếu bạn vẫn không thoải mái với điều đó, thì hãy đổi tên phương thức của bạn ensureDoorLocked().


4
Xin đừng lạm dụng meme "ngoại lệ cho kiểm soát dòng chảy", nếu hai tình huống, vì bất kỳ lý do gì, đảm bảo các khóa hành động khác nhau đáng kể để giải quyết, sau đó có các ngoại lệ khác nhau có ý nghĩa. Không rõ ràng từ ví dụ nếu đó là trường hợp mặc dù.
whatsisname

1
@whatsisname: Nếu đó là một meme không có cơ sở trên thực tế, thì hãy đăng câu trả lời của riêng bạn cho hiệu ứng đó.
Robert Harvey

1
@RobertHarvey Ngoại lệ được sử dụng cho "luồng điều khiển" của hộp thoại lỗi và ghi nhật ký lỗi. Ứng dụng logic chính rõ ràng là không chạy mặc dù các mệnh đề bắt. Tôi không nghĩ việc phân biệt giữa DoorLockedExceptionDoorAlreadyLockedExceptiongợi ý rằng, đó chỉ là việc có một trình xử lý lỗi có ý nghĩa hơn nhiều cho lock()phương thức với chi phí có một lớp gần như trùng lặp.
Mùa đông

1
Nếu bạn gọi khóa () và cửa bị khóa, bạn đã biết rằng cửa đã bị khóa. Bạn không cần phải nói những gì bạn đã biết.
Robert Harvey

2
@ewan: Ồ, ffs. Gọi nó là bất cứ điều gì bạn muốn; một liệt kê, một kết quả, bất cứ điều gì. Đó là cách tiếp cận đúng đắn.
Robert Harvey

3

Chủ đề của câu hỏi này "Các loại ngoại lệ tái chế có nên được ưa chuộng hơn các loại sử dụng một lần không?" dường như đã bị bỏ qua, không chỉ trong các câu trả lời, mà còn trong các chi tiết cho các câu hỏi (các câu trả lời là chi tiết của câu hỏi).

Tôi đồng ý với hầu hết những lời chỉ trích về mã mà người trả lời đã đưa ra.

Nhưng như một câu trả lời cho câu hỏi tiêu đề, tôi sẽ nói rằng bạn nên ưu tiên sử dụng lại 'ngoại lệ tái chế' hơn 'sử dụng một lần'.

Trong việc sử dụng xử lý ngoại lệ điển hình hơn, bạn có một DISTANCE tuyệt vời trong mã giữa nơi phát hiện và ném ngoại lệ, và nơi bắt ngoại lệ và xử lý ngoại lệ. Điều đó có nghĩa là sử dụng các loại riêng (mô-đun) trong xử lý ngoại lệ thường không hoạt động tốt. Một chương trình điển hình có xử lý ngoại lệ - sẽ có một số vị trí cấp cao nhất trong mã nơi xử lý ngoại lệ. Và vì đây là những vị trí cấp cao nhất, thật khó để họ có kiến ​​thức chi tiết về các khía cạnh hẹp của các mô-đun cụ thể.

Thay vào đó, họ có thể sẽ có một trình xử lý cấp cao cho một vài vấn đề cấp cao.

Lý do duy nhất sử dụng các lớp ngoại lệ tùy chỉnh chi tiết dường như có ý nghĩa trong mẫu của bạn, là bởi vì (và ở đây tôi đang nói xấu những người trả lời khác) - bạn không nên sử dụng xử lý ngoại lệ cho luồng điều khiển thông thường.


1
"Trong việc sử dụng xử lý ngoại lệ điển hình hơn, bạn có một KHOẢNG CÁCH tuyệt vời" Chúng tôi không phân tích những gì điển hình. Chúng tôi đang thiết kế một cách để nó hoạt động. Nhiều thiết kế tốt bắt được ngoại lệ của chúng ở cấp độ tiếp theo trong ngăn xếp cuộc gọi và trong phạm vi cực kỳ hạn chế. Điều đó không nên được coi là kiểm soát dòng chảy khi tất cả những gì họ làm là phục hồi từ sự cố. Các thiết kế ngoại lệ tốt nhất bắt đầu bằng cách lập kế hoạch làm thế nào để phục hồi trước khi xác định các ngoại lệ. Sau đó, các tên ngoại lệ không phải là về báo cáo vấn đề. Họ đang tìm giải pháp. Bạn có thể sử dụng chuỗi tin nhắn để báo cáo vấn đề.
candied_orange 17/07/18

"Chúng tôi không phân tích những gì điển hình. Chúng tôi đang thiết kế một cách để nó hoạt động.", Đồng ý, đó là những gì bạn đang làm. Đó không phải là những gì tôi đã làm. Nếu bạn nhìn vào tiêu đề, nó hỏi một câu hỏi. Nếu bạn nhìn vào phần thân của tin nhắn, nó sẽ hỏi ba câu hỏi (liên quan) khác nhau. Bạn đã trả lời các câu hỏi cơ thể (ở một mức độ - thực sự- nhiều hơn nữa bạn đã chỉ trích mã mẫu). Tôi trả lời câu hỏi tiêu đề.
Lewis Pringle

BTW, bạn tuyên bố "Nhiều thiết kế tốt bắt được ngoại lệ của chúng ở cấp độ tiếp theo trong ngăn xếp cuộc gọi và trong phạm vi cực kỳ hạn chế". Tôi không chắc chắn tôi sẽ hoàn toàn không đồng ý với điều đó. Nhưng tôi phải nói rằng, sau 40 năm lập trình, tôi gặp khó khăn khi đưa ra một ví dụ để hỗ trợ cho tuyên bố đó. Tôi nhận ra một chút của một spin-off (và tôi có thể không xử lý việc này một cách chính xác - mới đối với stackoverflow) - nhưng bạn có phiền khi đưa ra một ví dụ hoặc hai trong đó xử lý thử / bắt cục bộ như vậy là giải pháp tốt nhất? Nó chắc chắn không phải là một mô hình điển hình / phổ biến.
Lewis Pringle

Một ví dụ hả? Hãy để tôi giới thiệu bạn với Jon Skeet
candied_orange 17/07/18

OK, đó hoàn toàn không phải là một ví dụ về những gì bạn tuyên bố. Bạn nói "Nhiều thiết kế tốt bắt được ngoại lệ của chúng ở cấp độ tiếp theo trong ngăn xếp cuộc gọi và trong phạm vi cực kỳ hạn chế". Ví dụ của bạn chỉ là dịch một API chỉ tạo ra các ngoại lệ do lỗi thành một API tạo ra một số giá trị sentinel thay thế. Có lẽ đây chỉ là một sự khác biệt về từ ngữ giữa hai chúng tôi. Tôi sẽ không mô tả rằng đó là một "thiết kế tốt" nhưng là một "hack cục bộ để có được sự lựa chọn thiết kế bất tiện trong một công cụ bạn đang sử dụng".
Lewis Pringle

0

Một thay thế chưa được khám phá. Bạn có thể mô hình hóa điều sai với các lớp học của bạn, nếu bạn có những thứ này thay thế thì sao?

public interface OpenDoor
{
    public ClosedDoor close();
    public LockedDoor lock();
}

public interface ClosedDoor
{
    public OpenDoor open();
    public LockedDoor lock();
}

public interface LockedDoor
{
    // ? unlock? open?
}

Bây giờ không thể thử bất cứ điều gì là một lỗi. Bạn không cần bất kỳ ngoại lệ.


Nhưng trong hầu hết các ứng dụng, bạn không thể giữ tham chiếu đến các đối tượng đó, vì cơ sở dữ liệu có thể được thay đổi bất cứ lúc nào. Mặc dù vậy, nỗ lực sáng tạo tốt
Mùa đông

0

Tôi sẽ trả lời câu hỏi ban đầu, thay vì chỉ trích ví dụ.

Nói một cách đơn giản, nếu bạn mong đợi rằng khách hàng có thể cần phải phản ứng khác nhau cho từng trường hợp, thì nó đảm bảo một loại ngoại lệ mới.

Nếu bạn quyết định sử dụng nhiều loại ngoại lệ và tuy nhiên bạn hy vọng rằng máy khách có thể trong một số trường hợp muốn tạo một mệnh đề bắt chung cho chúng, có lẽ bạn nên tạo một siêu lớp trừu tượng mà cả hai đều mở rộng.


0

Đây là mã giả nhưng tôi nghĩ bạn sẽ hiểu ý tôi là:

public interface DoorService
{
    // ensures the door is open, uses key if locked, returns false if lock without a key or key does not fit
    bool Open(Door door, optional Key key) throws DoorStuckException, HouseOnFireException;

    // closes the door if open. already closed doors stay closed. Locked status does not change.
    void Close(Door door) throws throws DoorStuckException, HouseOnFireException;

    // closes the door if open and locks it
    void CloseAndLock(Door door, Key key) throws DoorStuckException, HouseOnFireException;
}

Có, tái sử dụng loại ngoại lệ có ý nghĩa, nếu bạn sử dụng ngoại lệ cho những điều thực sự đặc biệt . Bởi vì những điều thực sự đặc biệt chủ yếu là mối quan tâm xuyên suốt. Về cơ bản, bạn đang sử dụng Ngoại lệ cho các luồng điều khiển và điều đó dẫn đến những vấn đề đó.


Nhưng những loại ngoại lệ đó thực sự là không mô tả. Điều đó có nghĩa gì khi cửa bị "kẹt" khi bạn chỉ có thể mở, đóng hoặc khóa cửa?
Mùa đông

Đây chỉ là nuốt những ngoại lệ và không làm gì với chúng. Điều này không phù hợp trong bối cảnh bạn không muốn biết chuyện gì đang xảy ra. Cảm giác này giống như cách C # để làm ví dụ Java của tôi. Có một lý do tại sao tôi làm việc với Java.
Mùa đông

Chà, nó bị kẹt rồi. Nó không thể di chuyển. Vì vậy, bạn không thể mở nó và bạn không thể đóng nó. Đây không phải là trạng thái logic mà bạn cần khắc phục bằng cách gọi một phương thức khác, đó là trạng thái đặc biệt , mà bạn không thể giải quyết thông qua dịch vụ cửa.
nvoigt

Và nó không nuốt bất cứ thứ gì , nó không tạo ra ngoại lệ cho trạng thái chương trình không đặc biệt. Nếu vợ tôi yêu cầu tôi đóng cửa ban công và tôi đứng dậy và phát hiện ra nó đã đóng, tôi không chạy xung quanh và la hét và hoảng loạn. Tôi chỉ quay lại và nói "nó đã đóng cửa rồi".
nvoigt

Chính xác, với các ngoại lệ được kiểm tra, ngoại lệ không thể quay lại từ đầu chương trình phải được người gọi bắt. Trong cuộc gọi, bạn chỉ cần bỏ qua ngoại lệ hoặc đăng nhập một cảnh báo và bạn là tốt. Ngoài ra, cái mà bạn gọi là "ngoại lệ đặc biệt" chỉ là RuntimeExceptions. DoorStuckExceptionHouseOnFireExceptionkhông nên kiểm tra, chúng không có nghĩa là bị bắt trong tất cả các cuộc gọi.
Mùa đông
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.