Những cách tốt để cân bằng các ngoại lệ thông tin và mã sạch là gì?


11

Với SDK công khai của chúng tôi, chúng tôi có xu hướng muốn đưa ra những thông điệp rất thông tin về lý do tại sao một ngoại lệ xảy ra. Ví dụ:

if (interfaceInstance == null)
{
     string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    throw new InvalidOperationException(errMsg);
}

Tuy nhiên, điều này có xu hướng làm lộn xộn dòng mã, vì nó có xu hướng tập trung nhiều vào các thông báo lỗi hơn là những gì mã đang làm.

Một đồng nghiệp bắt đầu tái cấu trúc một số ngoại lệ ném vào một cái gì đó như thế này:

if (interfaceInstance == null)
    throw EmptyConstructor();

...

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Điều này làm cho logic mã dễ hiểu hơn, nhưng thêm rất nhiều phương thức bổ sung để xử lý lỗi.

Các cách khác để tránh vấn đề "logic lộn xộn thông điệp dài" là gì? Tôi chủ yếu hỏi về C # /. NET thành ngữ, nhưng cách các ngôn ngữ khác quản lý nó cũng hữu ích.

[Biên tập]

Sẽ rất tốt nếu có những ưu và nhược điểm của từng phương pháp.


4
IMHO giải pháp của đồng nghiệp của bạn là một giải pháp rất tốt và tôi đoán rằng nếu bạn thực sự có rất nhiều phương pháp bổ sung kiểu đó, bạn có thể sử dụng lại ít nhất một số trong số chúng. Có nhiều phương thức nhỏ là tốt khi các phương thức của bạn được đặt tên tốt, miễn là chúng tạo ra các khối xây dựng dễ hiểu của chương trình của bạn - điều này dường như là trường hợp ở đây.
Doc Brown

@DocBrown - Vâng, tôi thích ý tưởng này
FriendlyGuy

1
Suy nghĩ: Đừng biến thông điệp thành người mang tất cả các chi tiết ngoại lệ. Một sự kết hợp của việc sử dụng thuộc Exception.Datatính, bắt ngoại lệ "kén chọn", bắt mã cuộc gọi và thêm bối cảnh của chính nó, cùng với ngăn xếp cuộc gọi bị bắt, tất cả thông tin đóng góp sẽ cho phép các tin nhắn dài dòng hơn. Cuối cùng có System.Reflection.MethodBasevẻ hứa hẹn cung cấp chi tiết để chuyển sang phương pháp "xây dựng ngoại lệ" của bạn.
radarbob

@radarbob bạn đang gợi ý rằng có lẽ các ngoại lệ quá dài dòng? Có lẽ chúng ta đang làm cho nó quá nhiều như đăng nhập.
FriendlyGuy

1
@MackleChan, tôi đọc được rằng mô hình ở đây là đưa thông tin vào một thông điệp cố gắng nói 'chính xác những gì đã xảy ra, nhất thiết phải đúng về mặt ngữ pháp và giả vờ về AI: ".. thông qua công cụ xây dựng trống, nhưng .." Có thật không? Lập trình viên pháp y bên trong của tôi coi đây là hậu quả tiến hóa của lỗi phổ biến của việc ném lại dấu vết stack-stack và không nhận thức được Exception.Data. Sự nhấn mạnh nên được chụp từ xa. Tái cấu trúc ở đây là tốt, nhưng nó bỏ lỡ vấn đề.
radarbob

Câu trả lời:


10

Tại sao không có các lớp ngoại lệ chuyên ngành?

if (interfaceInstance == null)
{
    throw new ThisParticularEmptyConstructorException(<maybe a couple parameters>);
}

Điều đó đẩy định dạng và chi tiết đến ngoại lệ, và làm cho lớp chính không bị xáo trộn.


1
Ưu điểm: Rất sạch sẽ và có tổ chức, cung cấp tên có ý nghĩa cho mỗi ngoại lệ. Nhược điểm: có khả năng rất nhiều thêm lớp ngoại lệ.
FriendlyGuy

