Polimorficzne wyjątki - pomysł i wykonanie

1

Pracowałem nad pewnym projektem w pracy (nie powiem nazwy firmy), i trafiłem na przypadek w którym co jakiś czas do naszej implementacji dochodzą nowe wyjątki. Na początku był jeden, potem dochodziły nowe, co jakiś czas, teraz z tego kawałka implementacji lecą 4 wyjątki. Są to różne case'y, ale domenowo zbliżone do siebie. Starałem się rozplanować architekturę, czy na pewno nie przesadzamy z tymi wyjątkami, próbowałem podejść do tego od strony Dependency Inversion, jak również Principle of Least Astonishment, Liskov Substitution, bo czułem że to że mamy 4 wyjątki tak podobne do siebie to coś podejrzanego; po kilku godzinach rozmyślań doszedłem do wniosku, że tak, te 4 różne wyjątki muszą być rozróżnione, dlatego że chcemy je obsługiwać w różnych sposób, ale też to że dodajemy co jakiś czas musi być ogarnięte w jakiś sposób, bo jeśli zachowamy liniowy trend to za 2 lata będzie ich 12 :D

Więc, wpadłem na kontrowersyjny pomysł. Wprowadziłem takie rozwiązanie w piątek, wyjaśniłem to mojemu zespołowi; na razie wprowadzamy to w jednym miejscu, zobaczymy jak pójdzie.

Pomysł to abstrakcyjny, polimorficzny wyjątek. Pomysł za nim jest mniej więcej taki sam jak przy Open/Closed principle, żebyśmy mogli dodawać nowe wyjątki bez potrzeby edycji miejsc w których są catche.

Wygląda to mniej więcej tak

abstract class DomainException extends RuntimeException {
  public abstract void handle(Visitor visitor);
}

class DomainCaseFirst extends DomainException {
  public void handle(Visitor visitor) {
    visitor.handleFirst();
  }
}

class DomainCaseSecond extends DomainException {
  public void handle(Visitor visitor) {
    visitor.handleFirst();
  }
}

użycie implementacji wygląda tak

try {
  domainLogic.act();
} catch (DomainException exception) {
  exception.handle(this.visitor);
}

Z logiki biznesowej będziemy rzucać odpowiednie implementacje wyjątków. Zaleta tego rozwiązania jest taka, że można dodawać nowe implementacje do domeny biznesowej oraz nowe case'y handlowania, bez potrzeby edycji już istniejących użyć. Jeśli będziemy chcieli handlować wyjątki dokładniej, to możęmy zrobić catch'a na specyficzny case

try {
  domainLogic.act();
} catch (DomainCaseSecond exception) {
  // special handling
} catch (DomainException exception) {
  // general handling
  exception.handle(this.visitor);
}

Nie jestem pewien czy ten pomysł wypali - próbuję go pierwszy raz. Nie jestem pewien też czy ktoś na to już kiedyś wpadł (pewnie tak), ale nie znalazłem takich rzeczy - jeśli ktoś zna takie użycie już to proszę o linka. Nie wydaje mi się że pomysł z tym żeby używać wyjątków do kontroli przepływu był czymś dziwnym; polimorficzne wyjątki już prędzej. Na pewno umieszczenie logiki w wyjątkach to byłby bardzo dziwny pomysł, ale call do wizytora - chyba jest git? Nie jestem na 100% pewien tego podejścia.

Jeśli ktoś miałby pomysł na inne rozwiązanie tego problemu, to też chętnie usłyszę pomysły.

Może napiszę post za 6-12 miesięćy z update'em jak rozwiązanie się u nas sprawdza.

8

No ja tam wolę Try/Option/Either ale jak ktoś lubi goto obsługę przez wyjątki no to spoko.

4

Za bardzo nie rozumiem tego postu, przecież w taki sposób wyglądają hierarchie wątków w praktycznie każdym języku https://rollbar.com/blog/java-exceptions-hierarchy-explained/ . Co do alternatyw to możesz zawrzeć błąd w typie jako jakiś Either, co jest używane w językach funkcyjnych

0
slsy napisał(a):

Za bardzo nie rozumiem tego postu, przecież w taki sposób wyglądają hierarchie wątków w praktycznie każdym języku https://rollbar.com/blog/java-exceptions-hierarchy-explained/ . Co do alternatyw to możesz zawrzeć błąd w typie jako jakiś Either, co jest używane w językach funkcyjnych

A w którym z nich w wyjątkach są calle do wizytorów?

scibi_92 napisał(a):

No ja tam wolę Try/Option/Either ale jak ktoś lubi goto obsługę przez wyjątki no to spoko.

A z kolei jak Optionalami mogę ohandlować różne case'y biznesowe, tak żeby followały Open/Close?

4

@TomRiddle: Nie widzę tego jak Open-Closed ma się do wyjątków, bo wyjątki to trochę wytrych w OOP. Całe OOP i wzorce opierają się jak abstrahować zachowania pod postacią interfejsów. Z drugiej strony w przypadku exception handlingu interesuje nas po prostu typ wyjątku, wyjątki same w sobie nie mają logiki.

Co do pomysłu to wydaje mi się, że bez gotowego kodu ciężko ogarnąć czy ma to sens

9

Logika w wyjątkach i jeszcze wizytor do tego. Może to architektoniczny sen, a może to Java.

3

Czy na prawdę na tym forum rację bytu mają jedynie przyklepane schematy, a ekstremalne, choć przemyślane próby są automatycznie utożsamiane z "kiepskim pomysłem"?

Szkoda że nie znalazłem nikogo kto rozważyłby potencjalne wady i zalety, zamiast od razu odrzucać dany pomysł.

Tak czy tak, dziękuję za uczestnictwo! :)

4

Zastanawiam się po co wizytator z jedną implementacją. Będziesz miał różne wizytatorzy?

UPDATE pewne przemyślenia: wyjątki rzuca się dlatego że wołany nie wie jak zareagować na błąd. Jakby wiedział to by mógł wykonać odpowiednią reakcję. Dzięki wyjątków to wołający może zareagować w taki sposób jak chce

