Enum jako kod błędu

3

Powiedzmy, że mamy metodę, która zwraca jakiś Result/Either. Chcemy, aby klient tej metody patrząc na kod błędu wiedział, co poszło nie tak. Widzę 3 opcje:

  1. Zwracamy jakiś generyczny AppError dający nam dość ogólną informację o błędzie (NotValid, NotFound), szczegóły są we właściwości Message.
    Zalety: Prostota
    Wady: Żeby dowiedzieć się, co dokładnie się posypało, musimy zajrzeć do Message. Podobnie klient naszego API webowego nie wie, co dokładnie się stało (może jedynie przekazać Message do użytkownika końcowego). Słabo się to testuje, jeśli metoda może zwrócić np. z 3 różnych powodów NotValid a my w teście chcemy mieć pewność, że nasza metoda zwróciła błąd z powodu X, a nie Y.

  2. Dla każdej metody tworzymy jakiegoś enuma z typem błędu specyficznym dla danej metody.
    Zalety: Znika problem z testowaniem z podejścia (1). Kod błędu możemy przekazać klientowi we zwrotce z API. Wszystko jest explicit.
    Wady: Dużo tych enumów trzeba będzie potworzyć :/

  3. Dla każdego modułu tworzymy jakiś globalny słownik możliwych błędów (podejście pośrednie między (1) i (2)).
    Zalety: Podobnie jak w (2).
    Wady: Nie jesteśmy już tak explicit jak w punkcie (2) - słownik może mieć np. 40 kodów błędów, a nasza metoda może zwracać np. tylko 3 różne kody błędów. Sygnatura metody jest zbyt ogólna, podobnie jak pewnie będzie dokumentacja endpointu w swaggerze.

Którą opcję wybrać i kiedy?

2

Jeżeli robisz modul to nie robisz globalnej biblioteki bledow tylko lokalna dla danego modulu w ten sposob kazdy klocek dba o siebie. Jezeli robisz biblioteke czy clase do istniejacego kodu no to konwencja taka jak byla w zalozeniu, jesli robisz standalone i to inni potem beda do ciebei sie podpinac to juz zalezy od ciebie

4

Sensowny podział aplikacji na moduły/biblioteki/..., i potem osobny error-enum per biblioteka sprawdzał się u mnie dotychczas najlepiej; dodatkowo taki error-enum pełni wtedy funkcję papierka lakmusowego co do tego kiedy moduł warto by podzielić na odrębne części :-)

0

Nie wiem czy enum to jest najlepszy pomysł, nawet jak będzie 1 per moduł to zacznie bardzo szybko puchnąć. Gorzej, jak niektóre błędy będą zwracane z dodatkowymi parametrami to kicha.

Osobiście korzystam z Try + stare Exception, tak wiem nie za bardzo funkcyjne ale daje radę.

Wydaje mi się że tutaj najlepszym wyjściem (przynajmniej w językach z pattern matchingiem) byłby interface na błąd i potem można mieć enum co implementuje ten interface dla jakiejś spójnej klasy błędów, value type'y dla bardziej złożonych błędów - tyle że w Javie bez pattern matching (ok ok coś tam jest w 17) praca z tym to może być mordęga jeżeli chciało by się sprawdzić konkretny typ błędu.

4