Nếu có vấn đề, bạn có thể có một số lớp ngoại lệ giới hạn, vượt qua lớp có ngoại lệ bắt nguồn như một tham số và có một chuyển đổi khổng lồ bên trong các lớp ngoại lệ chung hơn. Nhưng nó có mùi khét với nó - không chắc là tôi sẽ đến đó.
ptyx

có mùi rất tệ (ngoại lệ kết hợp chặt chẽ với các lớp). Tôi đoán rằng thật khó để cân bằng các thông báo ngoại lệ có thể tùy chỉnh so với số lượng ngoại lệ.
FriendlyGuy

7

Microsoft dường như (thông qua việc nhìn vào nguồn .NET) đôi khi sử dụng các chuỗi tài nguyên / Môi trường. Ví dụ ParseDecimal:

throw new OverflowException(Environment.GetResourceString("Overflow_Decimal"));

Ưu điểm:

  • Tập trung các thông báo ngoại lệ, cho phép sử dụng lại
  • Giữ thông điệp ngoại lệ (được cho là không quan trọng đối với mã) khỏi logic của các phương thức
  • Loại ngoại lệ được ném là rõ ràng
  • Tin nhắn có thể được bản địa hóa

Nhược điểm:

  • Nếu một thông báo ngoại lệ được thay đổi, tất cả đều thay đổi
  • Thông báo ngoại lệ không dễ dàng có sẵn cho mã ném ngoại lệ.
  • Thông báo là tĩnh và không chứa thông tin về giá trị nào sai. Nếu bạn muốn định dạng nó, nó sẽ lộn xộn hơn trong mã.

2
Bạn đã để lại một lợi ích to lớn: Bản địa hóa văn bản ngoại lệ
17 trên 26

@ 17of26 - Điểm tốt, đã thêm nó.
FriendlyGuy

Tôi nêu lên câu trả lời của bạn, nhưng thông báo lỗi được thực hiện theo cách này là "tĩnh"; bạn không thể thêm công cụ sửa đổi cho chúng giống như OP đang thực hiện mã của mình. Vì vậy, bạn đã bỏ đi một số chức năng của anh ấy.
Robert Harvey

@RobertHarvey - Tôi đã thêm nó dưới dạng con. Điều đó giải thích tại sao các ngoại lệ tích hợp không bao giờ thực sự cung cấp cho bạn thông tin địa phương. Ngoài ra, FYI tôi là OP (tôi biết về giải pháp này, nhưng tôi muốn biết liệu những người khác có giải pháp tốt hơn không).
FriendlyGuy

6
@ 17of26 với tư cách là một nhà phát triển, tôi ghét các ngoại lệ được bản địa hóa với niềm đam mê. Tôi phải hủy bỏ chúng mỗi lần trước khi tôi có thể. google cho các giải pháp.
Konrad Morawski

2

Đối với kịch bản SDK công khai, tôi sẽ cân nhắc mạnh mẽ việc sử dụng Hợp đồng mã Microsoft vì chúng cung cấp các lỗi thông tin, kiểm tra tĩnh và bạn cũng có thể tạo tài liệu để thêm vào tài liệu XML và các tệp trợ giúp được tạo bởi Sand Castle . Nó được hỗ trợ trong tất cả các phiên bản trả phí của Visual Studio.

Một lợi thế nữa là nếu khách hàng của bạn đang sử dụng C #, họ có thể tận dụng các tập hợp tham chiếu hợp đồng mã của bạn để phát hiện các vấn đề tiềm ẩn ngay cả trước khi họ chạy mã của họ.

Các tài liệu đầy đủ cho Hợp đồng mã là ở đây .


2

Kỹ thuật tôi sử dụng là kết hợp và thuê ngoài xác nhận và ném hoàn toàn vào một chức năng tiện ích.

Lợi ích quan trọng nhất là nó được giảm xuống một lớp trong logic kinh doanh .