Ty jednak masz jeden standardowy sposób reakcji na wyjątki (na wyjątek A zawsze zareagujesz w sposób A', na wyjątek B w sposób B' itd). zastanawiam się czy w takim razie naprawdę potrzebujesz wyjątków

0
KamilAdam napisał(a):

Zastanawiam się po co wizytator z jedną implementacją. Będziesz miał różne wizytatorzy?

Tak, aktualnie są dwie implementacje, ale spodziewam się że będzie więcej.

UPDATE pewne przemyślenia: wyjątki rzuca się dlatego że wołany nie wie jak zareagować na błąd. Jakby wiedział to by mógł wykonać odpowiednią reakcję. Dzięki wyjątków to wołający może zareagować w taki sposób jak chce

Tak, chyba że chce mieć różne sposoby handlowania go w różnych wizytorach. Jeśli wyjątek od razu byłby ohandlowany, to wołający nie mógłbym o tym zdecydować. Teraz np mogę przekazać anonimową implementację.

Ty jednak masz jeden standardowy sposób reakcji na wyjątki (na wyjątek A zawsze zareagujesz w sposób A', na wyjątek B w sposób B' itd). zastanawiam się czy w takim razie naprawdę potrzebujesz wyjątków

Tak, dlatego że ścieżka którą idzie implementacja jest zagmatwana, i czasem jest dynamiczna, więc wyjątki to idealny sposób żeby powiadomić wołającego że coś się stało.

3

Jaki problem rozwiązujesz? Chcesz w różny sposób (za pomocą wyjątków) obsługiwać 4 błędy - masz 4 podklasy wyjątków. Po co tutaj visitor? Czego nie rozumiem? :)

4

Czy na prawdę na tym forum rację bytu mają jedynie przyklepane schematy, a ekstremalne, choć przemyślane próby są automatycznie utożsamiane z "kiepskim pomysłem"?

Bo nie wiem czemu chcesz sterować logika aplikacji przez goto. Bo nie ma za bardzo czegoś takiego jak DomenowyException. Exception to może być w##DSD się połączenie z siecią czy z bazą danych (bo tego się nie spodziewamy).

0

Tak, coś takiego się stosuje i nie jest to nic nowego (jak sam podejrzewałeś). Ja to najczęściej stosuje w ramach aplikacji webowych, gdzie jakiś middleware przechwytuje taki wyjątek i zwraca odpowiedni kod. Chociaż nie wiem po co tutaj visitor...

@scibi_92 nie zgadzam się z argumentem że to odpowiednik sterowania goto. Rzucając wyjątek przerywam cały flow logiki biznesowej, a warstwa aplikacji może zwrócić odpowiedni kod błędu do klienta. W przypadku DomainException to jest zazwyczaj w moim przypadku 422.

Zdaje sobie sprawę że są alternatywne podejścia, ale to czy coś jest właściwe zależy od języka, konkretnej aplikacji itp. Wszystko ma swoje wady i zalety. Zwracanie jakiegoś wyniku ma np. taką wadę że trzeba to "bąbelkować" przez warstwy wywołań logiki, obsługiwać w różnych miejscach itp. Fajnie to wygląda w językach funkcyjnych pozwalających "naturalnie" zaprzęgać coś takiego w łańcuch wywołań funkcji, i przerywać dalsze wywoływanie w przypadku wyniku błędu. Ale coś takiego nie zawsze jest możliwe, i rzucenie wyjątkiem pozwala na szybkie przerwanie wykonywania się logiki bez nadmiernego sprawdzania wyniku pomiędzy różnymi etapami.

Exception to może być w##DSD się połączenie z siecią czy z bazą danych (bo tego się nie spodziewamy).

Exception to może być rowniez np. brakująca wartość w warstwie domeny, która powinna być wyłapana wcześniej przez aplikację klienta (np. walidacja DTO w web API).

2

Exception to może być rowniez np. brakująca wartość w warstwie domeny, która powinna być wyłapana wcześniej przez aplikację klienta (np. walidacja DTO w web API).

No dobra, tu masz rację. Jeśli zakładamy że pesel nie powinien przejśc niepoprawny do domeny, to wtedy rzeczywiście wtedy może to być jakiś wyjątek. Czyli w sumie to i tak jest sytuacja teoretycznie wyjątkowa :D

0

Małe sprostowanie na podstawie tego co odpisali @scibi_92 oraz @Miang. Oczywiście jestem zdania że wyjątki są od sytuacji wyjątkowych, i nie powinny być używane do sterowania wykonywaniem się logiki biznesowej. Jeśli taki jest Twój zamysł @TomRiddle to popieram przedmówców że to nie jest najlepszy pomysł.

Z drugiej strony jeśli to pomoże rozwiązać jakiś konkretny problem i przyniesie więcej pożytku niż szkody, to kim ja jestem żeby oceniać coś zastosowane na potrzeby Waszego konkretnego przypadku? Bezrefleksyjne trzymanie się zasad bo "tak jest i się z tym nie dyskutuje" zabija innowacje. Do wszystkiego trzeba podchodzić z głową, a jeśli coś nie wypali to wyciągnąć z tego wnioski.

1

Tak, aktualnie są dwie implementacje, ale spodziewam się że będzie więcej.

bez potrzeby edycji miejsc w których są catche.

brzmi to sensownie, aczkolwiek ja osobiście nie lubię w odwiedzaczu tego że wymaga mnóstwa kodu i pewnie bym olał compile-time safety* i poszedł w kierunku:

ale nie mam pojęcia jakby to się sprawdziło w praktyce

public interface IExceptionHandler
{
	void Handle(BaseEx b);
}

public class Handler1 : IExceptionHandler
{
	public void Handle(BaseEx ex)
	{
		if (ex is AEx a)
		{
			Internal_A_Handler(a);
		}
		else if (ex is BEx b)
		{
			Internal_B_Handler(b);
		}
		else
		{
			throw new NotSupportedException("lol");
		}
	}

	private void Internal_B_Handler(BEx b)
	{
		// blabla
	}

	private void Internal_A_Handler(AEx a)
	{
		// blabla
	}
}

* - chociaż może dałoby się nowym C# expr switchem poczarować

public enum Test
{
    A,
    B
}

static void Main(string[] args)
{
    var a = Test.A;
    var q = a switch
    {
        Test.A => throw new NotImplementedException()
    };
}

