Vavrowy Either i różne typy błędów

Vavrowy Either i różne typy błędów
CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

Mam metodę ulepszającą budynek i chciałbym by wyglądała w następujący sposób -

Kopiuj
    public Either<? extends CityException, BuildingUpgradeRes> scheduleSawMillUpgrade(long cityId) {
        return getCity(cityId)
            .flatMap(pair -> {
                City city = pair.getFirst();
                CityEntity cityEntity = pair.getSecond();
                return city
                    .upgradeResourceBuilding(city.getBuildings().getSawMill())
                    .map(result -> {
                        /** kod **/ 
                        return new BuildingUpgradeRes(result._1, executionTime.toLocalDateTime());
                    });
            });
    }

Założenie jest takie, że metoda jest używana z kontrolera i nie powinna być używana nigdzie indziej, odbiorca na froncie powinien zadecydować co zrobić z konkretnym typem błędu, a możliwe są dwa - CityNotFoundException lub NotEnoughResourcesException (wszystkie dziedziczą po CityException).

Metoda w serwisie budująca klasę domenową City ma sygnaturę

Kopiuj
    Either<CityNotFoundException, Pair<City, CityEntity>> getCity(long id) {}

A metoda w klasie domenowej ulepszająca budynek ma taką:

Kopiuj
    public <T extends CityInfrastructure<T> & ResourceInfrastructure> Either<NotEnoughResourcesException, Tuple2<ResourcesValue, T>> upgradeResourceBuilding(T building) {}

No i tak - to się nie kompiluje, rozumiem że to przez to jak działa .flatMap(). Mogę oczywiście pozmieniać by sygnatury zwracały CityException zamiast konkretnego wyjątku. Ale nie chcę tego robić z oczywistego względu - stracę istotną informację jaki konkretnie wyjątek zwraca dana metoda.

Mogę zrobić coś w stylu,

Kopiuj
    public Either<Either<CityNotFoundException, NotEnoughResourcesException>, BuildingUpgradeRes> scheduleSawMillUpgrade(long cityId) { }

ale to też mi się nie podoba. Wydaje mi się że Either jest źle używany no i co jeśli będzie jakiś trzeci możliwy wyjątek?
Więc pytanie - co robić w takiej sytuacji, jakie jest funkcyjne podejście do tego typu błędów?

edytowany 1x, ostatnio: CountZero
Zobacz pozostały 1 komentarz
CountZero
Przyjąłem, że Either używam gdy błąd wynika z logiki biznesowej, a Try odpowiada klasycznym błędom typu FileNotFoundException czy ArithmeticException.
Lukasz_
Brzmi trochę niedeterministycznie, ale może jest to jakieś rozwiązanie.
CountZero
Owszem, Try by tutaj coś zmienił?
Lukasz_
Nie wiem czy Try, ale zdecydowanie się na jedno rozwiązanie byłoby dość rozsądne. Według mnie powinien to być Try, jako że to tego był projektowany. Ale ekspertem od Vavra nie jestem, a funkcjonalnie i Either się sprawdzi. Pewnie @Wibowit byłby w stanie powiedzieć coś więcej.
Wibowit
Eithera z Exceptionem rzadko widuję, zwłaszcza jeśli chodzi o przekazywanie do metody czy zwracanie z metody.
Wibowit
  • Rejestracja:około 20 lat
  • Ostatnio:około 3 godziny
4

