Jakie są dobre sposoby równoważenia wyjątków informacyjnych i czystego kodu?

11

Dzięki naszemu publicznemu pakietowi SDK chcemy przekazywać bardzo pouczające informacje o przyczynach wyjątku. Na przykład:

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

Ma to jednak tendencję do zaśmiecania przepływu kodu, ponieważ koncentruje się raczej na komunikatach o błędach niż na tym, co robi kod.

Kolega zaczął refaktoryzować niektóre wyjątki, zgłaszając coś takiego:

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

Co sprawia, że ​​logika kodu jest łatwiejsza do zrozumienia, ale dodaje wiele dodatkowych metod obsługi błędów.

Jakie są inne sposoby na uniknięcie problemu logiki zakłóceń długich wyjątków? Pytam przede wszystkim o idiomatic C # / .NET, ale pomocne są również sposoby zarządzania innymi językami.

[Edytować]

Byłoby miło mieć zalety i wady każdego podejścia.

FriendlyGuy
źródło
4
IMHO rozwiązanie twojego kolegi jest bardzo dobre i myślę, że jeśli naprawdę dostaniesz wiele dodatkowych metod tego rodzaju, możesz ponownie użyć przynajmniej niektórych z nich. Posiadanie wielu małych metod jest w porządku, gdy są one dobrze nazwane, o ile tworzą one łatwe do zrozumienia elementy składowe twojego programu - wydaje się, że tak właśnie jest.
Dok. Brown
@DocBrown - Tak, podoba mi się ten pomysł (dodany poniżej jako odpowiedź, plusy / minusy), ale zarówno dla inteligencji, jak i dla potencjalnej liczby metod, zaczyna również wyglądać na bałagan.
FriendlyGuy,
1
Myśl: Nie popełniaj wiadomość z operatorem wszystkich szczegółach wyjątku. Połączenie użycia Exception.Datawłaściwości, „wybrednego” wychwytywania wyjątków, wywoływania kodu i dodawania własnego kontekstu, wraz z przechwyconym stosem wywołań, wszystko to zapewnia informacje, które powinny pozwolić na znacznie mniej pełne komunikaty. Wreszcie System.Reflection.MethodBasewygląda obiecująco, podając szczegółowe informacje na temat metody „wyjątkowej konstrukcji”.
radarbob
@radarbob sugerujesz, że być może wyjątki są zbyt szczegółowe? Być może zbytnio przypominamy logowanie.
FriendlyGuy,
1
@MackleChan, czytam, że tutaj paradygmat polega na umieszczeniu informacji w wiadomości, która próbuje „zydaktycznie powiedzieć, co się wydarzyło, jest z konieczności poprawna gramatycznie, a pozory AI:„… działał pusty konstruktor, ale… ” Naprawdę? Mój wewnętrzny koder sądowy postrzega to jako ewolucyjną konsekwencję powszechnego błędu powtarzających się rzutów tracących stos i nieświadomości Exception.Data. Nacisk należy położyć na przechwytywanie danych telemetrycznych. Refaktoryzacja jest w porządku, ale pomija problem.
radarbob,

Odpowiedzi:

10

Dlaczego nie mieć specjalistycznych klas wyjątków?

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

To przesuwa formatowanie i szczegóły do ​​samego wyjątku i pozostawia główną klasę w porządku.

ptyx
źródło
1
Plusy: bardzo czysty i zorganizowany, zapewnia sensowne nazwy dla każdego wyjątku. Minusy: potencjalnie wiele dodatkowych klas wyjątków.
FriendlyGuy,
Jeśli ma to znaczenie, możesz mieć ograniczoną liczbę klas wyjątków, przekazać klasę, z której wyjątek powstał jako parametr i mieć gigantyczny przełącznik wewnątrz bardziej ogólnych klas wyjątków. Ale to ma delikatny zapach - nie jestem pewien, czy tam pójdę.
ptyx,
to pachnie naprawdę źle (ściśle powiązane wyjątki z klasami). Myślę, że ciężko byłoby zrównoważyć dostosowywane komunikaty wyjątków od liczby wyjątków.
FriendlyGuy,
7