aby rzucił

screenshot-20211017193028.png

a tak btw

odwiedzacz, double dispatch

3
TomRiddle napisał(a):

Czy na prawdę na tym forum rację bytu mają jedynie przyklepane schematy, a ekstremalne, choć przemyślane próby są automatycznie utożsamiane z "kiepskim pomysłem"?

Szkoda że nie znalazłem nikogo kto rozważyłby potencjalne wady i zalety, zamiast od razu odrzucać dany pomysł.

Bo tak jakby podałeś za mało informacji odnośnie tego, po co Ci to wszystko.

TomRiddle napisał(a):

Pracowałem nad pewnym projektem w pracy (nie powiem nazwy firmy), i trafiłem na przypadek w którym co jakiś czas do naszej implementacji dochodzą nowe wyjątki. Na początku był jeden, potem dochodziły nowe, co jakiś czas, teraz z tego kawałka implementacji lecą 4 wyjątki. Są to różne case'y, ale domenowo zbliżone do siebie. Starałem się rozplanować architekturę, czy na pewno nie przesadzamy z tymi wyjątkami, próbowałem podejść do tego od strony Dependency Inversion, jak również Principle of Least Astonishment, Liskov Substitution, bo czułem że to że mamy 4 wyjątki tak podobne do siebie to coś podejrzanego; po kilku godzinach rozmyślań doszedłem do wniosku, że tak, te 4 różne wyjątki muszą być rozróżnione, dlatego że chcemy je obsługiwać w różnych sposób, ale też to że dodajemy co jakiś czas musi być ogarnięte w jakiś sposób, bo jeśli zachowamy liniowy trend to za 2 lata będzie ich 12 :D

Skoro już tak dobrymi praktykami rzucamy, to przyrost o 8 typów w 2 lata to nie jest taki duży problem, żeby teraz porzucić YAGNI.

żebyśmy mogli dodawać nowe wyjątki bez potrzeby edycji miejsc w których są catche.

Ja tu chyba widzę źródło problemu - czemu jeden wyjątek może być obsługiwany w więcej niż jednym catchu?

Z logiki biznesowej będziemy rzucać odpowiednie implementacje wyjątków. Zaleta tego rozwiązania jest taka, że można dodawać nowe implementacje do domeny biznesowej oraz nowe case'y handlowania, bez potrzeby edycji już istniejących użyć.

A tu drugie źródło - gdyby nie rzucać hierarchii wyjątków z logiki biznesowej, to nie trzeba byłoby ich jakoś wymyślnie obsługiwać. Wyjątek z logiki biznesowej powinien kończyć przetwarzanie i trafiać do logów, co więcej można z tym zrobić?

Nie wydaje mi się że pomysł z tym żeby używać wyjątków do kontroli przepływu był czymś dziwnym

Nie jest dziwny, za to jest równie powszechny jak okropny.

A co do samego designu - z klasy X przekazujesz sterowanie do wyjątku, który następnie przekazuje go do zależności klasy X. Jaka jest zaleta z tego kołowrotka uzasadniająca jego istnienie i utrzymywanie?

TomRiddle napisał(a):

Tak, dlatego że ścieżka którą idzie implementacja jest zagmatwana, i czasem jest dynamiczna, więc wyjątki to idealny sposób żeby powiadomić wołającego że coś się stało.

Do tego, to chyba eventy służą, ewentualnie wzorzec obserwator.

6

Bez konkretnego przykładu trochę trudno ogarnąć jaki problem chcesz konkretnie rozwiązać, ale mam wrażenie że próbujesz zbudować Event Bus w oparciu o wyjątki. Bo skoro masz jakieś skomplikowane handlery to sugeruje że to nie są żadne wyjątki u ciebie, tylko eventy domenowe.

2

Jeżeli wyjątki są domenowo zbliżone do siebie, to nie powinny być 1 bardziej generycznym wyjątkiem? O ile w ogole jest sens rzucać jakimiś wyjątkami z domeny na zewnętrz, jak napisał @somekind .
Zasadnicze pytanie, na które nie odpowiedziałeś, to jak są obsługiwane owe wyjątki i jaką logiką sterują. To rzeczywiście brzmi - tak jak wspomniał @Shalom - jakbyście zrobili z wyjątków zdarzenia domenowe. I teraz z jednego problemu, który został niepotrzebnie wprowadzony, robicie drugi, generyczne handlery i hierarchia wyjątków domenowych plus jakieś visitory. I cyk, kolejny stopień komplikacji architektury osiągnięty.

0
somekind napisał(a):
TomRiddle napisał(a):

Pracowałem nad pewnym projektem w pracy (nie powiem nazwy firmy), i trafiłem na przypadek w którym co jakiś czas do naszej implementacji dochodzą nowe wyjątki. Na początku był jeden, potem dochodziły nowe, co jakiś czas, teraz z tego kawałka implementacji lecą 4 wyjątki. Są to różne case'y, ale domenowo zbliżone do siebie. Starałem się rozplanować architekturę, czy na pewno nie przesadzamy z tymi wyjątkami, próbowałem podejść do tego od strony Dependency Inversion, jak również Principle of Least Astonishment, Liskov Substitution, bo czułem że to że mamy 4 wyjątki tak podobne do siebie to coś podejrzanego; po kilku godzinach rozmyślań doszedłem do wniosku, że tak, te 4 różne wyjątki muszą być rozróżnione, dlatego że chcemy je obsługiwać w różnych sposób, ale też to że dodajemy co jakiś czas musi być ogarnięte w jakiś sposób, bo jeśli zachowamy liniowy trend to za 2 lata będzie ich 12 :D

Skoro już tak dobrymi praktykami rzucamy, to przyrost o 8 typów w 2 lata to nie jest taki duży problem, żeby teraz porzucić YAGNI.

No, dla mnie już te 4 to za dużo, początkowo miał być jeden. Także IMO to nie YAGNI.

żebyśmy mogli dodawać nowe wyjątki bez potrzeby edycji miejsc w których są catche.

Ja tu chyba widzę źródło problemu - czemu jeden wyjątek może być obsługiwany w więcej niż jednym catchu?