To jest jeden z przypadków, który obnaża największą bolączkę Javowych genericsów (i nie jest to bynajmniej reification) czyli skomplikowane sygnatury związane z brakiem obsługi declaration site variance (dostępnej w Scali, C#, Kotlinie, etc). W Javie mamy tylko use site variance i wynikające z tego szaleństwo pytajników, extendsów i superów. Prawdopodobnie gdyby tego szaleństwa trochę dorzucić i przerobić:
<U> Either<L,U> flatMap(Function<? super R,? extends Either<L,? extends U>> mapper)
na:
<E super L, U> Either<E,U> flatMap(Function<? super R,? extends Either<? extends E,? extends U>> mapper)
to może wtedy flatMap zadziałałby od kopa tak jak być chciał. Tymczasem do twojej dyspozycji jest metoda Either.narrow i za jej pomocą możesz zrobić rzutowanko, które cię interesuje, czyli:

Kopiuj
Either<Podklasa, Coś> eitherNiezrzutowany = ???;
Either<Nadklasa, Coś> eitherZrzutowany = Either.narrow(eitherNiezrzutowany);

Poza tym, moim zdaniem, konstrukcja Either<? extends CityException, BuildingUpgradeRes> nie ma sensu. Zamiast tego użyj Either<CityException, BuildingUpgradeRes>. ? extends CityException (zamiast samego CityException) to może by się przydało w typie argumentu jakiejś metody, ale pakowanie tego bez powodu do typu zwracanego nie przynosi korzyści.

PS: Mnóstwo innych klas z Vavra ma metodę narrow po to, by ręcznie poradzić sobie z wariancją tam gdzie sygnatury metod nie są wystarczająco szalone.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 4x, ostatnio: Wibowit
CountZero
Dzięki, popatrzę na to jutro. Co do wildcarda w zwracanym typie - racja, rzeczywiście to nie ma sensu. A myślałem że ma.
CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

No więc działa,

Kopiuj
    public Either<CityException, BuildingUpgradeRes> scheduleSawMillUpgrade(long cityId) {
        return Either
            .<CityException, Pair<City, CityEntity>>narrow(this.getCity(cityId))
            .flatMap(pair -> {
                City city = pair.getFirst();
                CityEntity cityEntity = pair.getSecond();
                return Either.narrow(city
                    .upgradeResourceBuilding(city.getBuildings().getSawMill())
                    .map(result -> {
                        /**Kod **/
                    }));
            });
    }

ale niestety jakoś szczególnie pięknie to nie wygląda. Dzięki za pomoc.

Bambo
tego flatMapa możesz opakować w osobną metodkę, będzie wyglądać trochu lepiej - sam miewam z tym problem
CountZero
W sumie dobry pomysł, zastanawia mnie czemu to nie jest lepiej zrobione
Bambo
w Kotlinie trochę lepiej ... w sumie ja w priv projektach nie piszę już w Javie. A pamiętam, że miałem dużo gorsze przypadki niż Twój i wyglądało to jak ..
CountZero
Kotlin super, ale wciąż nie mogę się przekonać. Chociaż im bardziej skomplikowane rzeczy piszę w Javie tym bardziej zaczyna irytować, przynajmniej ósemka
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:8 miesięcy
  • Postów:779
3

Tu mam jakiś przykład z mocniej skomplikowanym przykładem:

Kopiuj
public class IncomeCalculatorFacade {

    private final CountryFinancialDataProvider factoryProvider;
    private final ExchangeRateService exchangeRateService;
    private final OfferDataFormValidator validator;

    public IncomeCalculatorFacade(final CountryFinancialDataProvider factoryProvider, final ExchangeRateService exchangeRateService, final OfferDataFormValidator validator) {
        this.factoryProvider = factoryProvider;
        this.exchangeRateService = exchangeRateService;
        this.validator = validator;
    }

    public Either<AppError, BigDecimal> calculateMonthlyIncomeNetInPLN(final OfferDataDto form) {

        return validator.getValidatedForm(form)
                .flatMap(validForm -> DailyIncomeGross.tryCreate(form.getDailyRateGross())
                        .flatMap(dailyIncomeGross -> tryCalculateForCountry(dailyIncomeGross, form.getCountry().toUpperCase())));

    }

    private Either<AppError, BigDecimal> tryCalculateForCountry(final DailyIncomeGross dailyIncomeGross, final String countryStr) {
        return tryGetCountryFromStr(countryStr)
                .flatMap(country -> tryGetFactoryAndCalculate(dailyIncomeGross, country));
    }

    private Either<AppError, Country> tryGetCountryFromStr(final String country) {
        return Try.of(() -> Country.valueOf(country))
                .toEither(new AppError(ErrorReason.COUNTRY_NOT_SUPPORTED, country));
    }

    private Either<AppError, BigDecimal> tryGetFactoryAndCalculate(final DailyIncomeGross dailyIncomeGross, final Country country) {
        return factoryProvider.provide(country)
                .map(factory -> calculateByCountryTaxData(dailyIncomeGross, factory))
                .toEither(new AppError(ErrorReason.COUNTRY_NOT_SUPPORTED, country.name()));
    }

    private BigDecimal calculateByCountryTaxData(final DailyIncomeGross dailyIncomeGross, final CountryFinancialData countryFinancialData) {
        final Currency currency = countryFinancialData.getCurrency();
        final BigDecimal beforeExchange = dailyIncomeGross.calculateMonthlyIncomeNet(countryFinancialData.getSimpleTaxPolicy());

        if(isOtherCurrency(currency)) {
            return beforeExchange.multiply(exchangeRateService.getRate(currency)).setScale(2, RoundingMode.HALF_UP);
        }

        return beforeExchange;
    }

    private boolean isOtherCurrency(final Currency currency) {
        return currency != exchangeRateService.referenceCurrency();
    }

}
edytowany 1x, ostatnio: Bambo
Zobacz pozostały 1 komentarz
CountZero
Fajny, szkoda że nie dałeś kolorowania składni xd. Ja sam do teraz się zastanawiam czy warto jest używać vavra w javie.
Bambo
@CountZero: proszę Cię bardzo :D .. btw robiłem to jako projekt na rekrutację ostatnio :D Swoją drogą lubię zawsze z tech leadami na rozmowach rozkminiać wyjątki vs propagowanie błędów na krańce systemu w postaci np enumów/klas błędu jak tu.
CountZero
To teraz dam z czystym sumieniem plusa :D. I jak na takie pomysły reagują na rekrutacji? U mnie w robocie vavr to raczej tylko taka bardzo ekscentryczna ciekawostka
Bambo
Podziękował :) Wiesz co, niektórzy łapią wtf'a, że jak to można tak nie rzucać wyjątkami i 1 raz spotykają się z takim podejściem i mówią, że najłatwiej po prostu przechwycić wszystkie wyjątki w jednym miejscu i argument, że to jest przecież stosowanie "goto" na nich nie działa. Druga część mówi, że ogólnie spoko, ale to koszt kazać wszystkim z projektu ogarnąć takie podejście i to utrzymać, więc zostaje standard :D
CountZero
No czyli tak jak myślałem, u mnie to samo... ale przynajmniej można sobie samemu tak popisać xd
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:8 miesięcy
  • Postów:779
