Cách thiết kế ngoại lệ


11

Tôi đang vật lộn với một câu hỏi rất đơn giản:

Tôi hiện đang làm việc trên một ứng dụng máy chủ và tôi cần phát minh ra một hệ thống phân cấp cho các ngoại lệ (một số trường hợp ngoại lệ đã tồn tại, nhưng cần có khung chung). Làm thế nào để tôi thậm chí bắt đầu làm điều này?

Tôi đang nghĩ đến việc làm theo chiến lược này:

1) Điều gì đang xảy ra?

  • Một cái gì đó được hỏi, mà không được phép.
  • Một cái gì đó được hỏi, nó được cho phép, nhưng nó không hoạt động, do tham số sai.
  • Một cái gì đó được yêu cầu, nó được cho phép, nhưng nó không hoạt động, vì lỗi nội bộ.

2) Ai là người đưa ra yêu cầu?

  • Ứng dụng khách
  • Một ứng dụng máy chủ khác

3) Xử lý tin nhắn: như chúng ta đang xử lý một ứng dụng máy chủ, tất cả chỉ là về việc nhận và gửi tin nhắn. Vậy điều gì sẽ xảy ra nếu việc gửi tin nhắn bị trục trặc?

Như vậy, chúng ta có thể nhận được các loại ngoại lệ sau:

  • ServerNot ALLowedException
  • ClientNot ALLowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (trong trường hợp máy chủ không biết yêu cầu đến từ đâu)
    • ServerI InternalalException
    • ClientI InternalalException
  • MessageHandlingException

Đây là một cách tiếp cận rất chung để xác định thứ bậc ngoại lệ, nhưng tôi sợ rằng tôi có thể thiếu một số trường hợp rõ ràng. Bạn có ý tưởng về lĩnh vực nào tôi không đề cập đến, bạn có biết về bất kỳ nhược điểm nào của phương pháp này không hoặc có cách tiếp cận tổng quát hơn cho loại câu hỏi này (trong trường hợp sau, tôi có thể tìm thấy nó ở đâu)?

Cảm ơn trước


5
Bạn đã không đề cập đến những gì bạn muốn đạt được với hệ thống phân cấp lớp ngoại lệ của bạn (và điều đó hoàn toàn không rõ ràng). Ghi nhật ký có ý nghĩa? Cho phép khách hàng phản ứng hợp lý với các ngoại lệ khác nhau? Hay cái gì?
Ralf Kleberhoff

2
Nó có thể hữu ích để làm việc thông qua một số câu chuyện và sử dụng các trường hợp đầu tiên, và xem những gì xuất hiện. Ví dụ: khách hàng yêu cầu X , không được phép. Khách hàng yêu cầu X , nhưng yêu cầu không hợp lệ. Làm việc thông qua họ suy nghĩ về việc ai nên xử lý ngoại lệ, họ có thể làm gì với nó (nhắc nhở, thử lại, bất cứ điều gì) và thông tin nào họ cần để làm tốt điều đó. Sau đó , khi bạn biết ngoại lệ cụ thể của mình là gì và thông tin nào mà người xử lý cần để xử lý chúng, bạn có thể tạo chúng thành một hệ thống phân cấp đẹp.
Vô dụng

1
Về mức độ liên quan, tôi đoán: softwareengineering.stackexchange.com/questions/278949/NH
Martin Ba

1
Tôi chưa bao giờ thực sự hiểu được mong muốn muốn sử dụng nhiều loại ngoại lệ khác nhau. Thông thường đối với hầu hết catchcác khối tôi sử dụng, tôi không sử dụng ngoại lệ nhiều hơn so với thông báo lỗi mà nó chứa. Tôi thực sự không có gì khác tôi có thể làm cho một ngoại lệ liên quan đến việc không đọc tệp vì một người không cấp phát bộ nhớ trong quá trình đọc, vì vậy tôi có xu hướng chỉ bắt std::exceptionvà báo cáo thông báo lỗi có trong đó, có thể là trang trí nó với "Failed to open file: %s", ex.what()một bộ đệm ngăn xếp trước khi in nó.