Bo ta sama logika biznesowa jest wołana z trzech miejsc, jako user, z panelu administratora zupełnie inną drogą jako admin, oraz z crona jako user cron. Każdy z nich musi obsłużyć te 4 wyjątki w inny sposób, spodziewam się że być może, będzie więcej miejsc w przyszłości które będą ją wołały, ale tutaj to już raczej YAGNI.

Z logiki biznesowej będziemy rzucać odpowiednie implementacje wyjątków. Zaleta tego rozwiązania jest taka, że można dodawać nowe implementacje do domeny biznesowej oraz nowe case'y handlowania, bez potrzeby edycji już istniejących użyć.

A tu drugie źródło - gdyby nie rzucać hierarchii wyjątków z logiki biznesowej, to nie trzeba byłoby ich jakoś wymyślnie obsługiwać. Wyjątek z logiki biznesowej powinien kończyć przetwarzanie i trafiać do logów, co więcej można z tym zrobić?

No ale ja chcę tylko obsłużyć 4 sytuacje wyjątkowe. Dla usera chciałbym zwrócić odpowiedni response że się nie udało; dla admina chciałbym zwrócić cały stack track z debugiem oraz innymi rzeczami, a w cronie chciałbym żeby na wyjątek zrobił reschedule'a aktualnego joba. I jeśli ktoś mi chce tutaj mówić, że to nie tak powinno się dziać, to od razu odpowiadam - nie Twój interes co moja aplikacja robi; ja szukam tylko odpowiedniej architektury pod to.

Nie jest dziwny, za to jest równie powszechny jak okropny.

A co do samego designu - z klasy X przekazujesz sterowanie do wyjątku, który następnie przekazuje go do zależności klasy X. Jaka jest zaleta z tego kołowrotka uzasadniająca jego istnienie i utrzymywanie?

No, tak działa wizytor, prawda?

A to czy wizytor to tutaj dobre to podejście to nie wiem, nie byłem pewien na 100%, dlatego wjechałem na forum. Możliwe że jest jakiś inny wzorzec projektowy który lepiej do tego pasuje. Bo 4 catch'e to IMO zły pomysł.

TomRiddle napisał(a):

Tak, dlatego że ścieżka którą idzie implementacja jest zagmatwana, i czasem jest dynamiczna, więc wyjątki to idealny sposób żeby powiadomić wołającego że coś się stało.

Do tego, to chyba eventy służą, ewentualnie wzorzec obserwator.

No ale takie wenty i obserwator nie zatrzymają dalszego działania programu, prawda? A jak się pojawi błąd (wyjątek), to ja chcę przerwać wykonywanie i poinformować tego kto ją zawołał.

PS: no chyba że eventami da się jakoś przerwać wywoływanie?

Shalom napisał(a):

Bez konkretnego przykładu trochę trudno ogarnąć jaki problem chcesz konkretnie rozwiązać, ale mam wrażenie że próbujesz zbudować Event Bus w oparciu o wyjątki.

Ta sama odpowiedź - czy takim eventbusem da się przerwać wywołanie zadań logiki biznesowej? Nie chciałbym obsłużyć błędu, żeby potem wołana logika jeszcze coś tam robiła dodatkowego. Wyjątki zatrzymują wywołanie dopóki nie zostaną złapane, dlatego je wybrałem.

Bo skoro masz jakieś skomplikowane handlery to sugeruje że to nie są żadne wyjątki u ciebie, tylko eventy domenowe.

Hmm, no być może.

To jest np to że user chce wywołać jakąś akcję, ale nie ma na tyle kredytów na swoim koncie, powiedzmy. Albo, jest limit na jakąś akcję do powiedzmy 3ech dziennie, a user chce zrobić coś 4ty raz tego dnia. Ewentualnie to może być coś w stylu, "wyślij wiadomość do mojego polecającego", ale akurat ten user nie ma polecającego.

Ja taką akcję teraz stopuję wyjątkiem NotEnoughCreditsException, ActionLimitReachedException oraz RootReferalException. Być może to jest to o czym mówisz, że to jest "event domenowy", tylko jak takim sposobem zatrzymać flow? Tak jak wyjątki zatrzymują unless caught?

4

To jest np to że user chce wywołać jakąś akcję, ale nie ma na tyle kredytów na swoim koncie, powiedzmy. Albo, jest limit na jakąś akcję do powiedzmy 3ech dziennie, a user chce zrobić coś 4ty raz tego dnia. Ewentualnie to może być coś w stylu, "wyślij wiadomość do mojego polecającego", ale akurat ten user nie ma polecającego.

Serio to dla ciebie są sytuacje wyjątkowe? o_O Bo jak dla mnie to są zupełnie normalne przypadki użycia systemu. Ja bym coś takiego implementował normalnie przez Either (ewentualnie od biedy rzucenie wyjątku jeśli nie mamy fallbacka tylko chcemy wylecieć do samej góry)+generowanie eventu jeśli chcesz zeby jakieś inne elementy systemu jakoś to zdarzenie obsłużyły. W przypadku z wyjątkiem nie ma żadnego handlingu oprócz top-level catch który zamieni wyjątek na odpowiedni response code, a to że ta sytuacja generuje dodatkowo event który jakoś jest obsługiwany w ogóle nie ma zadnego związku z rzuconym wyjątkiem.
Dzięki temu np. obłsuga tego eventu w ogóle może być ogarnięta też przez zupełnie inne serwisy w naszym systemie, bo taki event może lecieć do jakiejs Kafki.

0
Shalom napisał(a):

To jest np to że user chce wywołać jakąś akcję, ale nie ma na tyle kredytów na swoim koncie, powiedzmy. Albo, jest limit na jakąś akcję do powiedzmy 3ech dziennie, a user chce zrobić coś 4ty raz tego dnia. Ewentualnie to może być coś w stylu, "wyślij wiadomość do mojego polecającego", ale akurat ten user nie ma polecającego.

Serio to dla ciebie są sytuacje wyjątkowe? o_O

Noooo, tak.

Bo jak dla mnie to są zupełnie normalne przypadki użycia systemu.

No zależy chyba jak na to patrzysz, prawda?

