Problem z Mono i Either

0

Cześć, mam takie metody w repozytorium:

Mono<Entity> findById(long id);

Mono<Entity> save(Entity entity);

a pewna z metod encji ma taką sygnaturkę:

Either<Error, Entity> businessMethod();

Chciałbym w serwisie pobrać z repo encję, wykonać metodę biznesową, zapisać i zwrócić id zapisanej encji czyli de facto Mono<Either<AppError, Long>>.

W kodzie imperatywnym jest to proste:

Entity e = findById(id);
e.businessMethod();
repo.save(e).getId();

A z mono i eitherami robi się hardkowoiej. Mam coś takiego w kotlinie:

 repo
                    .loadById(id)
                    .map {
                        it.tryBusinessMethod()
                    }
                    .flatMap { 
                        it.flatMap { 
                            repo.save(it)
                        }
                        .map{ it.id }
                    }

Ale ni chu nie mogę dojść .. to nie działą

3

Taki potworek:

 val x: Mono<Either<Error, Long>> = repo
                .loadById(id)
                .map {
                    it.businessMethod()
                }
                .flatMap { result ->
                    result.map {
                        repo.save(it)
                                .map {
                                    result.map { it.getId() }
                                }
                    }.mapLeft { Mono.just(Either.left<Error, Long>(it)) }
                    .getOrElseGet(java.util.function.Function.identity())
                }

Działa. Wygląda tragicznie....

.mapLeft { Mono.just(Either.left<Error, Long>(it)) }
                    .getOrElseGet(java.util.function.Function.identity())

można wydzielić do osobnej funkcji this::mergeError.

0

Dzięki, mistrz @jarekr000000

Ale no właśnie, wygląda jak g .. niby to funkcyjne miało takie zwięzłe być :/

0
jarekr000000 napisał(a):

Taki potworek:

 val x: Mono<Either<Error, Long>> = repo
                .loadById(id)
                .map {
                    it.businessMethod()
                }
                .flatMap { result ->
                    result.map {
                        repo.save(it)
                                .map {
                                    result.map { it.getId() }
                                }
                    }.mapLeft { Mono.just(Either.left<Error, Long>(it)) }
                    .getOrElseGet(java.util.function.Function.identity())
                }

Działa. Wygląda tragicznie....

.mapLeft { Mono.just(Either.left<Error, Long>(it)) }
                    .getOrElseGet(java.util.function.Function.identity())

można wydzielić do osobnej funkcji this::mergeError.

Odkopię trochę temat, napisałem sobie taki projekt, i wszyło że prawie wszystko zwraca Mono<Either<Error, Something>>
i teraz zaczyna się problem bo moje businessMethod() też zwraca Mono<Either<Error, Something>> i po jakiś mapowaniach maksymalnie powstaje Mono<Either<Error, Mono<Something>>>, obecnie radzę sobie z tym tak, że robię flatmap i wstawam coś takiego:

httpService.someMethod().flatmap( result ->
          if (result.isRight()) {
            return otherService.businessMethod()
          } else {
            return Mono.just(Either.left(result.getLeft()));
          }
);

ale tego staje się za dużo i nie wiem jak sobie z tym radzić??

mam przygotowaną klasę która Either<Error, Something> oraz Mono<Either<Error, Something>>> na końcu zamienia mi na jakiś response HTTP,
server to Netty więc .block()/.blockOptional() trochę odpada

3

Mieszanie Mono z Either to jakieś nieporozumienie. Po co Ci to Mono? Dlaczego to robisz, co Ci to daje? Jeśli już używasz programowania reaktywnego, to dlaczego Mono.error jest złe?

Czekam jeszcze na mix np. z Arrow, w końcu ktoś wyskoczy z Mono<IO<Either<...>>> ;)

1
Charles_Ray napisał(a):

Mieszanie Mono z Either to jakieś nieporozumienie. Po co Ci to Mono? Dlaczego to robisz, co Ci to daje? Jeśli już używasz programowania reaktywnego, to dlaczego Mono.error jest złe?