3
Ngoài ra, tôi không thể lường trước tất cả các loại ngoại lệ sẽ được ném ở vị trí đầu tiên. Tôi có thể dự đoán được chúng ngay bây giờ, nhưng các đồng nghiệp có thể giới thiệu những cái mới trong tương lai, ví dụ như vậy, thật vô vọng đối với tôi để biết, cho đến bây giờ và mãi mãi, tất cả các loại ngoại lệ khác nhau có thể được đưa ra trong quá trình hoạt động. Vì vậy, tôi chỉ bắt siêu chung với một khối bắt duy nhất. Tôi đã thấy các ví dụ về những người sử dụng nhiều catchkhối khác nhau trong một trang khôi phục, nhưng thường thì chỉ cần bỏ qua thông báo bên trong ngoại lệ và in một tin nhắn được bản địa hóa hơn ...

Câu trả lời:


5

Nhận xét chung

(một chút thiên kiến)

Tôi thường không đi theo một hệ thống phân cấp ngoại lệ chi tiết.

Điều quan trọng nhất: một ngoại lệ cho người gọi của bạn biết rằng phương thức của bạn không hoàn thành công việc. Và người gọi của bạn phải nhận được thông báo về điều đó, vì vậy anh ta không chỉ tiếp tục. Điều đó làm việc với bất kỳ ngoại lệ, bất kể bạn chọn lớp ngoại lệ nào.

Khía cạnh thứ hai là đăng nhập. Bạn muốn tìm các mục nhật ký có ý nghĩa bất cứ khi nào có vấn đề. Điều đó cũng không cần các lớp ngoại lệ khác nhau, chỉ có các tin nhắn văn bản được thiết kế tốt (tôi cho rằng bạn không cần một máy tự động để đọc nhật ký lỗi của bạn ...).

Khía cạnh thứ ba là phản ứng của người gọi của bạn. Người gọi của bạn có thể làm gì khi nhận được một ngoại lệ? Ở đây có nghĩa là có các lớp ngoại lệ khác nhau, vì vậy người gọi có thể quyết định có thử lại cùng một cuộc gọi hay không, sử dụng một giải pháp khác (ví dụ sử dụng nguồn dự phòng thay thế) hoặc từ bỏ.

Và có thể bạn muốn sử dụng ngoại lệ của mình làm cơ sở để thông báo cho người dùng cuối về vấn đề. Điều đó có nghĩa là tạo một thông báo thân thiện với người dùng bên cạnh văn bản quản trị viên cho tệp nhật ký, nhưng không cần các lớp ngoại lệ khác nhau (mặc dù có thể điều đó có thể giúp việc tạo văn bản dễ dàng hơn ...).

Một khía cạnh quan trọng để ghi nhật ký (và cho các thông báo lỗi người dùng) là khả năng sửa đổi ngoại lệ với thông tin ngữ cảnh bằng cách bắt nó ở một lớp nào đó, thêm một số thông tin ngữ cảnh, ví dụ như tham số phương thức và ném lại.

Hệ thống phân cấp của bạn

Ai đang đưa ra yêu cầu? Tôi không nghĩ bạn sẽ cần thông tin người đưa ra yêu cầu. Tôi thậm chí không thể tưởng tượng làm thế nào bạn biết rằng sâu bên trong một số ngăn xếp cuộc gọi.

Xử lý tin nhắn : đó không phải là một khía cạnh khác, mà chỉ là các trường hợp bổ sung cho "Điều gì đang xảy ra?".

Trong một bình luận, bạn nói về một lá cờ " không đăng nhập " khi tạo một ngoại lệ. Tôi không nghĩ rằng tại nơi bạn tạo và ném ngoại lệ, bạn có thể đưa ra quyết định đáng tin cậy cho dù có đăng nhập ngoại lệ đó hay không.

Tình huống duy nhất tôi có thể tưởng tượng là một số lớp cao hơn sử dụng API của bạn theo cách đôi khi sẽ tạo ra ngoại lệ và lớp này sau đó biết rằng nó không cần phải làm phiền bất kỳ quản trị viên nào ngoại lệ, vì vậy nó âm thầm nuốt ngoại lệ. Nhưng đó là một mùi mã: một ngoại lệ dự kiến ​​là một mâu thuẫn trong chính nó, một gợi ý để thay đổi API. Và đó là lớp cao hơn sẽ quyết định, không phải là mã tạo ngoại lệ.