Z punktu widzenia aplikacji - tak, normalne. Z punktu widzenia domeny biznesowej, nielegalna akcja IMO. Takie jest moje zdanie, nie chcę wchodzić w dyskusje na ten temat. Jakby, naturalne wydaje mi się że jak user chce coś kupić, ale nie ma kasy, to powinien dostać wyjątek (IMO!).

Ja bym coś takiego implementował normalnie przez Either (ewentualnie od biedy rzucenie wyjątku jeśli nie mamy fallbacka tylko chcemy wylecieć do samej góry)+generowanie eventu jeśli chcesz zeby jakieś inne elementy systemu jakoś to zdarzenie obsłużyły. W przypadku z wyjątkiem nie ma żadnego handlingu oprócz top-level catch który zamieni wyjątek na odpowiedni response code, a to że ta sytuacja generuje dodatkowo event który jakoś jest obsługiwany w ogóle nie ma zadnego związku z rzuconym wyjątkiem.

No dobrze, ale czy w taki sposób można zatrzymać flow programu? Jak mówiłem, jak logika biznesowa robi trzy rzeczy, A, B, C, i z B poleci wyjątek, to nie chcę żeby C się zrobiło. Z tymi ewentami tak nie ma, rozumiem? Że wykonają się tak czy tak wszystkie, i musiałbym dodać jakiś inny mechanizm który by nie dopuścił żeby C się wykonało?

Dzięki temu np. obłsuga tego eventu w ogóle może być ogarnięta też przez zupełnie inne serwisy w naszym systemie, bo taki event może lecieć do jakiejs Kafki.

No, nie, kafki w ogóle w to nie chcę mieszać, i nie chcę żeby inne serwisy handlowały ten wyjątek, tylko caller.

4

Być może to jest to o czym mówisz, że to jest "event domenowy", tylko jak takim sposobem zatrzymać flow? Tak jak wyjątki zatrzymują unless caught?

Zdradzę sekret: słowo kluczowe return nie musi być na samym końcu metody.

0

Ja zagram tutaj adwokata diabła, bo- raz jeszcze podkreślając- jeśli rzucimy wyjątkiem którego już nie obsługuje logika biznesowa, a po prostu jak w opisanym przeze mnie wcześniej przypadku obsłuży go warstwa aplikacji i odpowiednio zwróci komunikat o błędzie do klienta (web UI czy cokolwiek innego), to takie podejście jest jak najbardziej normalne. Owszem, można zastosować either itp, jak i całą IFologię z tym związaną, ale to zależy od konkretnego projektu. Czasem to właśnie takie bawienie się w opcjonale będzie przerostem formy nad treścią, czasem nie ale wprowadzenie tego retrospekcyjnie nie da wymiernych korzyści itp. To kwestia konkretnego przypadku więc bezrefleksyjne rzucanie się bo "olaboga, dla czego tak?" jest trochę bez sensu. Co za tym idzie odpowiadając na Twój komentarz @scibi_92

if lepszy jest na pewno od wyjątku

Nie, nie lepszy, ani nie gorszy. To naprawdę zależy.

Jeśli natomiast chodzi o Twój wymóg @TomRiddle aby komunikować pewne wydarzenia innym modułom w systemie to faktycznie do tego najlepiej nadadzą się eventy. Najlepiej poczytaj sobie o architecture event-driven. Jest to coś co można dosyć łatwo wprowadzić nawet w procesie, przy wykorzystaniu wzorca mediator. Nie musi to od razy być coś asynchronicznego z kolejkowaniem jeśli nie ma takiej potrzeby.

3

@TomRiddle:

Z punktu widzenia aplikacji - tak, normalne. Z punktu widzenia domeny biznesowej, nielegalna akcja IMO.

Nie bardzo cię rozumiem. Nielegalność akcji nie ma nic wspólnego z wyjątkowością. To jest normalne działanie systemu - nie masz kasy na koncie, nie możesz wypłacić. To jest zwykła domenowa walidacja akcji, nie ma w niej nic wyjątkowego.

No dobrze, ale czy w taki sposób można zatrzymać flow programu? Jak mówiłem, jak logika biznesowa robi trzy rzeczy, A, B, C, i z B poleci wyjątek, to nie chcę żeby C się zrobiło. Z tymi ewentami tak nie ma, rozumiem? Że wykonają się tak czy tak wszystkie, i musiałbym dodać jakiś inny mechanizm który by nie dopuścił żeby C się wykonało?

Tak jak pisałem - albo robimy tam jakieś Eithery i wtedy akcja "sama" się przewie bo jak masz jakieś validateAction().map(this::doX).map(this::doY)... to pierwszy Left sprawi że kolejne map czy flatMap się już nie wykonają, albo, tak jak pisałem, możesz rzucić sobie wyjątek jeśli bardzo chcesz, ale to tylko takie ułatwienie żeby "wyskoczyć" do samej góry wywołań i zwrócić jakiś error code.

Jeśli chcesz zakomunikować komponentom systemu, że "coś się stało" to powinieneś wygenerować sobie odpowiedni event i tyle.

No, nie, kafki w ogóle w to nie chcę mieszać, i nie chcę żeby inne serwisy handlowały ten wyjątek, tylko caller.

Jasne, teraz nie, ale sam pisałeś ze dochodzą ci kolejne "handlery". Nie zdziwi mnie jak pojawi sie wymaganie że jak user wykona nielegalną akcje, to taka informacja powinna zostać przesłana do jakiegoś serwisu security czy audit ;) Chciałem tylko pokazać że opcja z eventami nie ma takich mocnych ograniczeń jak to co teraz robisz.

Kolejny problem jaki widzę z tymi twoimi wyjątkami jest taki, ze ktoś ci moze ten wyjątek złapać, mniej lub bardziej przypadkiem, i nagle w ogóle te twoje handlery się nie odpalą. Ot głupi przykład to odpalenie czegoś w innym watku albo z jakiegoś CompletableFuture. Exlpicite emitowanie eventu jest dużo bardziej niezawodne.

2
TomRiddle napisał(a):

