Обработка ошибок с помощью определенных типов ошибок и сопоставления шаблонов C#

  • Михаил
  • 12 мин. на прочтение
  • 54
  • 25 Nov 2022
  • 25 Nov 2022

Обработка ошибок — один из скучных, но необходимых аспектов разработки. Часто это второстепенная мысль — « Я просто оберну это в try/catch и зарегистрирую » — но небольшое дополнительное усилие может сделать ваши приложения намного более надежными.

Во-первых, важно понимать, что ошибки и исключения — это разные понятия.

Исключения действительно должны создаваться только тогда, когда ситуация... исключительная. Исключение сообщает нам, что система находится в неожиданном состоянии, которое мы считали невозможным. Хорошим примером является старое любимое исключение нулевой ссылки. Это вызывается, когда мы обращаемся к свойству ссылки на объект, предполагая, что оно существует... но ссылка не указывает на объект.

Ошибки представляют собой вещи, которые могут пойти не так во время нормальной работы системы. Мы ожидаем , что они произойдут, и когда они случаются, мы реагируем на них соответствующим образом. Одним из примеров является ввод неверных адресных данных во время оформления заказа при совершении покупок в Интернете. Это будет происходить часто, поэтому нам нужно обработать это и обновить пользовательский интерфейс, чтобы помочь клиенту решить проблему.

Когда я проектирую систему, мне нравится создавать определенный тип C# для представления каждого потенциального случая ошибки. Это заставляет меня больше думать о том, что на самом деле может пойти не так, и полученный код, как правило, лучше подходит для этого.

Еще одним преимуществом этого является то, что это значительно упрощает обработку каждого случая ошибки. Классы ошибок могут содержать информацию , относящуюся к возникшей проблеме. Это делает потребляющий код менее сложным, поскольку вам не нужно анализировать ненужные данные, чтобы получить то, что вам нужно.

Давайте рассмотрим упрощенный пример, адаптированный из реального кода, который проверяет погашение подарочного сертификата.

Во-первых, мы создаем общий базовый Errorтип:

public abstract class Error
{
    public Error(string description) =>
        Description = description;

    public string Description { get; }
}

Все ошибки в приложении могут происходить из этого базового типа.

При любой ошибке, связанной с ваучером, нам, вероятно, потребуется знать код ваучера. Итак, мы немного конкретизируем:

public abstract class VoucherError : Error
{
    public VoucherError(string code, string description) 
        : base(description) => Code = code;
    
    public string Code { get; }
}

Теперь мы можем смоделировать наши конкретные случаи ошибок:

public class ZeroOrderTotalError : VoucherError 
{
    public ZeroOrderTotalError(string code) 
        : base(code, "Cannot redeem voucher against a cart with zero total.") {}
}

public class ZeroBalanceError : VoucherError 
{
    public ZeroBalanceError(string code) 
        : base(code, $"Voucher has a remaining balance of 0.") {}
}

public class InvalidRedemptionAmountError : VoucherError 
{
    public InvalidRedemptionAmountError(string code, decimal redemptionAmount)
        : base(code, $"Redemption amount must be greater than zero (was {redemptionAmount}).") =>
        RedemptionAmount = redemptionAmount;

    public decimal RedemptionAmount { get; }
}

public class InsufficientBalanceError : VoucherError 
{
    public InsufficientBalanceError(
        string code, 
        decimal redemptionAmount, 
        decimal balance)
        : base(code, $"Voucher balance ({balance:0.00}) is less than redemption amount ({redemptionAmount:0.00}).")
    {
        RedemptionAmount = redemptionAmount;
        Balance = balance;
    }

    public decimal RedemptionAmount { get; }

    public decimal Balance { get; }
}

Обратите внимание, что все они происходят от VoucherError.

Теперь, когда мы смоделировали наши ошибки, мы можем использовать их в нашем коде. Эта функция проверки возвращает кортеж , который сообщает вызывающей стороне, допустимо ли действие, а если нет, то в чем заключалась ошибка.

public static (bool canBeApplied, VoucherError error) ValidateVoucherRedemption(
    Voucher voucher, 
    Cart cart,
    decimal amountToRedeem)
{
    /* 
    Guard clauses are a sensible use of exceptions.
    
    Our logic assumes that all arguments are non-null references, so if 
    they *are* null, it's either programmer error, or something is wrong 
    in the consuming code. Either way, we want to fail immediately.
    */
    
    if (cart is null)
    {
        throw new ArgumentNullException(nameof(cart));
    }

    if (voucher is null)
    {
        throw new ArgumentNullException(nameof(voucher));
    }
    
    if (cart.Total <= 0)
    {
        // Cart doesn't have any items (or they're all free)
        return (false, new ZeroOrderTotalError(voucher.Code));
    }

    if (amountToRedeem <= 0)
    {
        // The customer is attempting to redeem a zero or negative value
        return (false, new InvalidRedemptionAmountError(voucher.Code, amountToRedeem));
    }

    if (voucher.Balance == 0)
    {
        // The voucher has been fully redeemed already
        return (false, new ZeroBalanceError(voucher.Code));
    }
    
    if (voucher.Balance < amountToRedeem)
    {
        // The remaining voucher balance doesn't cover the redemption amount
        return (false, new InsufficientBalanceError(voucher.Code, amountToRedeem, voucher.Balance));
    }

    return (true, default);
}

Часто сообщения об ошибках, которые вы хотите показать пользователям, будут отличаться от тех, которые вы хотите видеть как разработчик. Поскольку у нас есть вся информация, это теперь тривиально с использованием сопоставления с образцом C#:

public static string GetCustomerVoucherErrorMessage(VoucherError error)
{
    switch (error)
    {
        case ZeroBalanceError e:
            return $"Sorry, your voucher ({e.Code}) has no remaining balance.";
        case InsufficientBalanceError e:
            return $"Sorry, your voucher ({e.Code}) only has a remaining balance of {e.Balance:0.00}.";
        case InvalidRedemptionAmountError e:
            return "The amount you redeem must be greater than zero.";
        case ZeroOrderTotalError e:
            return "This cart is free: save your voucher for your next order!";
        default:
            throw new Exception($"Unknown voucher error: {error.GetType().Name}");
    }
}

Вот как мы будем использовать это, например, в методе действия ASP.NET:

[HttpPost]
public IActionResult RedeemVoucher(string code, decimal amount)
{
    // Imagine there is code to get voucher and cart data below

    ... 

    var (valid, error) = ValidateVoucherRedemption(voucher, cart, amount);

    if (!valid)
    {
        // Log the raw error, containing information for developers
        LogError(error);

        var viewModel = new RedeemVoucherViewModel {
            ErrorMessage = GetCustomerVoucherErrorMessage(error)
        };

        // Show the customer a response with the friendly error messaging
        return View(viewModel);
    }

    return RedirectToAction("Cart");
}