Theo kinh nghiệm của tôi, một mã lỗi kết hợp với văn bản thân thiện với người dùng hoạt động rất tốt. Quản trị viên có thể sử dụng mã lỗi để tìm thêm thông tin.
Sjoerd

1
Tôi thích ý tưởng đằng sau câu trả lời này nói chung. Từ kinh nghiệm của tôi Ngoại lệ thực sự không bao giờ nên biến thành một con quái vật quá phức tạp. Mục đích chính của một ngoại lệ là cho phép mã cuộc gọi giải quyết một vấn đề cụ thể và nhận thông tin gỡ lỗi / thử lại có liên quan mà không làm rối loạn phản ứng của chức năng.
greggle138

2

Điều chính cần lưu ý khi thiết kế mẫu phản hồi lỗi là đảm bảo nó hữu ích cho người gọi. Điều này áp dụng cho dù bạn đang sử dụng ngoại lệ hoặc mã lỗi được xác định, nhưng chúng tôi sẽ hạn chế việc minh họa bằng các ngoại lệ.

  • Nếu ngôn ngữ hoặc khung của bạn đã cung cấp các lớp ngoại lệ chung, hãy sử dụng chúng ở nơi chúng phù hợp và nơi chúng được mong đợi một cách hợp lý . Đừng định nghĩa các lớp của riêng bạn ArgumentNullExceptionhoặc ArgumentOutOfRangengoại lệ. Người gọi sẽ không mong đợi để bắt những người đó.

  • Xác định một MyClientServerAppExceptionlớp cơ sở để bao gồm các lỗi duy nhất trong ngữ cảnh của ứng dụng của bạn. Không bao giờ ném một thể hiện của lớp cơ sở. Một phản hồi lỗi mơ hồ là CÔNG VIỆC TUYỆT VỜI . Nếu có "lỗi nội bộ", thì hãy giải thích lỗi đó là gì.

  • Đối với hầu hết các phần, hệ thống phân cấp bên dưới lớp cơ sở nên rộng, không sâu. Bạn chỉ cần đào sâu nó trong các tình huống có ích cho người gọi. Ví dụ: nếu có 5 lý do một thông báo có thể thất bại từ máy khách đến máy chủ, bạn có thể xác định một ServerMessageFaultngoại lệ, sau đó xác định một lớp ngoại lệ cho mỗi 5 lỗi đó bên dưới. Bằng cách đó, người gọi chỉ có thể bắt được siêu lớp nếu cần hoặc muốn. Cố gắng hạn chế điều này cho các trường hợp cụ thể, hợp lý.

  • Đừng cố gắng xác định tất cả các lớp ngoại lệ của bạn trước khi chúng thực sự được sử dụng. Bạn sẽ kết thúc việc làm lại hầu hết. Khi bạn gặp trường hợp lỗi trong khi viết mã, sau đó quyết định cách tốt nhất để mô tả lỗi đó. Lý tưởng nhất, nó nên được thể hiện trong bối cảnh của những gì người gọi đang cố gắng làm.

  • Liên quan đến điểm trước đó, hãy nhớ rằng chỉ vì bạn sử dụng ngoại lệ để phản hồi lỗi, điều đó không có nghĩa là bạn phải chỉ sử dụng ngoại lệ cho các trạng thái lỗi. Hãy nhớ rằng việc ném ngoại lệ rất tốn kém và chi phí thực hiện có thể thay đổi từ ngôn ngữ này sang ngôn ngữ khác. Với một số ngôn ngữ, chi phí cao hơn tùy thuộc vào độ sâu của ngăn xếp cuộc gọi, do đó, nếu lỗi nằm sâu trong ngăn xếp cuộc gọi, hãy kiểm tra xem bạn có thể sử dụng các loại nguyên thủy đơn giản (mã lỗi số nguyên hoặc cờ boolean) để đẩy không lỗi sao lưu ngăn xếp, vì vậy nó có thể được ném gần hơn với lời gọi của người gọi.

  • Nếu bạn bao gồm ghi nhật ký như là một phần của phản hồi lỗi, người gọi sẽ dễ dàng rơi xuống để nối thông tin ngữ cảnh vào một đối tượng ngoại lệ. Bắt đầu từ nơi thông tin được sử dụng trong mã đăng nhập. Quyết định số lượng thông tin cần thiết cho nhật ký là hữu ích (mà không phải là một bức tường văn bản khổng lồ). Sau đó làm việc ngược lại để đảm bảo các lớp ngoại lệ có thể dễ dàng được cung cấp với thông tin đó.