Tôi cá là bạn không thể làm tốt hơn trừ khi bạn có thể giảm thêm - để loại bỏ tất cả các xác nhận đối số và bộ bảo vệ trạng thái đối tượng khỏi logic nghiệp vụ, chỉ giữ các điều kiện ngoại lệ hoạt động.

Tất nhiên, có nhiều cách để làm điều đó - Ngôn ngữ được gõ mạnh, "không có đối tượng không hợp lệ được phép bất cứ lúc nào", Thiết kế theo Hợp đồng , v.v.

Thí dụ:

internal static class ValidationUtil
{
    internal static void ThrowIfRectNullOrInvalid(int imageWidth, int imageHeight, Rect rect)
    {
        if (rect == null)
        {
            throw new ArgumentNullException("rect");
        }
        if (rect.Right > imageWidth || rect.Bottom > imageHeight || MoonPhase.Now == MoonPhase.Invisible)
        {
            throw new ArgumentException(
                message: "This is uselessly informative",
                paramName: "rect");
        }
    }
}

public class Thing
{
    public void DoSomething(Rect rect)
    {
        ValidationUtil.ThrowIfRectNullOrInvalid(_imageWidth, _imageHeight, rect);
        // rest of your code
    }
}

1

[lưu ý] Tôi đã sao chép câu hỏi này từ câu hỏi thành câu trả lời trong trường hợp có ý kiến ​​về nó.

Di chuyển mỗi ngoại lệ vào một phương thức của lớp, tham gia vào bất kỳ đối số nào cần định dạng.

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Gửi tất cả các phương thức ngoại lệ vào khu vực và đặt chúng ở cuối lớp.

Ưu điểm:

  • Giữ thông điệp ra khỏi logic cốt lõi của phương thức
  • Cho phép thêm thông tin logic vào từng thông báo (bạn có thể truyền các đối số cho phương thức)

Nhược điểm:

  • Phương pháp lộn xộn. Có khả năng bạn có thể có rất nhiều phương pháp chỉ trả lại ngoại lệ và không thực sự liên quan đến logic kinh doanh.
  • Không thể sử dụng lại tin nhắn trong các lớp khác

Tôi nghĩ rằng những khuyết điểm bạn trích dẫn vượt xa những lợi thế.
neontapir

@neontapir các khuyết điểm đều dễ dàng giải quyết. Các phương thức phụ trợ nên được đưa ra IntentionRevealsNames và được nhóm thành một số #region #endregion(điều này khiến chúng bị ẩn khỏi IDE theo mặc định) và nếu chúng được áp dụng trên các lớp khác nhau, hãy đặt chúng vào một internal static ValidationUtilitylớp. Btw, đừng bao giờ phàn nàn về tên định danh dài trước một lập trình viên C #.
rwong

Tôi sẽ phàn nàn về các khu vực mặc dù. Theo suy nghĩ của tôi, nếu bạn thấy mình muốn dùng đến các khu vực, lớp học có thể có quá nhiều trách nhiệm.
neontapir

0

Nếu bạn có thể thoát khỏi một số lỗi chung hơn một chút, bạn có thể viết hàm truyền chung chung cho bạn, đó là loại nguồn:

public static I CastOrThrow<I,T>(T t, string source)
{
    if (t is I)
        return (I)t;

    string errMsg = string.Format(
          "Failed to complete {0}, because type: {1} could not be cast to type {2}.",
          source,
          typeof(T),
          typeof(I)
        );

    throw new InvalidOperationException(errMsg);
}


/// and then:

var interfaceInstance = SdkHelper.CastTo<IParameter>(passedObject, "Action constructor");

Có thể có các biến thể (nghĩ SdkHelper.RequireNotNull()), chỉ kiểm tra các yêu cầu trên đầu vào và ném nếu chúng thất bại, nhưng trong ví dụ này kết hợp giữa các diễn viên với việc tạo ra kết quả là tự họa và nhỏ gọn.