No ja widzę jeszcze jedną opcję: bazowa klasa ErrorBase i dziedziczące po niej klasy konkretnych błędów. Nie będzie ich wiele, z grubsza potrzebne są takie (w zależności oczywiście od rodzaju softu:

  • nie można znaleźć danych w źródle;
  • błąd zapisu danych;
  • błąd komunikacji po HTTP;
  • błąd wysłania do kafki (na przykład);
  • błąd opakowujący wyjątek;
  • błąd walidacji;
  • błąd jakiejś operacji biznesowej;
  • no i jakiś ostateczny błąd na pozostałe przypadki.

Całość jest polimorficzna, można np. dodawać metody pozwalające wyświetlić użytkownikowi inny komunikat niż ten, który ląduje w logach, można mieć konstruktor nadający id błędu, można je też ładnie pattern matchingiem skonwertować na odpowiedzi HTTP (jeśli robimy API).
Enumy (przynajmniej takie jak w C/C#) się słabo skalują i mają niewielkie możliwości. Same z nimi problemy w takiej sytuacji.

0

@somekind: Powiedzmy, że mamy metodę odpowiedzialną za dodawanie produktu do zamówienia, która może zwrócić NotValidError z 2 powodów:

  • produkt został wycofany ze sprzedaży,
  • zapasy produktu się wyczerpały.

Zrobiłbyś osobne klasy: ProductNotActiveError i ProductSoldOutError dziedziczące po NotValidError (1), czy w konstruktorze NotValidError przyjmowałbyś po prostu jakiś kod błędu, np. enum albo const string (2)?

Podejście (1) ma tę zaletę, że komunikat o błędzie (user-friendly i programmer-friendly) może być przechowywany bezpośrednio w klasie błędu i można po prostu zrobić:

return new ProductNotActiveError(...);

W podejściu (2) to metoda dodająca produkt musi przekazać komunikat(y) w konstruktorze błędu, np.

return new NotValidError(
    ErrorCode.ProductNotActive,
    "Produkt został wycofany ze sprzedaży.",
    $"Użytkownik: {userContext.UserName} próbował dodać nieaktywny produkt o ID: {request.ProductId} do zamówienia o ID: {order.Id}.");

Czy może komunikat dla użytkownika pobierać na podstawie kodu błędu z jakiegoś słownika przy mapowaniu błędu na odpowiedź HTTP, a sama metoda dodająca produkt niech jedynie zdefiniuje komunikat, który trafi do logów? Wydaje się, że tym przypadku mamy bardziej zarysowany podział UI i logiki aplikacji.

0

Jaki problem rozwiązuje tutaj użycie Enuma?

0

@Charles_Ray: Słabo się to testuje, jeśli metoda może zwrócić np. z 3 różnych powodów NotValid a my w teście chcemy mieć pewność, że nasza metoda zwróciła błąd z powodu X, a nie Y. No chyba, że w testach interesować nas będzie jedynie, czy dostaliśmy 400, a to z jakiego powodu, nie będzie istotne.

0

Ok, to możesz ustawić jakieś stringowe pole reason - czy to będzie za mało „enterprise”? :) Przecież klient nie będzie posiadał tego Twojego enuma.

Co więcej - takie wykorzystanie Enuma w testach może być zwodnicze, bo zobacz - jak zrobisz refaktoring rename na takim enumie, to IDE automatycznie poprawi Ci tez testy. Testy przechodzą, wdrażasz zmianę i nagle klienci wywalają się, bo pojawiła się nieznana wartość.

3
nobody01 napisał(a):

@somekind: Powiedzmy, że mamy metodę odpowiedzialną za dodawanie produktu do zamówienia, która może zwrócić NotValidError z 2 powodów:

  • produkt został wycofany ze sprzedaży,
  • zapasy produktu się wyczerpały.

Zrobiłbyś osobne klasy: ProductNotActiveError i ProductSoldOutError dziedziczące po NotValidError (1), czy w konstruktorze NotValidError przyjmowałbyś po prostu jakiś kod błędu, np. enum albo const string (2)?

Oddzielne klasy utworzę, jeśli będzie to miało sens, a więc jeśli w jakimś momencie coś będzie inaczej przetwarzane, albo w inny sposób prezentowane użytkownikowi końcowemu. Jeśli nie, to nie będę tworzył zbędnych klas, bo w ten sposób możemy szybko osiągnąć setki typów, które niczego tak naprawdę nie będą wnosiły.

Kwestia kodu błędu i komunikatu dla użytkownika to odrębna sprawa. Ja bym raczej starał się mieć zarówno kod błędu (niekoniecznie enum) jak i komunikat po angielsku dla konsumenta API. A UI powinien sobie zmapować kod błędu na komunikat w odpowiednim dla użytkownika języku.

1 użytkowników online, w tym zalogowanych: 0, gości: 1