Cuối cùng, trừ khi ứng dụng của bạn hoàn toàn có thể xử lý một cách duyên dáng với lỗi hết bộ nhớ, đừng cố gắng xử lý những lỗi đó hoặc các ngoại lệ thời gian chạy thảm khốc khác. Hãy để hệ điều hành giải quyết nó, bởi vì trong thực tế, đó là tất cả những gì bạn có thể làm.


2
Ngoại trừ viên đạn thứ hai đến viên đạn cuối cùng của bạn (về chi phí ngoại lệ), câu trả lời là tốt. Viên đạn về chi phí ngoại lệ là sai lệch, bởi vì trong tất cả các cách phổ biến để thực hiện ngoại lệ ở mức thấp, chi phí ném ngoại lệ hoàn toàn độc lập với độ sâu của ngăn xếp cuộc gọi. Các phương pháp báo cáo lỗi thay thế có thể tốt hơn nếu bạn biết rằng lỗi sẽ được xử lý bởi người gọi ngay lập tức, nhưng không nhận được một vài chức năng khỏi ngăn xếp cuộc gọi trước khi ném ngoại lệ.
Bart van Ingen Schenau

@BartvanIngenSchenau: Tôi đã cố gắng không gắn kết điều này với một ngôn ngữ cụ thể. Trong một số ngôn ngữ ( ví dụ Java ), độ sâu của ngăn xếp cuộc gọi ảnh hưởng đến chi phí khởi tạo. Tôi sẽ chỉnh sửa để phản ánh rằng nó không bị cắt và sấy khô.
Mark Benningfield

0

Vâng, tôi sẽ khuyên bạn trước tiên và trước hết hãy tạo một Exceptionlớp cơ sở cho tất cả các trường hợp ngoại lệ được kiểm tra có thể bị ứng dụng của bạn ném ra. Nếu ứng dụng của bạn được gọi DuckType, sau đó tạo một DuckTypeExceptionlớp cơ sở.

Điều này cho phép bạn bắt bất kỳ ngoại lệ nào của DuckTypeExceptionlớp cơ sở để xử lý. Từ đây, các trường hợp ngoại lệ của bạn sẽ phân nhánh với các tên mô tả có thể làm nổi bật hơn loại vấn đề. Ví dụ: "DatabaseConnectionException".

Hãy để tôi rõ ràng, tất cả nên được kiểm tra ngoại lệ cho các tình huống có thể xảy ra mà bạn có thể muốn xử lý duyên dáng trong chương trình của bạn. Nói cách khác, bạn không thể kết nối với cơ sở dữ liệu, do đó, một DatabaseConnectionExceptioncú ném mà sau đó bạn có thể bắt để chờ và thử lại sau một khoảng thời gian.

Bạn sẽ không thấy ngoại lệ được kiểm tra cho một vấn đề rất bất ngờ, chẳng hạn như truy vấn SQL không hợp lệ hoặc ngoại lệ con trỏ null và tôi khuyến khích bạn để các ngoại lệ này vượt qua hầu hết các mệnh đề bắt (hoặc bị bắt và truy xuất lại khi cần thiết) cho đến khi bạn đến chính bộ điều khiển chính nó, sau đó có thể bắt RuntimeExceptionhoàn toàn cho mục đích ghi nhật ký.

Sở thích cá nhân của tôi là không RuntimeExceptionxem xét lại một ngoại lệ không được kiểm tra như một ngoại lệ khác, vì bản chất của một ngoại lệ không được kiểm soát, bạn sẽ không mong đợi nó và do đó, việc lấy lại nó dưới một ngoại lệ khác là ẩn thông tin. Tuy nhiên, nếu đó là sở thích của bạn, bạn vẫn có thể bắt RuntimeExceptionvà ném một DuckTypeInternalExceptionthứ không giống như DuckTypeExceptionxuất phát từ đó RuntimeExceptionvà do đó không được kiểm tra.