Nie ma w tym nic dziwnego. Mono<Either> jest normalne

  1. Mono.error jest słabe - bo error w Mono pochodzi z Throwable. Nie każdy chciałby mieć swoje errory jako throwable.

  2. Nie zawsze zresztą Either.left oznacza error, a w szczególności nie krytyczny i być może nie chcemy, żeby strumień kończył się na obsłudze Exceptiona.
    Możemy chcieć normalnie takie elementy przetwarzać. Choć ten problem bardziej dotyczy Flux.

Edit:
A takia funkcja rozwiąże problem Mono<Either<Error, Mono<Something>>>

 fun <L,R> Mono<Either<L, Mono<R>>>.flatMapRight() = this.flatMap {
    it.mapLeft { Mono.just(Either.left<L, R>(it)) }
            .map {
                it.map {Either.right<L, R>(it)}
            }.getOrElseGet {it }
}

0

Ad. 1 Takie jest API (nie wiem dlaczego nie Exception), co nie oznacza, że trzeba tam pchać Errory. Nie wydaje mi się również, że jakakolwiek poważna libka wepchnie tam np. OOME.
Ad. 2 To wydaje mi się akurat do zamodelowania używając po prostu operatorów onErrorResume(...) - także jest na to „idiomatyczne” rozwiązanie w samym Rx.

Dalej będę twierdził, że mieszanie Rx z Either prowadzi do niepotrzebnego zaciemnienia kodu, przez co utrzymanie czegoś takiego będzie bardziej czasochłonne. Już sam Rx jest ciężki. Chyba, że onanizm technologiczny, z tym nie będę się spierał ;)

2

ad 1. Nie chodzi o OOME czy inne tego typu. Może chcesz tam wrzucić Stringa, albo Enuma, albo (zwykle) całe ADT opisujące błedy. I w szczególności nie chcesz tam na pewno pchać stacktrace.
Czasem błąd biznesowy potrzebuje więcej opisu - np. w dokumencie takim i takim, dla takiego usera, a taki był status... -można te informacje wpakować we własnego Exceptiona ... tylko potem trzeba gdzieś pracowicie wypakować przez instanceof.. i zwymiotować patrząc jak za każdym razem do czegoś takiego dokleja się stackTrace... na którym nam zupełnie nie zależy.

ad 2. Mam w kodzie i Eithery i on errorResume. I całkiem dobrze mi się żyje - Either.left opisuje błędy biznesowe (dokument miał np. niepoprawne dane), Exceptiony i resume są na łapanie problemów bardziej low level (takich naprawdę nieprzewidzianych - typu zerwało mi się połączenie sieciowe.. i może zechcę powtórzyć (a może nie) ).
Po prostu potraktuj ten Either jak normalną klasę -- tak jakbyś miał tam coś w stylu class Moja (val status:Status, val result:RealResult) tyle, ze Either opisuje akurat taką parę generycznie.
Ogólnie: nie każdy błąd to Exception (czy tam Throwable).

0

Właśnie chciałem zbudować coś na wzór tego iż Error zawiera nie tylko co się wywaliło, ale też przy jakich danych, dobrą uwagą jakiej nie zastosowałem to rozdzielenie błędów biznesowych i wpakowałem wszystko w Either, możliwe że zamiana niektórych błędów na Mono.error() możę odchudzi niektóre zwracane typy, prawdopodobnie sam case projektu też nie pasuje do wykorzystania Mono czy Flux razem z Either jako że zadanie ma bardziej nastawione na komunikację

0

Ok, ma to jak najbardziej sens - wzięcie co najlepsze z obu bibliotek. Szkoda, że wygląda to paskudnie w kodzie. Odnośnie tego nieszczęsnego Throwabla, którego nie mam zamiaru bronić, można by się pokusić o własna klasę wyjątku i nadpisać fillInStacktrace na pustą implementację, jeśli to bardzo boli. Trochę rzeźba jednak, mi nigdy nie przeszkadzały te stacktracy.

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