No ale ja chcę tylko obsłużyć 4 sytuacje wyjątkowe. Dla usera chciałbym zwrócić odpowiedni response że się nie udało; dla admina chciałbym zwrócić cały stack track z debugiem oraz innymi rzeczami, a w cronie chciałbym żeby na wyjątek zrobił reschedule'a aktualnego joba. I jeśli ktoś mi chce tutaj mówić, że to nie tak powinno się dziać, to od razu odpowiadam - nie Twój interes co moja aplikacja robi; ja szukam tylko odpowiedniej architektury pod to.

Wreszcie opisałeś problem, który chcesz rozwiązać. Powinieneś w pierwszym poście. :P

Skoro masz trzy aplikacje, każda inaczej obsługuje wyjątki, to oznacza 3 catche (po jednym w każdej aplikacji). Nie trzeba żadnych wzorców.

No, tak działa wizytor, prawda?

Owszem, w ten sposób działa, i przez to jest tak bardzo nieintuicyjny. :)

To jest np to że user chce wywołać jakąś akcję, ale nie ma na tyle kredytów na swoim koncie, powiedzmy. Albo, jest limit na jakąś akcję do powiedzmy 3ech dziennie, a user chce zrobić coś 4ty raz tego dnia. Ewentualnie to może być coś w stylu, "wyślij wiadomość do mojego polecającego", ale akurat ten user nie ma polecającego.

Ja taką akcję teraz stopuję wyjątkiem NotEnoughCreditsException, ActionLimitReachedException oraz RootReferalException. Być może to jest to o czym mówisz, że to jest "event domenowy", tylko jak takim sposobem zatrzymać flow? Tak jak wyjątki zatrzymują unless caught?

W ogóle nie powinieneś zaczynać flow, skoro warunki nie są spełnione. Wtedy nie trzeba byłoby niczego przerywać wyjątkiem.

TomRiddle napisał(a):

Z punktu widzenia aplikacji - tak, normalne. Z punktu widzenia domeny biznesowej, nielegalna akcja IMO. Takie jest moje zdanie, nie chcę wchodzić w dyskusje na ten temat. Jakby, naturalne wydaje mi się że jak user chce coś kupić, ale nie ma kasy, to powinien dostać wyjątek (IMO!).

Wyjątek powinien dostać np., gdy mimo walidacji na każdym etapie będzie brakowało jakichś wymaganych danych, albo gdy utraci połączenie z bazą danych, a nie wtedy, gdy próbuje wykonać coś wbrew regułom biznesowym. To, że ludzie się mylą albo kombinują to nie jest nic wyjątkowego, to normalna i oczywista rzecz. Coś oczywistego nie może być jednocześnie wyjątkowe.

2

Ja nadal nie rozumiem dlaczego nie można:

  • Mieć funkcję/komendę/akcję/whatever która łączy A,B i C i która zwróci wartość wskazująca czy operacja się powiodła, zamiast propagować wyjątek do klientów. Kij w wyjątkiem, nich sobie będzie, ale niech nie niesie logiki i będzie obsłużony w jednym miejscu. Wyjątek może nie dopuścić do wykonania C, skoro tak to macie rozwiązane, ok.
  • Informacje dla administratora załatwić poprzez konfigurację poziomu logowania.
  • Reschedule uzależnić od tego czy akcja się powiodła. Ewentualnie reschedule uzależnić od jakiegoś health checka?

Jeżeli problemem jest to, że masz 3 klientów, i każdy ma bogatą logikę obsługiwania wyjątków i sterowanie logiką na tej podstawie, to pytanie czy nie da się tego zamknąć w jednym bycie, który wyjątków na zewnątrz nie zwraca.

2

@Shalom:

No, nie, kafki w ogóle w to nie chcę mieszać, i nie chcę żeby inne serwisy handlowały ten wyjątek, tylko caller.

Jasne, teraz nie, ale sam pisałeś ze dochodzą ci kolejne "handlery". Nie zdziwi mnie jak pojawi sie wymaganie że jak user wykona nielegalną akcje, to taka informacja powinna zostać przesłana do jakiegoś serwisu security czy audit ;) Chciałem tylko pokazać że opcja z eventami nie ma takich mocnych ograniczeń jak to co teraz robisz.

Jeszcze warto podkreślić że tak jak już wspomniałem eventy != używanie kolejek, asynchroniczność itp. Jeśli coś takiego nie wchodzi w grę to spoko- można emitować eventy i obsługiwać je w ramach tego samego procesu.

0
Shalom napisał(a):
TomRiddle napisał(a):

Z punktu widzenia aplikacji - tak, normalne. Z punktu widzenia domeny biznesowej, nielegalna akcja IMO.

Nie bardzo cię rozumiem. Nielegalność akcji nie ma nic wspólnego z wyjątkowością.

No dobra, tu się zgadzam.

To jest normalne działanie systemu - nie masz kasy na koncie, nie możesz wypłacić. To jest zwykła domenowa walidacja akcji, nie ma w niej nic wyjątkowego.

Ale jednak, chciałbym też móc moim podejściem ohandlować coś co faktycznie jest wyjątkowe. Np gdyby ktoś podał zły SECRET_KEY w .env, to chciałbym żeby część userowa na to zareagowała inaczej niż część adminiowa albo cronowa. A co do postu @somekind, to to nie są 3 aplikacje. To jest jedna aplikacja, do której mogą się zalogować dwa typy userów (plus job w cronie). Panel admina i panel usera to jedna aplikacja.

TomRiddle napisał(a):

No dobrze, ale czy w taki sposób można zatrzymać flow programu? Jak mówiłem, jak logika biznesowa robi trzy rzeczy, A, B, C, i z B poleci wyjątek, to nie chcę żeby C się zrobiło. Z tymi ewentami tak nie ma, rozumiem? Że wykonają się tak czy tak wszystkie, i musiałbym dodać jakiś inny mechanizm który by nie dopuścił żeby C się wykonało?

Tak jak pisałem - albo robimy tam jakieś Eithery i wtedy akcja "sama" się przewie bo jak masz jakieś validateAction().map(this::doX).map(this::doY)... to pierwszy Left sprawi że kolejne map czy flatMap się już nie wykonają, albo, tak jak pisałem, możesz rzucić sobie wyjątek jeśli bardzo chcesz, ale to tylko takie ułatwienie żeby "wyskoczyć" do samej góry wywołań i zwrócić jakiś error code.