Wydaje się, że Microsoft (patrząc na źródło .NET) czasami używa ciągów zasobów / środowiska. Na przykład ParseDecimal:

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

Plusy:

  • Centralizacja komunikatów o wyjątkach, umożliwiająca ich ponowne użycie
  • Trzymanie wiadomości wyjątku (prawdopodobnie nie mają znaczenia dla kodu) z dala od logiki metod
  • Rodzaj zgłaszanego wyjątku jest jasny
  • Wiadomości mogą być lokalizowane

Cons:

  • Jeśli jeden komunikat wyjątku zostanie zmieniony, wszystkie się zmienią
  • Komunikat o wyjątku nie jest tak łatwo dostępny dla kodu zgłaszającego wyjątek.
  • Komunikat jest statyczny i nie zawiera żadnych informacji o błędnych wartościach. Jeśli chcesz go sformatować, w kodzie jest więcej bałaganu.
FriendlyGuy
źródło
2
Pominąłeś ogromną korzyść: Lokalizacja tekstu wyjątku
17 z 26
@ 17of26 - Dobrze, dodałem.
FriendlyGuy
Poparłem twoją odpowiedź, ale komunikaty o błędach wykonane w ten sposób są „statyczne”; nie możesz dodawać do nich modyfikatorów, tak jak robi to OP w swoim kodzie. Więc skutecznie pominąłeś niektóre jego funkcje.
Robert Harvey,
@RobertHarvey - dodałem go jako oszustwo. To wyjaśnia, dlaczego wbudowane wyjątki tak naprawdę nigdy nie dostarczają informacji lokalnych. Również FYI Jestem OP (wiedziałem o tym rozwiązaniu, ale chciałem wiedzieć, czy inni mają lepsze rozwiązania).
FriendlyGuy,
6
@ 17of26 jako programista, z pasją nienawidzę zlokalizowanych wyjątków. Muszę je odblokować za każdym razem, zanim będę mógł np. google dla rozwiązań.
Konrad Morawski,
2

W przypadku publicznego scenariusza SDK zdecydowanie rozważam użycie kontraktów Microsoft Code, ponieważ dostarczają one błędów informacyjnych, kontroli statycznych, a także można generować dokumentację do dodania do dokumentów XML i plików pomocy generowanych przez Sandcastle . Jest obsługiwany we wszystkich płatnych wersjach programu Visual Studio.

Dodatkową zaletą jest to, że jeśli Twoi klienci używają C #, mogą wykorzystać zestawy referencyjne umów kodu, aby wykryć potencjalne problemy nawet przed uruchomieniem kodu.

Pełna dokumentacja dotycząca umów kodowych znajduje się tutaj .

James World
źródło
2

Techniką, której używam, jest łączenie i outsourcing walidacji i rzucanie całkowicie funkcji użytecznej.

Najważniejszą zaletą jest to, że jest zredukowana do logiki biznesowej .

Założę się, że nie da się tego zrobić lepiej, jeśli nie można go jeszcze bardziej ograniczyć - aby wyeliminować wszystkie sprawdzania poprawności argumentów i zabezpieczenia obiektów przed stanem z logiki biznesowej, zachowując jedynie wyjątkowe warunki operacyjne.

Istnieją oczywiście sposoby na to - silnie napisany język, projektowanie „niedozwolonych obiektów w dowolnym momencie”, projektowanie na podstawie umowy itp.

Przykład:

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
    }
}
rwong
źródło
1

[uwaga] Skopiowałem to pytanie z odpowiedzi na wypadek, gdyby były na ten temat komentarze.

Przenieś każdy wyjątek do metody klasy, biorąc wszelkie argumenty wymagające formatowania.

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

Umieść wszystkie metody wyjątków w regionie i umieść je na końcu klasy.

Plusy:

  • Utrzymuje komunikat poza podstawową logiką metody
  • Pozwala na dodawanie informacji logicznych do każdej wiadomości (możesz przekazać argumenty do metody)

Cons:

  • Zaśmiecenie metod. Potencjalnie możesz mieć wiele metod, które zwracają wyjątki i nie są tak naprawdę powiązane z logiką biznesową.
  • Nie można ponownie użyć wiadomości w innych klasach
