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

Vavrowy Either i różne typy błędów
CountZero
  • Rejestracja:ponad 7 lat
  • Ostatnio:10 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:prawie 20 lat
  • Ostatnio:8 minut
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:ponad 7 lat
  • Ostatnio:10 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:6 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:6 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:ponad 7 lat
  • Ostatnio:10 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:6 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:6 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:ponad 7 lat
  • Ostatnio:10 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.

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)