Nếu bạn thích, bạn có thể phân loại ngoại lệ của mình cho các mục đích tổ chức, chẳng hạn như DatabaseExceptionbất kỳ điều gì liên quan đến cơ sở dữ liệu, nhưng tôi vẫn khuyến khích các ngoại lệ đó xuất phát từ ngoại lệ cơ sở của bạn DuckTypeExceptionvà do đó trừu tượng và do đó có tên mô tả rõ ràng.

Theo nguyên tắc chung, sản lượng đánh bắt thử của bạn nên được càng generic khi bạn di chuyển lên callstack của người gọi để xử lý trường hợp ngoại lệ, và một lần nữa, trong điều khiển chính của bạn, bạn sẽ không xử lý một DatabaseConnectionExceptionmà là đơn giản DuckTypeExceptiontrong đó tất cả các trường hợp ngoại lệ kiểm tra của bạn lấy được.


2
Lưu ý rằng câu hỏi được gắn thẻ "C ++".
Martin Ba

0

Hãy cố gắng đơn giản hóa điều đó.

Điều đầu tiên sẽ giúp bạn suy nghĩ trong một chiến lược khác là: bắt gặp nhiều ngoại lệ rất giống với việc sử dụng các ngoại lệ được kiểm tra từ Java (xin lỗi, tôi không phải là nhà phát triển C ++). Điều này không tốt vì nhiều lý do nên tôi luôn cố gắng không sử dụng chúng, và chiến lược ngoại lệ phân cấp của bạn nhớ cho tôi rất nhiều điều đó.

Vì vậy, tôi khuyên bạn nên có một chiến lược linh hoạt và khác: sử dụng các ngoại lệ và lỗi mã không được kiểm soát .

Ví dụ, xem mã Java này:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

Và ngoại lệ duy nhất của bạn :

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Chiến lược này tôi đã tìm thấy trên liên kết này và bạn có thể tìm thấy việc triển khai Java ở đây , nơi bạn có thể xem thêm chi tiết về nó, bởi vì đoạn mã trên được đơn giản hóa.

Vì bạn cần tách các ngoại lệ khác nhau giữa các ứng dụng "máy khách" và "máy chủ khác", bạn có thể có nhiều lớp mã lỗi thực hiện giao diện ErrorCode.


2
Lưu ý rằng câu hỏi được gắn thẻ "C ++".
Sjoerd

Tôi chỉnh sửa để thích nghi với ý tưởng.
Dherik

0

Trường hợp ngoại lệ là gotos không hạn chế và phải được sử dụng cẩn thận. Chiến lược tốt nhất cho họ là hạn chế chúng. Hàm gọi phải xử lý tất cả các ngoại lệ được ném bởi các hàm mà nó gọi hoặc chương trình bị chết. Chỉ có chức năng gọi có ngữ cảnh chính xác để xử lý ngoại lệ. Có chức năng hơn nữa trên cây gọi xử lý chúng là gotos không giới hạn.

Ngoại lệ không có lỗi. Chúng xảy ra khi hoàn cảnh ngăn chương trình hoàn thành một nhánh của mã và chỉ ra một nhánh khác để nó tuân theo.

Các ngoại lệ phải nằm trong ngữ cảnh của hàm được gọi. Ví dụ: một hàm giải phương trình bậc hai . Nó phải xử lý hai trường hợp ngoại lệ: Division_by_zero và vuông_root_of_negative_number. Nhưng những ngoại lệ này không có ý nghĩa đối với các hàm gọi bộ giải phương trình bậc hai này. Chúng xảy ra bởi vì phương pháp được sử dụng để giải các phương trình và chỉ đơn giản là nghĩ lại chúng làm lộ ra phần bên trong và phá vỡ đóng gói. Thay vào đó, Division_by_zero nên được truy xuất lại dưới dạng not_quadratic và Square_root_of_negative_number và no_real_roots.

Không cần một hệ thống ngoại lệ. Một enum của các trường hợp ngoại lệ (xác định chúng) được ném bởi một chức năng là đủ vì chức năng gọi phải xử lý chúng. Cho phép họ xử lý cây cuộc gọi là một goto ngoài ngữ cảnh (không giới hạn).

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.