0

@CountZero:

A tak z ciekawości .. piszesz coś na wzór Ogama ? Bo ja też przymierzałem się jakiś czas temu i miałem grube rozkminy.

CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

Tak, piszę, chociaż jest to bardziej podobne do Plemion. Obecnie jest jakieś kilkanaście k linijek kodu z generatora + kilka k kodu napisanego przeze mnie. Też właśnie coś kojarzę że pisałeś podobny projekt, kontynuujesz go? Jeśli chciałbyś zobaczyć jak to u mnie wygląda to chętnie Ci wyślę na pm. link do Gitlaba.

edytowany 1x, ostatnio: CountZero
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:8 miesięcy
  • Postów:779
0

@CountZero: no jasne, chętnie zobaczę. Tak, ja pisałem, bo chciałem w praktyce sprawdzić takie solidne DDD w starciu z mniej trywialną domeną + wrzuć kotlina, webfluxa i może jooq i powiem Ci, że już przy rozkminianiu modułów i co z czym gada zaczęły się jazdy -> wg zasady DDD możesz w 1 transakcji zapisywać 1 agregat. No i pozdro :D Podobnie sprawa aktualizacji surowców. Nawet zatrudniłem @hcubyc i też rozkminiał XD

Teraz ze znajomymi kończę 2 apki biznesowe + dostałem od graficzki mojej grafiki i piszę grę w Unity, więc jak to skończę to planuje wrócić i może przez ten czas gdzieś na posiedzeniach rannych w wc uda mi się coś sensowniejszego z tym Ogamem w połączeniu z DDD wymyślic, bo założyłem, że chcę to zrobić wg zasad.

edytowany 2x, ostatnio: Bambo
CountZero
Jak zaczynałem projekt to też byłem zafiksowany na punkcie DDD - teraz już mniej zwracam uwagę na jego zasady a bardziej na bardziej ogólną "architekturę heksagonalną" i po prostu OOP. Inaczej bym oszalał chyba :/
hcubyc
No generalnie pewne zasady trzeba łamać lub naginać, jeżeli świadomie to spoko, bo ciężko po bożemu każdy problem rozwiązać ;/
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:8 miesięcy
  • Postów:779
0

@CountZero: @hcubyc

Wiecie co, trochę pisałem priv z Jakubem, gwałciłem talki Sobótki o DDD i dla mnie bardzo enigmatyczne jest to, żeby niekoniecznie dzielić moduły tak jak są rzeczowniki, czyli w tym przypadku Baza Militarna, Budynek, Statek, Gracz, Technologia i co tam dalej. Przecież to się wydaje takie logiczne XD. Starałem się zatem podejść do tego czasownikami, tak jak mówi Sobótka i pamiętasz @hcubyc - chciałem za wszelką cenę wprowdzić agregat Upgrading który ma info o jakimś ulepszaniu czegoś - no ale to prowadzi zaraz do ES.

Największy problem jaki napotkałem to jak naliczać surowce, żeby stan był spójny. No bo zakładamy, że do naliczania surowców żaden job nie chodzi (chyba, że to dobry pomysł). Zatem stan trzeba updatować "lazy". No i mamy sytuacje, że ktoś Cię najeżdża, wygrał z Tobą bitkę i masz taką logikę, że Ci wtedy kradnie 75% metalu z całości. Trzeba ten metal policzyć -> zatem do Twojej ilości metalu z ostaniego updata trzeba doliczyć to ile Ci wyprodukowała jakaś rafineria czy kopalnia czy co tam masz. No ok, niby spoko, a co jeśli ostatni update metalu miałeś 2h temu, bo załóżmy, że się uja działo, a przez ten czas, dla uproszczenia 1h temu kończyła Ci się upgradować kopalnia metalu ? Nie policzysz stanu wg wzoru: staryStan + 2h * wspolczynnik, bo zakłamiesz :D No są corner casy ...

edytowany 1x, ostatnio: Bambo
CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

Na razie dzielę funkcjonalność w większości po "rzeczownikach" i jakoś to działa. A taki case "Upgrading" rozwiązałbym po prostu stworzeniem komendy "Upgrade" który przechowuje ten rozkaz. U Ciebie to się sprawdza? Wydaje się to być mega ciężkie, zwłaszcza jeśli jest bardzo dużo funkcjonalności.

Co do Twojego przykładu - ja planuje to rozwiązać w taki sposób, że po prostu po upgradzie budynku jest aktualizowany stan surowców, zapisywana data upgradu no i potem wszystko liczone jest już z nowym współczynnikiem. Jest to jedna operacja więcej (aktualizacja surowców po upgradzie), no ale dzięki temu nie ma kombinowania.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.