Які хороші способи збалансування інформаційних винятків та чистого коду?


11

З нашим загальнодоступним SDK ми, як правило, хочемо надсилати дуже інформативні повідомлення про те, чому відбувається виняток. Наприклад:

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);
}

Однак це, як правило, захаращує потік коду, оскільки, як правило, робить велику увагу на повідомленнях про помилки, а не на те, що робить код.

Колега почав перетворювати деякі винятки, кидаючи щось подібне:

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);
}

Це полегшує розуміння логіки коду, але додає багато додаткових методів для обробки помилок.

Які ще способи уникнути проблеми "логіки, що заважають довгим виняткам"? Я в першу чергу запитую про ідіоматичну C # /. NET, але як корисні інші мови.

[Редагувати]

Було б непогано мати і плюси і мінуси кожного підходу.


4
ІМХО рішення вашого колеги є дуже хорошим, і я думаю, якщо ви отримаєте дійсно багато додаткових методів такого роду, ви можете використовувати принаймні деякі з них. Маючи безліч маленьких методів, це добре, коли ваші методи називаються добре, якщо вони створюють легкі для розуміння будівельні блоки вашої програми - це, мабуть, так і тут.
Док Браун

@DocBrown - Так, мені подобається ідея (додана як відповідь нижче, разом із плюсами / мінусами), але як для intellisense, так і для потенційної кількості методів вона також починає виглядати як захаращення.
FriendlyGuy

1
Думка: чи не зробити повідомлення про носії всіх деталей виключення. Поєднання використання Exception.Dataвластивості, "прискіпливого" вилучення винятків, виклику коду та додавання власного контексту, а також стека захоплених викликів вносять інформацію, яка повинна дозволяти набагато менше багатослівних повідомлень. Нарешті, це System.Reflection.MethodBaseвиглядає багатообіцяючим щодо надання деталей для переходу до вашого методу "побудова виключень".
radarbob

@radarbob Ви припускаєте, що, можливо, винятки занадто багатослівні? Можливо, ми робимо це занадто багато, як ведення журналу.
FriendlyGuy

1
@MackleChan, я читав, що парадигма тут полягає в тому, щоб поставити інформацію в повідомлення, яке намагається сказати "точно, що сталося, обов'язково граматично правильне, і прикид AI:" .. через порожній конструктор спрацював, але ... " Дійсно? Мій внутрішній криміналістичний кодер розглядає це як еволюційний наслідок поширеної помилки повторних кидків, що втрачають стек, і невідомості Exception.Data. Акцент повинен бути захоплено телеметрією. Рефакторинг тут прекрасний, але він пропускає проблему.
radarbob

Відповіді:


10

Чому б не провести спеціалізовані класи виключень?

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

Це підштовхує форматування та деталі до самого винятку, а основний клас залишає незабрудненим.


1
Плюси: Дуже чистий і організований, дає значущі назви для кожного винятку. Мінуси: потенційно багато зайвих класів виключень.
FriendlyGuy

Якщо це має значення, ви можете мати обмежену кількість класів винятків, передавати клас, де виняток виник як параметр, і мати гігантський перемикач всередині більш загальних класів виключень. Але це має слабкий запах - не впевнений, я б туди поїхав.
ptyx

що пахне дійсно погано (щільно поєднані винятки з класами). Я думаю, що було б важко збалансувати налаштовані повідомлення про виключення та кількість винятків.
FriendlyGuy

7

Microsoft, здається, (переглядаючи джерело .NET) іноді використовує рядки ресурсу / середовища. Наприклад ParseDecimal:

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

Плюси:

  • Централізація повідомлень про винятки, що дозволяє повторно використовувати
  • Тримаючи повідомлення про виключення (можливо, яке не має значення для кодування) подалі від логіки методів
  • Тип виключення, що викидається, зрозумілий
  • Повідомлення можна локалізувати

Мінуси:

  • Якщо одне повідомлення про виключення буде змінено, всі вони змінюються
  • Повідомлення про виняток не так легко доступне для коду, який викидає виняток.
  • Повідомлення статичне і не містить інформації про те, які значення неправильні. Якщо ви хочете відформатувати його, це більше захаращеність у коді.

2
Ви залишили величезну користь: Локалізація тексту виключення
17 з 26

@ 17of26 - Добрий момент, додав це.
FriendlyGuy

Я підтримав вашу відповідь, але повідомлення про помилки, зроблені таким чином, є "статичними"; Ви не можете додавати до них модифікаторів, як це робить ОП у своєму коді. Таким чином, ви фактично залишили частину його функціональності.
Роберт Харві

@RobertHarvey - я додав це як підступ. Це пояснює, чому вбудовані винятки ніколи насправді не дають місцевої інформації. Також FYI Я - ОП (я знав про це рішення, але хотів дізнатися, чи мають інші кращі рішення).
FriendlyGuy

6
@ 17of26 як розробник, я ненавиджу локалізовані винятки із пристрастю. Я маю щоразу їх розблокувати, перш ніж я можу, наприклад. google для рішень.
Конрад Моравський

2