No dobra, tylko wtedy te Eithery muszą przejść przez wszystkie warstwy, nie miałbym prostych metod które zwracają jedną wartość, tylko taki Either :/ Nie jestem pewien czy chciałbym mieć coś takiego w swojej logice biznesowej. Im bardziej skomplikowana logika biznesowa, tym taki either będzie bardziej skomplikowany. To tak na prawdę, gdyby takie potencjalne źródło problemu mogło wyjść z bardzo niskiego poziomu z logiki biznesowej, to to by znaczyło że praktycznie każda funkcja u mnie musiałaby zwracać Either. I'm not sure :/

Jeśli chcesz zakomunikować komponentom systemu, że "coś się stało" to powinieneś wygenerować sobie odpowiedni event i tyle.

No, tylko w sumie nie chcę tego robić. Chcę zastopować akcję, którą zrobił user/admin/cron, i dać możliwość callerowi zareagować na to.

Wysyłanie eventów IMO byłoby spoko, gdyby to były jakieś peryferia aplikacji (typu właśnie z web do kafki), ale w samej logice biznesowej to mi się wydaje bad idea.

No, nie, kafki w ogóle w to nie chcę mieszać, i nie chcę żeby inne serwisy handlowały ten wyjątek, tylko caller.

Jasne, teraz nie, ale sam pisałeś ze dochodzą ci kolejne "handlery". Nie zdziwi mnie jak pojawi sie wymaganie że jak user wykona nielegalną akcje, to taka informacja powinna zostać przesłana do jakiegoś serwisu security czy audit ;) Chciałem tylko pokazać że opcja z eventami nie ma takich mocnych ograniczeń jak to co teraz robisz.

No, jeśli zajdzie taka potrzeba, to eventy pewnie będą oczywistym wyjściem. Ale na razie nie ma.

Kolejny problem jaki widzę z tymi twoimi wyjątkami jest taki, ze ktoś ci moze ten wyjątek złapać, mniej lub bardziej przypadkiem, i nagle w ogóle te twoje handlery się nie odpalą. Ot głupi przykład to odpalenie czegoś w innym watku albo z jakiegoś CompletableFuture. Exlpicite emitowanie eventu jest dużo bardziej niezawodne.

No, niby tak, ale ten sam problem jest z Eitherami, ktoś go może przechwycić i zwrócić inny either.

Poza tym, jeśli jakiś caller sobie złapie ten wyjątek i nie puści go dalej, to znaczy że obsłużył już błąd i nie powinien być propagowany dalej, więc nie wiem czy to tak znowu źle. A jeśli mówisz o tym że w mojej logice biznesowej coś go złapie, i nie puści, to to znaczy że jest w niej bug, i unity to powinny wykryć IMO.

somekind napisał(a):

Skoro masz trzy aplikacje, każda inaczej obsługuje wyjątki, to oznacza 3 catche (po jednym w każdej aplikacji). Nie trzeba żadnych wzorców.

No ale to jest jedna aplikacja.

TomRiddle napisał(a):

No, tak działa wizytor, prawda?

Owszem, w ten sposób działa, i przez to jest tak bardzo nieintuicyjny. :)

Dla mnie ma sens :D Może po prostu trzeba dobrze znać wzorce, a nie tylko o nich słyszeć kiedyś :D Joke. No offence.

To jest np to że user chce wywołać jakąś akcję, ale nie ma na tyle kredytów na swoim koncie, powiedzmy. Albo, jest limit na jakąś akcję do powiedzmy 3ech dziennie, a user chce zrobić coś 4ty raz tego dnia. Ewentualnie to może być coś w stylu, "wyślij wiadomość do mojego polecającego", ale akurat ten user nie ma polecającego.

Ja taką akcję teraz stopuję wyjątkiem NotEnoughCreditsException, ActionLimitReachedException oraz RootReferalException. Być może to jest to o czym mówisz, że to jest "event domenowy", tylko jak takim sposobem zatrzymać flow? Tak jak wyjątki zatrzymują unless caught?

W ogóle nie powinieneś zaczynać flow, skoro warunki nie są spełnione. Wtedy nie trzeba byłoby niczego przerywać wyjątkiem.

No ale to są case'y których nie da się/ciężko sprawdzić przed wywołaniem flow. To jest coś w stylu

user.buyItem("Monitor");

...gdy nie wiemy ile user ma kasy i nie wiemy ile "Monitor" kosztuje, cała ta sprawdzajka dzieje się w środku logiki biznesowej. Żeby to sprawdzić zanim się ją zawoła, to trzebaby ją zduplikować w sumie.

Z punktu widzenia aplikacji - tak, normalne. Z punktu widzenia domeny biznesowej, nielegalna akcja IMO. Takie jest moje zdanie, nie chcę wchodzić w dyskusje na ten temat. Jakby, naturalne wydaje mi się że jak user chce coś kupić, ale nie ma kasy, to powinien dostać wyjątek (IMO!).

Wyjątek powinien dostać np., gdy mimo walidacji na każdym etapie będzie brakowało jakichś wymaganych danych, albo gdy utraci połączenie z bazą danych, a nie wtedy, gdy próbuje wykonać coś wbrew regułom biznesowym. To, że ludzie się mylą albo kombinują to nie jest nic wyjątkowego, to normalna i oczywista rzecz. Coś oczywistego nie może być jednocześnie wyjątkowe.

No tak, i takie sytuacje też są, i takie również chcę obsłużyć - tylko że inaczej dla różnych caller'ów (inaczej dla usera, dla admina i dla crona).

3

to by znaczyło że praktycznie każda funkcja u mnie musiałaby zwracać Either

Często właśnie tak jest! Wszystko co może się "nie udać" zwraca Either czy inny Optional. Niczym się to specjalnie nie rózni od jakiegoś throws XYZException. Najgorsze co można zrobić to rzucanie RuntimeException z takiego miejsca, bo RuntimeException jest podobny tutaj do nulla -> nie widać go! Optinal czy Either czy nawet ten throws... sygnalizują na poziomie typesystemu że coś się może nie udać.

i dać możliwość callerowi zareagować na to.

