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

0

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

    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ę

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

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

    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,

    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?

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:

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.

0

No więc działa,

    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.

3

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

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

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

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.

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.

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 ...

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.

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