Для загальнодоступного сценарію SDK я б настійно розглядав можливість використання контрактів з кодом Microsoft, оскільки вони забезпечують інформативні помилки, статичні перевірки, а також ви можете створювати документацію для додавання в XML-документи та файли довідки, створені Sandcastle . Він підтримується у всіх платних версіях Visual Studio.

Додатковою перевагою є те, що якщо ваші клієнти використовують C #, вони можуть використовувати довідкові збори вашого кодового контракту для виявлення потенційних проблем ще до того, як вони запустять свій код.

Повна документація для Код контрактів знаходиться тут .


2

Я використовую техніку - комбінувати та передавати аутентифікацію та взагалі передавати функцію корисності.

Найважливішою перевагою є те, що вона зводиться до однорівневої в логіці бізнесу .

Б'юсь об заклад, що ви не можете зробити кращого, якщо не зможете додатково зменшити його - усунути всі перевірки аргументів та засоби охорони об'єктів із бізнес-логіки, зберігаючи лише виняткові експлуатаційні умови.

Звичайно, є способи цього - чітко набрана мова, "не допустимі недійсні об'єкти ніколи", дизайн за контрактом тощо.

Приклад:

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

[Примітка] Я скопіював це з питання у відповідь, якщо є коментарі до нього.

Перемістіть кожен виняток у метод класу, взявши будь-які аргументи, які потребують форматування.

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);
}

Закріпіть усі методи виключень у регіоні та поставте їх у кінці заняття.

Плюси:

  • Виводить повідомлення з основної логіки методу
  • Дозволяє додавати логічну інформацію до кожного повідомлення (ви можете передати аргументи методу)

Мінуси:

  • Спосіб захаращення. Потенційно у вас може бути безліч методів, які просто повертають винятки і насправді не пов'язані з бізнес-логікою.
  • Неможливо повторно використовувати повідомлення в інших класах

Я думаю, що мінуси, які ви вказали, набагато перевищують переваги.
neontapir

@neontapir мінуси легко вирішити. Допоміжним методам слід надати IntentionRevealingNames та згрупувати їх у деякі #region #endregion(що спричиняє їх приховання від IDE за замовчуванням), а якщо вони застосовні для різних класів, перенесіть їх у internal static ValidationUtilityклас. До речі, ніколи не нарікайте на довгі імена ідентифікаторів перед програмістом C #.
rwong

Я б поскаржився на регіони. На мій погляд, якщо ви хочете вдатися до регіонів, клас, мабуть, мав занадто багато обов'язків.
neontapir

0

Якщо ви можете уникнути трохи більш загальних помилок, ви можете написати загальнодоступну загальну статичну функцію лиття для вас, що визначає тип джерела:

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");

Можливі варіанти (подумайте SdkHelper.RequireNotNull()), які перевіряють лише вимоги до входів і кидають, якщо вони не спрацьовують, але в цьому прикладі поєднання ролях і отримання результату є самодокументаційним та компактним.

Якщо ви перебуваєте на .net 4.5, є способи змусити компілятор вставити ім'я поточного методу / файлу як параметр методу (див. CallerMemberAttibute ). Але для SDK ви, ймовірно, не можете вимагати від клієнтів перейти на 4.5.


Йдеться більше про викиди винятків (і управління інформацією за винятком порівняно з тим, наскільки це захаращує код), а не про цей конкретний приклад кастингу.
FriendlyGuy

0

Що ми любимо робити для помилок ділової логіки (не обов'язково помилок аргументів тощо) - це мати єдиний перерахунок, який визначає всі потенційні типи помилок:

/// <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,

    // ...
}

Ці [Display(Name = "...")]атрибути визначають ключ в ресурсі файлів , які будуть використовуватися для перекладу повідомлень про помилки.

Також цей файл може використовуватися як відправна точка для пошуку всіх випадків, коли у вашому коді генерується певний тип помилок.

Перевірка бізнес-правил може бути делегована на спеціалізовані класи Validator, які дають списки порушених бізнес-правил.

Потім ми використовуємо спеціальний тип винятку для транспортування порушених правил:

[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;
    }
}

Виклики сервісу бекенда обгорнуті загальним кодом обробки помилок, який переводить порушене правило бізнесу в повідомлення про помилку, прочитане користувачем:

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;
}

Ось ToTranslatedString()метод розширення, для enumякого можна зчитувати ключі ресурсів з [Display]атрибутів і використовувати ResourceManagerдля перекладу цих ключів. Значення відповідного ключа ресурсу може містити заповнювачі string.Format, які відповідають заданому MessageParameters. Приклад запису у файлі 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>

Приклад використання:

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

При такому підході ви можете від'єднати генерацію повідомлення про помилку від генерації помилки, без необхідності вводити новий клас винятків для кожного нового типу помилок. Корисно, якщо різні фронти повинні відображати різні повідомлення, якщо відображене повідомлення має залежати від мови користувача та / або ролі тощо.


0

Я б використовував захисні пропозиції, як у цьому прикладі, щоб перевірити параметри можна повторно.

Крім того, ви можете використовувати шаблон для побудови, так що більше однієї перевірки може бути ланцюжком. Це буде трохи схоже на вільні твердження .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.