Jak dla mnie to jest idealny przykład na użycie Eithera skoro chcesz żeby ktoś kto zawołał daną metodę mógł zareagować na jakiegoś Lefta.

No, niby tak, ale ten sam problem jest z Eitherami, ktoś go może przechwycić i zwrócić inny either.

Nie zgodzę się, bo sygnatura funkcji się nagle nie będzie pasować :) Rzucenie lub nie jakiegoś RuntimeException jest niewidzialne. Ktoś go złapie i nawet nie będzie wiedział że ty masz gdzieś wyżej jakiś handler który chciał to złapać. W przypadku Eithera od razu będzie widać że "coś" musimy zwrócić. Co więcej, wołając taką metodę od razu widzisz ze ona mogła nie zadziałałać (bo zwraca Either) i może warto coś z tym zrobić. Jak masz metodę która gdzieśtam na dole moze walnąć RuntimeException to wołając ją nie masz pojęcia że coś takiego może się stać, chyba że przekopiesz się przez cały kod. Bardzo łatwo coś takiego przeoczyć i łyknąć wyjątek jak wątek umrze.

Poza tym, jeśli jakiś caller sobie złapie ten wyjątek i nie puści go dalej, to znaczy że obsłużył już błąd i nie powinien być propagowany dalej, więc nie wiem czy to tak znowu źle.

To zależy. Jestem w stanie wyobrazić sobie catch(RuntimeException e) nad jakimś kawałkiem kodu, żeby sobie go zalogować nie wiedząc że ktoś jednak jeden z tych wyjątków chciał obsłużyć w specjalny sposób.

0
Shalom napisał(a):
TomRiddle napisał(a):

to by znaczyło że praktycznie każda funkcja u mnie musiałaby zwracać Either

Często właśnie tak jest! Wszystko co może się "nie udać" zwraca Either czy inny Optional. Niczym się to specjalnie nie rózni od jakiegoś throws XYZException. Najgorsze co można zrobić to rzucanie RuntimeException z takiego miejsca, bo RuntimeException jest podobny tutaj do nulla -> nie widać go! Optinal czy Either czy nawet ten throws... sygnalizują na poziomie typesystemu że coś się może nie udać.

No właśnie zacząłem zauważać, że to co opisujesz jest bardzo podobne do checked-wyjątków.

TomRiddle napisał(a):

i dać możliwość callerowi zareagować na to.

Jak dla mnie to jest idealny przykład na użycie Eithera skoro chcesz żeby ktoś kto zawołał daną metodę mógł zareagować na jakiegoś Lefta.

No dobra, ale czy to znaczy że polimorifczny AbcResult w Either<>, byłby tożsamy z AbcException który jest checked wyjątkiem? Jak dla mnie checked exception spoko w tym case'ie, jeśli miałbym wybrać.

No, niby tak, ale ten sam problem jest z Eitherami, ktoś go może przechwycić i zwrócić inny either.

Nie zgodzę się, bo sygnatura funkcji się nagle nie będzie pasować :) Rzucenie lub nie jakiegoś RuntimeException jest niewidzialne. Ktoś go złapie i nawet nie będzie wiedział że ty masz gdzieś wyżej jakiś handler który chciał to złapać. W przypadku Eithera od razu będzie widać że "coś" musimy zwrócić. Co więcej, wołając taką metodę od razu widzisz ze ona mogła nie zadziałałać (bo zwraca Either) i może warto coś z tym zrobić. Jak masz metodę która gdzieśtam na dole moze walnąć RuntimeException to wołając ją nie masz pojęcia że coś takiego może się stać, chyba że przekopiesz się przez cały kod. Bardzo łatwo coś takiego przeoczyć i łyknąć wyjątek jak wątek umrze.

Tzn, w przypadku w którym ktoś połyka wyjątek to tak, wiadomo.

Ale mówiłeś że wyjątek ktoś może złapać i rzucić inny. Ja tylko mówię, że z Eitherami jest podobnie, ktoś może zjeść Either i zwrócić inny.

PS: Zgadzam się żę połykanie wyjątków to częstszy przypadek, ale raczej w moim projekcie nie pozwalam tak robić, więc Ctrl+F, "catch (RuntimeException) {}" returns 0 matches.

Poza tym, jeśli jakiś caller sobie złapie ten wyjątek i nie puści go dalej, to znaczy że obsłużył już błąd i nie powinien być propagowany dalej, więc nie wiem czy to tak znowu źle.

To zależy. Jestem w stanie wyobrazić sobie catch(RuntimeException e) nad jakimś kawałkiem kodu, żeby sobie go zalogować nie wiedząc że ktoś jednak jeden z tych wyjątków chciał obsłużyć w specjalny sposób.

No wiadomo.

Ale, jak już mówiłem - dla mnie takie wyciszenie wyjątku to jest bug w domenie biznesowej. Domena miała rzucić wyjątek a nie rzuca, ergo bug, ergo unit testy powinny mi to złapać. Także nie boję się specjalnie tego.

Poza tym, jeszcze jest inna sprawa. Jeśli domena biznesowa wystawia jakiś jeden interfejs (jedną fasadę lub jeden entry point), to pod spodem może mieć znacznie bardziej pokaszanioną logikę. I teraz, to że interfejs wystawia Eithera albo wyjątek, nie znaczy że implementacja musi polegać na wyjątkach/eitherze. Przecież, dopóki te same testy jednostkowe przechodzą, to raz przyklepany interfejs (wyjątki lub either) jest stały, ale implementacja, nie ważne jaka, powinna móc się zmieniać do woli. Więc trzeba by się zastanowić, które z tych podejść jest okej:

  • Either<> zarówno w interfejsie jak i w bebecach
  • Wyjątek zarówno w interfejsie jak i bebechach
  • Implementacja na wyjątkach, interfejs łapie i zwraca Either z odpowiednim Result
  • Implementacja na Either<>, ale rzucany wyjątek na samym końcu.

I od razu uprzedzam - jeśli ktoś chce automatycznie wybrać jedną z tych opcji, i odrzucić 3 pozostałe bez sensownych argumentów i przemyśleń, to może w ogóle nie pisać. Ja się spodziewam przemyślanych odpowiedzi, kompanów do debaty, rozumnych rozważań, a nie powtarzania schematów.

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