Nếu bạn đang ở .net 4.5, có nhiều cách để trình biên dịch chèn tên của phương thức / tệp hiện tại làm tham số phương thức (xem CallerMemberAttibute ). Nhưng đối với SDK, có lẽ bạn không thể yêu cầu khách hàng của mình chuyển sang 4.5.


Đó là nhiều hơn về các trường hợp ngoại lệ nói chung (và quản lý thông tin trong một ngoại lệ so với mức độ mã hóa mã), chứ không phải về ví dụ cụ thể về việc truyền này.
FriendlyGuy

0

Những gì chúng tôi muốn làm cho các lỗi logic nghiệp vụ (không phải lỗi đối số không cần thiết, v.v.) là có một enum xác định tất cả các loại lỗi tiềm ẩn:

/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {

    /// <summary>
    /// Indicates that a valid body weight value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
    PatientBodyWeightMissingForDoseCalculation = 1,

    /// <summary>
    /// Indicates that a valid body height value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
    PatientBodyHeightMissingForDoseCalculation = 2,

    // ...
}

Các [Display(Name = "...")]thuộc tính xác định khóa trong các tệp tài nguyên sẽ được sử dụng để dịch các thông báo lỗi.

Ngoài ra, tệp này có thể được sử dụng làm điểm bắt đầu để tìm tất cả các lần xuất hiện trong đó một loại lỗi nhất định được tạo trong mã của bạn.

Việc kiểm tra Quy tắc kinh doanh có thể được ủy quyền cho các lớp Trình xác thực chuyên biệt đưa ra danh sách các Quy tắc kinh doanh bị vi phạm.

Sau đó, chúng tôi sử dụng loại Ngoại lệ tùy chỉnh để vận chuyển các quy tắc bị vi phạm:

[Serializable]
public class BusinessLogicException : Exception {

    /// <summary>
    /// The Business Rule that was violated.
    /// </summary>
    public BusinessRuleId ViolatedBusinessRule { get; set; }

    /// <summary>
    /// Optional: additional parameters to be used to during generation of the error message.
    /// </summary>
    public string[] MessageParameters { get; set; }

    /// <summary>
    /// This exception indicates that a Business Rule has been violated. 
    /// </summary>
    public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
        ViolatedBusinessRule = violatedBusinessRule;
        MessageParameters = messageParameters;
    }
}

Các cuộc gọi dịch vụ cuối cùng được gói trong mã xử lý lỗi chung, giúp chuyển Quy tắc Busines bị vi phạm thành thông báo lỗi người dùng có thể đọc được:

public object TryExecuteServiceAction(Action a) {
    try {
        return a();
    }
    catch (BusinessLogicException bex) {
        _logger.Error(GenerateErrorMessage(bex));
    }
}

public string GenerateErrorMessage(BusinessLogicException bex) {
    var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
    if (bex.MessageParameters != null) {
        translatedError = string.Format(translatedError, bex.MessageParameters);
    }
    return translatedError;
}

Đây ToTranslatedString()là một phương thức mở rộng để enumcó thể đọc các khóa tài nguyên từ [Display]các thuộc tính và sử dụng ResourceManagerđể dịch các khóa này. Giá trị cho khóa tài nguyên tương ứng có thể chứa giữ chỗ cho string.Format, khớp với mã được cung cấp MessageParameters. Ví dụ về một mục trong tệp resx:

<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
    <value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
    <comment>{0} ... Patient name</comment>
</data>

Ví dụ sử dụng:

throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);

Với phương pháp này, bạn có thể tách rời việc tạo thông báo lỗi từ việc tạo lỗi, mà không cần phải giới thiệu một lớp ngoại lệ mới cho mỗi loại lỗi mới. Hữu ích nếu các giao diện khác nhau sẽ hiển thị các thông báo khác nhau, nếu thông báo được hiển thị sẽ phụ thuộc vào ngôn ngữ và / hoặc vai trò của người dùng, v.v.


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.