FriendlyGuy
źródło
Myślę, że przytaczane przez ciebie minusy znacznie przewyższają zalety.
neontapir,
@neontapir przeciw wszystkie łatwo rozwiązać. Metodom pomocniczym należy nadać nazwę IntentionRevealingNames i pogrupować je w niektóre #region #endregion(co powoduje, że są one domyślnie ukryte przed IDE), a jeśli mają zastosowanie w różnych klasach, należy umieścić je w internal static ValidationUtilityklasie. Przy okazji, nigdy nie narzekaj na długie nazwy identyfikatorów przed programistą C #.
rwong
Narzekałbym jednak na regiony. Moim zdaniem, jeśli chcesz uciekać się do regionów, klasa prawdopodobnie miała zbyt wiele obowiązków.
neontapir
0

Jeśli możesz uniknąć nieco bardziej ogólnych błędów, możesz napisać publiczną statyczną ogólną funkcję rzutowania, która podaje typ źródła:

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

Możliwe są różne warianty (pomyśl SdkHelper.RequireNotNull()), które sprawdzają tylko wymagania dotyczące danych wejściowych i rzucają, jeśli zawiodą, ale w tym przykładzie połączenie obsady z uzyskaniem wyniku jest samoudokumentujące i kompaktowe.

Jeśli korzystasz z .net 4.5, istnieją sposoby, aby kompilator wstawił nazwę bieżącej metody / pliku jako parametr metody (patrz CallerMemberAttibute ). Ale w przypadku zestawu SDK prawdopodobnie nie możesz wymagać od klientów przejścia na wersję 4.5.

jdv-Jan de Vaan
źródło
Chodzi bardziej o generowanie wyjątków (i zarządzanie informacjami w wyjątku, a nie o to, jak dużo go zaśmieca), a nie o ten konkretny przykład rzutowania.
FriendlyGuy,
0

To, co lubimy robić w przypadku błędów logiki biznesowej (niekoniecznie błędów argumentów itp.), To mieć jeden wyliczenie, które definiuje wszystkie potencjalne typy błędów:

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

    // ...
}

Te [Display(Name = "...")]atrybuty określają klucz w zasobie pliki mają być wykorzystane do tłumaczenia komunikatów o błędach.

Ponadto ten plik może służyć jako punkt początkowy do znalezienia wszystkich przypadków, w których w kodzie generowany jest określony rodzaj błędu.

Sprawdzanie reguł biznesowych można przekazać do wyspecjalizowanych klas Validatora, które dostarczają listy naruszonych reguł biznesowych.

Następnie używamy niestandardowego typu wyjątku do transportu naruszonych reguł:

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

Wywołania usługi wewnętrznej bazy danych są pakowane w ogólny kod obsługi błędów, który tłumaczy naruszoną zasadę Busines na czytelny dla użytkownika komunikat o błędzie:

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

Oto ToTranslatedString()metoda rozszerzenia, enumktóra może odczytać klucze zasobów z [Display]atrybutów i użyć ResourceManagerdo przetłumaczenia tych kluczy. Wartość odpowiedniego klucza zasobu może zawierać symbole zastępcze string.Formatodpowiadające podanemu MessageParameters. Przykład wpisu w pliku 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>

Przykładowe użycie:

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

Dzięki takiemu podejściu można oddzielić generowanie komunikatu o błędzie od wygenerowania błędu, bez potrzeby wprowadzania nowej klasy wyjątków dla każdego nowego typu błędu. Przydatne, jeśli różne nakładki powinny wyświetlać różne komunikaty, jeśli wyświetlany komunikat powinien zależeć od języka użytkownika i / lub roli itp.

Georg Patscheider
źródło
0

Użyłbym klauzul ochronnych, jak w tym przykładzie, aby można było ponownie użyć sprawdzania parametrów.

Ponadto można użyć wzorca konstruktora, aby połączyć więcej niż jedną weryfikację. Będzie to wyglądać trochę jak płynne twierdzenia .

Martin K.
źródło