danek

  1. Poznań
danek
2019-07-10 12:43

Nie wiem czy to jest jakiś duży problem, ale mam wrażenie, że ludzie pisząc testy, zapominają o tym, że można wydzielać metody. O co mi chodzi?
Prosty przykład:
Fragment testu sprawdzający czy po zaplanowaniu pobraniu pewnych danych, dane faktyczie są pobrane. Takich testów jest kilka

    system.setExternalSourceMatchList(List.of(new MatchInfo("host", "guest", 1, false, MatchResult.NOT_SET, system.now()))); 
    system.advanceTimeBy(3, TimeUnit.HOURS);
    system.setExternalSourceMatchList(List.of(new MatchInfo("host", "guest", 1, true, MatchResult.DRAW, matchStartDate)));
    system.advanceTimeBy(10, TimeUnit.MINUTES);

Zamiast tego, przygotowując sobie dwie proste metody (aby można było użyć tego w kilku testach)

    private MatchInfo planned() {
        matchStartDate = system.now();
        return new MatchInfo("host", "guest", 1, false, MatchResult.NOT_SET, matchStartDate);
    }

    private MatchInfo finished() {
        return new MatchInfo("host", "guest", 1, true, MatchResult.DRAW, matchStartDate);
    }

Całośc dla osoby czytającej całość po raz pierwszy wygląda prościej

    system.setExternalSourceMatchList(List.of(planned()));
    system.advanceTimeBy(3, TimeUnit.HOURS);
    system.setExternalSourceMatchList(List.of(finished()));
    system.advanceTimeBy(10, TimeUnit.MINUTES);

Jeśli dane wejściowe są bardziej złożone, to można iść nawet krok dalej i przygotować sobie specjalne metody pod budowanie odpowiednich danych.
Mieliśmy raz projekt na zaliczenie który analizował scenariusze. Scenariusz składał się z pewnych metadanych i opisu kolejnych kroków, gdzie krok mógł mieć podkroki. Stworzyliśmy buildera i dzięki naturalnym wcięciom używamym w javie już sam kod odzwierciedlał strukture danych:

    ScenarioDTO scenarioDTO = new ScenarioBuilder()
                .addSteps(
                        step("",
                                step(""),
                                step(""),
                                step("",
                                        step(""))),
                        step("")
                )
                .build();

Dzięki temu nie trzeba się zagłębiać co jest podkrokiem czego, bo naturalnie było widać.

Więc wydaje mi się, że warto czasem trochę dopracować kod testów, żeby w przyszłości łatwiej je było czytać

xfin

A po co komu czytać testy - widzisz zielone to jest dobrze, widzisz czerwone to jest źle :D

tdudzik

Do poprzedniego projektu udało mi sie wprowadzic takie udogodnienia, mam na myśli buildery właśnie. Ktoś nieznający domeny sporo mógł się dowiedzieć z samych testów. W tym temacie polecam "Growing Object Oriented Software Guided by Tests" :)

kelog

Niestety, w wielu projektach nie inwestuje się czasu w jakieś utile generujące gotowe dane. Zamiast tego mamy ścianę new set set set set new set set set. Zwykle skopiowaną kilka razy :P

Potat0x

A gdy potrzebne są przykłady poprawnych DTO nie tylko w testach, ale też w produkcyjnej części kodu - jak ładnie do tego podejść? Stworzyć osobną klasę ValidMatchInfoDtoGenerator? Dodać statyczną metodę zwracającą przykładowe DTO do MatchInfoBuildera? Od pierwszego sposobu odrzuca mnie namnożenie klas, a od drugiego łamanie SRP i zaśmiecanie buildera.

danek

W testach podaje zawsze poprawnego DTOsa, chyba, że testuje gdzieś walidacje. Co do samego sprawdzania danych wejściowych to jak najbardziej mam osobne klasy od tego. Mnożenie klas to nic złego ;)

Potat0x

Jest metoda zwracająca RequestDtoBuilder w pełni zainicjalizowany zahardkodowanymi, poprawnymi wartościami. Wystarczy .build() i mamy prawidłowy RequestDto. Potrzebuję tego w testach - do łatwiejszego testowania walidatorów i kontrolerów, i na produkcji - żeby dorzucić przykład poprawnego RequestDto do odpowiedzi 422. Zastanawiam się gdzie umieścić taką metodę :)

tdudzik

Zduplikować. A little copying is better than a little dependency. :)

Potat0x

Takiego rozwiązania się nie spodziewałem :P

tdudzik

Czasami najprostsze rozwiązania są najlepsze. :)

no_solution_found

problemem jest wg mnie to, że testy traktuje się jako "gorszą" część projektu, gdzie wzorce projektowe i clean code już tak potrzebny nie jest.

Kermii

Dla mnie najlepiej tworzyć dane przez klasy w testach odpowiadające tym używanych w implementacji w połączeniu z generic builderem - https://howtocodetutorial.wor[...]ic-builder-pattern-in-java-8/

I możemy już dowolnie zbudować obiekt z nazwą wyszczególnioną w nazwie metody statycznej, przykład
TestObjects.anObjectWithInvalidId()

nohtyp

@danek Nie wiem czy to jest jakiś duży problem, ale mam wrażenie, że ludzie pisząc testy, zapominają o tym, że można wydzielać metody. O co mi chodzi? Nie zapominają, często jest tak, że refaktor testów leci na etapie utrzymania, bo do tego czasu jeszcze wiele rzeczy może się zmienić. Poza tym z doświadczeniem człowiek mniej czasu chce poświęcać na tworzenie złożonych testów.

danek

@nohtyp: ale nie chodzi mi o złożone testy, tylko o czytelne

nohtyp

Jeśli chcesz mieć czytelne to pisz dobry kod, w twoim przypadku klasa MatchInfo to problem, ponieważ ma rozbudowaną liczbę parametrów wejściowych. Czy jesteś pewien, że na tej liście czegoś nie zapomniałeś? Ogólnie słabo czyta się kod klas, które przyjmują nie wiadomo co i poprawa tej rzeczy nie tylko wpływa na czytelność testów, ale i kodu produkcyjnego. Zamiast klepać testy bezmyślnie, pomyśl czy rzeczywiście robienie opakowań dla takich wywołań ma sens.

danek

To jest json serializowany do DTOsa, normalne IDE podpowiada co jest czym i nie da się zapomnieć ;) Fakt, niefortunny przypadek, ale nie to jest główną myślą

nohtyp

W takim razie słabe to jest, a przykład jest spoko bo pokazuje, brak myślenia przy promowaniu kolorowych technik. https://stackoverflow.com/a/7962503

danek

Tylko jak rozbijesz to na kilka klas to nadal problem pozostanie ten sam. Chodzi mi o to, żeby w metodzie klasy unikać zbędnego tworzenia bardziej złożonych danych wejściowych, żeby nie zaburzały czytania. Co do samej klasy, to tak, nie jest idealna, ale lenistwo wygrało z ortodoksyjnością ;) Klasa jest tworzona wprost tylko gdzies w jakims smutnym mapperze, a potem unika się zbędnego getX().getY().getZ();

danek

Aczkolwiek, dobrym tematem do przemyślenia jest, na ile 'ulepszać' DTOsy, żeby tworzenie ich było 'ładniejsze'. Czy warto? Na ile warto?

danek
2019-06-25 19:01

Ciekawa strona. Pozwala coś narysować i stara się dopasować do tego symbol z unicode.
http://shapecatcher.com/

jarekr000000

Kujowa. Narysowałem coś i mi dopasował - w kolejności: Latin small letter ae with macron: ǣ, Bell: 🔔, Splashing sweat symbol: 💦, Registered sign: ®, Open mailbox with lowered flag: 📭, Person with folded hands: 🙏 - wszystko do .... niepodobne (no może ostatnie trochę).

danek

ano, też mi jakieś niepodobne dało. Pokazało Tifinagh letter yaa: ⵄ, Georgian letter rae: რ, Cherokee letter ne: Ꮑ, Cyrillic small letter de: д.

krwq

mi sensownie zadziałało ale nie szukałem niczego konkretnego (wynik pasował z rysunkiem)

azalut

fajnie fajnie ale dopasowanie troche kuleje chyba, bo mi nie rozpoznał nic, nawet chleba :o

Aryman1983

Buginese letter va: ᨓ :-)

jarekr000000

@Mjuzik: brawo. U Ciebie rozpoznał nawet nieźle.

krwq

@Mjuzik: mi nie rozpoznał ale też nie miałem oczekiwań :D

danek
2019-06-03 11:44

No i w końcu się nadziałem na te legendarne pułapki springa

Krótkie tło fabularne:
Potrzebuje w swojej aplikacji na starcie całości odczytać z bazy daty rozpoczęcia wydarzeń, aby wiedzieć o której zacząć próbować pobierać ich wynik. Jako, że kiedyś byłem leniwy niektóre moduły korzystają ciągle z spring data zamiast JOOQ i tak było też w tym przypadku.

Godziny wydarzeń (meczy) są zamieniane na odpowiednie opóźnienie względem teraz i lądują w ScheduledExecutorService. Samo pobranie wyniku jest trochę bardziej złożoną operacją, ponieważ trzeba:

  • pobrać z bazy konfigurację dla danego źródła danych (pobieram wyniki z różnych stron, każda ma swój format danych)
  • pobrać sam wynik, zapisać go
  • sprawdzić czy któryś z graczy nie powinien dostać jakichś punktów i zapisać jego nowy stan

Testy przechodzą, czas na pierwsze odpalenie całości. Startuje i... nic. Lekkie wtf, sprawdzam bazę, faktycznie nic się nie stało. Zapinam debuggera i lecę linijka po linii.
Drugie wtf. W pewnym momencie debbuger jakby sam się odpinał. Tym razem przechodzę całość wchodząc w każdą metodę po drodze: debugger znika po pobraniu przez springowe repository.
Tutaj krótkie info poboczne: mam napisany adapter do springowego repository, aby pasował do mojego interface. Metoda która pobiera jeden obiekt wygląda tak:

@Override  
public Option<D> findOne(UUID uuid) {  
     Option<E> entity = Option.of(repository.findOne(uuid));  //debugger twierdzi, że tu obiekt jest
     return entity.map(entityToDomainMapper);  //po wyjsciu z tej metody debugger znika
}

No więc patrze co siedzi w tym obiekcie entity: jest i moja encja! Wygląda ona tak:

 @NoArgsConstructor  
@AllArgsConstructor  
@Getter  
@Entity  
class LeagueDetails {  
    @Id  
    private UUID leagueUUID;  
    @OneToMany(cascade = CascadeType.ALL)  
    private List<LeagueDetailsSetting> config;  
    private String clientShortcut;  

}

No właśnie: List. Debugger zdążył mnie uprzedzić i już wyświetlał hibernatowy błąd związany z lazy loadingiem.

Podejście drugie.
Dodałem wszędzie fetch = FetchType.EAGER (nie są to duże dane, więc nie ma problemu). Odpalenie całości, patrzę w logi, selecty latają. Dla pewności jeszcze sprawdzam debuggerem, wszystko przechodzi jak powinno.

Ale

Nie bez powodu na początku wpisu zaznaczyłem zapisać. Kod może i się wykonał, ale w bazie pustka.
W tym momencie dostałem potężnymi flashbackami. Wszyscy wszędzie straszą transakcjyjnością springa, kiedy@Transactional zadziała kiedy nie, tym że save to nie zawsze zapisuje.

Prawdopodobnie po prostu nie wiem o jakiejś małej i nic nieznaczącej rzeczy, prawdopodobnie przez to, że to nie idzie przez requesta tylko jest odpalane z wnętrza. Tylko trochę nie mam czasu teraz w to wszystko wnikać (sesja :<), a i tak już przepisywałem wszystko na JOOQ.

@jarekr000000 czy jest jakiś łatwy sposób mapowania obiektów na JOOQ czy trzeba ręcznie?

#java #spring

CountZero

Nie pokazałeś gdzie robisz zapisywanie ani kiedy zaczyna się tranzakcja. Może chodzi o to, że transakcje w Springu są obsługiwane tylko na metodach publicznych wywołanych spoza klasy?

danek

@CountZero: teraz już trochę o tym doczytałem i wiem, że trzeba ręcznie te transakcje ustawiać. Nie miałem nigdzie ustawianego @Transactional ponieważ liczyłem (naiwnie, ale póki co przypadkiem to się sprawdzało), że po prostu będzie to działać (aka It just works :P ). Brakuje mi też jakiejś informacji zwrotnej, że w sumie to nic się nie stało

jarekr000000

JOOQ ma generator kodu (gradle, maven) (na podstawie bazy). Nie wiem czy o to pytasz.

danek

chodzi mi o wygenerowanie podstawowych crudowych zapytań, może być na podstawie schematu bazy

jarekr000000

@danek: ale tego normalnego generatora używałeś ? tam jest mapping tylko. Takich metod jak springdata to nie generuje.

danek

@jarekr000000: samo mapowanie raczej nie jest problemem, bardziej nużące jest pisanie insertów, selectów itp, bo trzeba wszystkie pola ręcznie, wszystkie kolumny itp

danek
2019-05-08 14:59

Jeszcze w temacie niestosowania Exception.

Tak samo jak do nieużywania wyjątków do sterowania całym flow, Either można użyć do różnego rodzaju walidacji na różnych etapach przetwarzania. Potrzebne są dwa warunki:

  • lepiej jak dane są 'opakowanie' w jeden obiekt (jakiś DTO)
  • każda metoda przyjmuje takiego DTOsa i zwraca Either<Error, NowyDTO>

Nawet jeśli chcemy wykonać samą walidację, starczy zrobić taką "przezroczystą" metodę:

    Either<BetError, NewBet> validateParameters(NewBet newBet){
        //jakas super walidacja sprawdająca co trzeba
        //jesli wszystko w porządku
        return Either.right(newBet);
    }

Co to daje?
Dzięki temu logika biznesowa jest po prostu ciągiem map()/flatMap() kolejnych kroków przetwarzania bez "skoków w bok" gdy coś w międzyczasie pójdzie nie tak (przykład niżej).

Side effects
Tak jak powinno się ich unikać, to niestety czasem trzeba się z nimi pogodzić (jakiś zapis do bazy czy coś podobnego). Zostaje wtedy albo taka jak wyżej "przezroczysta" metoda albo po prostu peek()

    Either<BetError, UUID> addBetToMatch(NewBet newBet) {
        return betValidator
            .validateParameters(newBet)
            .map(Bet::of)
            .peek(bet -> repository.save(bet.getUuid(), bet))
            .map(Bet::getUuid);
    }

Jeszcze jako bonus przy walidacji. Vavr udostępnia pattern matching, który jest takim uber switchem. Staram się do niego przekonać ale jakoś w Javie, przez brak wsparcia składni, wydaje mi się to trochę na siłę:

    Either<BetError, NewBet> validateParameters(NewBet newBet) {
        return Match(newBet).of(
            Case($(matchNotExist()), Either.left(BetError.MATCH_NOT_FOUND)),
            Case($(betWithUUIDExist()), Either.left(BetError.BET_ALREADY_SET)),
            Case($(matchHasAlreadyBegun()), Either.left(BetError.MATCH_ALREADY_STARTED)),
            Case($(), Either.right(newBet))
        );
    }

cały kod

A następnym razem o testach przy upływającym czasie :)

Poszukiwacz literówek: @Chramar

Michał Sikora

to niestety czasem trzeba się z nimi pogodzić (jakiś zapis do bazy czy coś podobnego) - zapis do bazy zawsze może zwracać obiekt, który został zapisany albo błąd zapisu, więc też można opakować w Either i korzystać z flatMap. Dla mnie efekty uboczne to są rzeczy typu logi, analityka itp. i one pasują do peek.

DisQ

@Michał Sikora: Zapis do bazy danych jest efektem ubocznym. Jest to komunikacja z światem zewnętrznym, pisząc funkcję tego typu nie jesteś w stanie zapewnić, że wywołując ją za każdym razem dostaniesz ten sam rezultat. Czyli łamiesz tutaj referential transparency. W językach innych niż Java można to ładniej modelować -> IO czy BIO

Michał Sikora

Racja, źle się wyraziłem. Chodziło mi o to, że peek nie powinen raczej służyć do zapisu w bazie danych, tylko do rzeczy, które nawet jak się nie udadzą, to nic złego się nie stanie w systemie teraz albo w przyszłości.

Akihito

Czy to nie jest czasem monada? :0

DisQ

@Akihito: Jeśli mowa o Either to tak, jest możliwość zaimplementowania go tak, aby spełniał wszystkie prawa monad (nie wiem jak to jest zrobione w Vavrze) więc jest monadą

danek
2019-04-23 13:30

Krótki suplement do poprzedniego wpisu. Jak mapuje błędy na odpowiedzi http. Trochę mniej treści, a więcej kodu.

Mam jeden interface na wszystkie błędy:

public interface ResponseError {
    String getMessage();
    int getHttpCode();
}

Przykładowe enum:

public enum UserError implements ResponseError {
    //normalnie jest ich oczywiście więcej
    EMPTY_USERNAME_OR_PASSWORD("Empty username or password", 400),
    DUPLICATED_USERNAME("Duplicated username", 400);

    private int httpCode;
    private String message;

    UserError(String message, int httpCode) {
        this.httpCode = httpCode;
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    @Override
    public int getHttpCode() {
        return httpCode;
    }
}

Narzuciłem sobie, aby wszystkie fasady zwracały:
Either<? extends ResponseError, ?>
Dzięki temu mam jeden generyczny mapper:

public final class ResponseResolver {
    private ResponseResolver() {}

    static ResponseEntity resolve(Either<? extends ResponseError, ?> input) {
        return input
                .map(ResponseEntity::ok)
                .getOrElseGet(ResponseResolver::createErrorResponse);
    }

    private static ResponseEntity createErrorResponse(ResponseError error) {
        ErrorResponse response = new ErrorResponse(error.getMessage());
        int httpCode = error.getHttpCode();
        return new ResponseEntity<>(response, HttpStatus.valueOf(httpCode));
    }
    //Bonusowo na tej samej zasadzie działa z Option (Optionalem)
    public static <T> ResponseEntity<T> resolve(Option<T> input) {
        return input
                .map(x -> new ResponseEntity<>(x, HttpStatus.OK))
                .getOrElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }
}

(Gdzie klasa ErrorResponse jest zwykłym DTO z jednym polem, żeby wynik był jsonem)

No i teraz w kontrolerze (na przykładzie springa):

    @PostMapping("login")
    public ResponseEntity generateToken(@RequestBody LoginUserInfo loginUserInfo) {
        Either<UserError, UserToken> userDetails = userFacade.login(loginUserInfo);
        return ResponseResolver.resolve(userDetails);
    }

No i właściwie to tyle ;)

Powyższe rozwiązanie ma właściwie pewną wadę: miesza warstwy. Czy jest to problem? Zazwyczaj nie, bo i tak pisze się soft dedykowany pod pracę na serwerach i nikt go w inny sposób nie odpala. Dodatkowo ta nadmiarowa informacja w postaci kodu http i wiadomości nie przeszkadza w żaden sposób w odpaleniu kodu np. z konsoli. Rozwiązaniem tego mógłby być osobny mapper w warstwie http, ale to już wchodzi trochę w sztukę dla sztuki, więc wydaje mi się, że taki kompromis pomiędzy pragmatycznością, a łamaniem abstrakcji między warstwami jest jeszcze akceptowalny.

@Desu @CountZero

#java

Desu

"wydaje mi się, że taki kompromis pomiędzy pragmatycznością, a łamaniem abstrakcji między warstwami jest jeszcze akceptowalny." - oby więcej osób miało takie podejście ;)

artur52

dopóki domena ma jednego odbiorcę dopóty ten kompromis jest ok. W przypadku różnych klientów (ui/mobile/api) ta dodatkowa warstwa może się przydać :)

danek

@artur52: jeżeli komunikacja idzie po tych samych endpointach to jest to nadal taki sam klient z perspektywy serwera

artur52

@danek: zgadzam się, ale czasem są różne endpointy a te same wejścia do domen, przynajmniej w moim przypadku

Visual Code

Fajne, myślę, że jest to bardzo przystępnie napisane, wszyscy są w stanie to zrozumieć, jeśli nawet nigdy tego nie robiła.

Desu

@danek: chętnie zobaczę więcej postów od Ciebie. Powoli konwertuję się na Javę, a mało się pisze o takich praktykach w większości tutoriali :)

danek

@Desu: idz w kotlina od razu

Desu

@danek: "Lombok - how to program in Kotlin if you boss doesn't let you" :D

CountZero

Dzięki za powiadomienie. Sposób spoko, mam podobny, ale ma jedną bardzo dużą wadę która mnie irytuje i która imo na dłuższą metę go skreśla - w kontrolerze, w sygnaturze metody generateToken, w typie zwracanym nie ma informacji co dokładnie zwracamy. Tzn. jest ResponseEntity, ale nie wiadomo co to jest, nie ma ResponseEntity<Error, String>, albo coś w tym stylu. Jak piszesz kod to pamiętasz o co chodzi, ale za trzy miesiące, gdy widzisz pięć takich metod to nie masz zielonego pojęcia co one tak naprawdę zwracają i musisz szukać odpowiedzi w kodzie na tak trywialne pytanie. I jeszcze drugi problem - brak typowania w ResponseEntity. Jak gdzieś się walniesz i zamiast UserToken zwrócisz np. UserRoles to kompilator się nawet nie zająknie i błąd wyjdzie w runtimie. O ile wyjdzie :).

danek

@CountZero: nic nie stoi na przeszkodzie by to ResponseEntity<Error, String> tam zapisać ;) Typowanie masz zapewnione na poziomie fasady, kontroler jest "głupi"/"przezroczysty" i tylko to przekazuje dalej. Dla niego nie ma znaczenia jakie dane faktycznie tam siedzą, bo jego odpowiedzialnością jest tylko wziąć jakieś informacje i wysłać je w świat po http. Własnie cała zabawa w architekturze, którą stosuje polega na tym, żeby maksymalnie "ogłupić" wszystko poza logiką biznesową i sprowadzić inne warstwy tylko do przepuszczania danych dalej. No ale jeśli chcesz zawsze możesz dopisać odpowiednie testy "integracyjne". ;)

CountZero

Ale gdzie? ResponseEntity ma tylko jeden typ generyczny. Zgadzam się, że kontroler jest głupi i nie musi o tym wiedzieć - ale programista już nie i dla niego brak informacji o tym co przekazuje metoda w kontrolerze jest problematyczne. Jak będziesz miał sto takich metod w 10 kontrolerach i będziesz musiał się domyślać co te metody zwracają, to szybko kod stanie się ciężki do utrzymania (w warstwie kontrolerów). Na dodatek nie będziesz miał pewności że tak jest, bo na etapie kompilacji oczywiste błędy typu return ResponseResolver.resolve(userFacade.register(loginUserInfo) ); // whoops, meant to invoke userFacade.login() nie zostaną wykryte. Oczywiście można zakładać że się nikt nigdy nie walnie w takiej prostej rzeczy no ale wiadomo jak to jest :).

azalut

zastanawia mnie: czy w każdym poprawnym response zwracasz 200 OK? czy w prawdziwej implementacji zwracasz tez inne kody na sukces?

Desu

@azalut: pierwsze, co mi przyszło do głowy, to: https://4programmers.net/Pastebin/11058, można to oczywiście ubrać w jakieś tam dodatkowe klasy.

danek

@azalut: nie miałem jeszcze takiej potrzeby, ale zawsze można podobną jak tu rzecz zrobić dla wartości poprawnej

DamianSn

Ktoś może powiedzieć jak to jest w praktyce ? Czy rzeczywiście porzuca się wyjątki w sterowaniu logiką na rzecz podobnych rozwiązań ? Fajnie rozwiązuje to problem częstego rzucania wyjątków, ale jednak trochę zmniejsza czytelność i komplikuje kodzik.

jarekr000000

@DamianSn: jak komplikuje? skoro uprzascza.

DamianSn

Może źle to nazwałem. Bardziej chodziło mi o to, że trzeba napisać trochę więcej kodu. Throw w metodzie i @ExceptionHandler wymaga mniej pracy, ale przerywa flow i powoduje, że stosujemy wyjątki do wcale nie takich "wyjątkowych" sytuacji. Nie miałem styczności z kodem produkcyjnym i sam się zastanawiam jak to wygląda. W tutorialach i książkach wszyscy rzucają jak leci, nawet przy walidacji i stąd się zastanawiam jak to jest :)

danek

@DamianSn: tak samo możesz używać GOTO żeby pisać mniej kodu ;) Może i kodu jest lekko więcej i lekko więcej trzeba pomyśleć pisząc, no ale w końcu za to nam płacą. A co do tutoriali to często bezmyślne są kopiowanie z innych i sami nie umieją tego uzasadnić.

DamianSn

Ja nie podważam wyższości tego rozwiązania, wręcz jestem całkowicie za nim. Z ciekawości tylko spytałem jak to jest w realiach, bo nie mam z nimi styczności. Sam ostatnio pisząc projekcik do szuflady w jednej metodzie rzucałem trzy różne wyjątki i nie spodobało mi się to kompletnie. Co do tutoriali to prawda, ale zdziwiło mnie, że wiele polecanych książek ( Spring in Action itp. ) nie wspomniało nic o alternatywach. Wyszukując w internecie też raczej pustki. Więc nie wiem czy po prostu ludzie w prawdziwych projektach rzucają na potęgę, czy ukrywają tą wiedzę przed światem :)

danek

tak Ci odpowiem: Pewna dziewczynka obserwując jak jej mama przygotowuje mięso, zapytała ją:”Mamo, dlaczego zanim włożysz mięso do brytfanny to ucinasz jego końcówki?”. „Bo moja mama tak robiła” – odpowiedziała jej matka. „Jeśli chcesz się dowiedzieć dlaczego, to zapytaj swojej babci” – dodała. Dziewczynka poszła więc do babci i zapytała: „Babciu, dlaczego zanim włożysz mięso do brytfanny to ucinasz jego końcówki?”. „Bo moja mama tak robiła” – odpowiedziała jej babcia. „Jeśli chcesz się dowiedzieć dlaczego, to zapytaj swojej prababci” – dodała. Dziewczynka poszła więc do prababci i zapytała: „Prababciu, dlaczego zanim włożysz mięso do brytfanny to ucinasz jego końcówki?”. „A bo mam za małą brytfannę” – odpowiedziała prababcia.

marlukk

danek Fajne rozwiązanie, ale mam pytanko. Co jeśli w moich treściach błędów chciałbym mieć komunikaty parametryzowane np. Nie znaleziono gracza o id=? i w miejsce ? wartość ma być nadawana dynamicznie.

danek

@marlukk: zamiast enum używaj obiektów

danek
2019-04-18 14:54

Wydawało mi się, że używanie wyjątków do sterowania flow programu jest dość powszechnie uznane za złe, jednak potem przeczytałem dość długą dyskusję o sensie używania Optionali (że nieczytelne itp). Sam jeszcze nawet juniorem nie jestem, więc do niedawna jeszcze w moim projekcie przy braku użytkownika leciał UserNotFoundException, no ale uznałem w końcu, że to strasznie głupie i tak stopniowo przebiegała przebudowa:
Pierwsze co przyszło mi do głowy to Optional (a konkretniej Option z vavr):

private Option<UserEntity> findUserById(long id) {  
    return Option.of(repository.findOne(id)); 
}

Oczywiste zalety to brak głupich wyjątków oraz nullchecków. Problem zaczyna się, kiedy jest jakiś dłuższy proces gdzie więcej niż jedna rzecz może pójść nie tak (powyżej jedyne co to użytkownika może nie być). Zostając w temacie niech będzie przykład logowania gdzie po podaniu nazwy i hasła ma zostać zwrócony token. Coś w stylu
String login(String username, String password)

Co może nie wyjść? np brak użytkownika, niepoprawne hasło. Samo Option tutaj nie pomoże bo skąd mamy potem wiedzieć co nie wyszło?
Tutaj z pomocą przychodzi Either, kolejna super rzecz z vavra oraz enum z możliwymi błędami:

enum UserError {  
  WRONG_PASSWORD,USERNAME_NOT_FOUND
}

Teraz starczy zmienić powyższą sygnaturę na
Either<UserError, UserToken> login(String username, String password)

Z biegiem czasu praktycznie wszystkie metody u mnie zwracają Either, albo tam gdzie starcza to Option. Dzięki temu część funkcji można fajnie skrótowo zapisać z pominięciem ifów (tak na prawdę te ify są tylko wewnątrz Either i Option). Dla przykładu to logowanie:

return findUserByUsername(username)  
        .flatMap(user -> checkPasswordFor(candidatePassword, user))
        .map(UserEntity::getUuid)  
        .map(tokenManager::generateTokenFor);

Z minusów, niestety trzeba nauczyć się nowego API i trochę innego sposobu myślenia. Dodatkowo dla niektórych pierwotnie to może się wydawać mniej czytelne.

Oczywiście nie neguje wyjątków całkowicie. Są miejsca gdzie powinny one zostać np łapie i odpowiednio rzucam dalej SQLException jakby coś złego było z bazą (bo to faktycznie jest sytuacja wyjątkowa)

Wpis powstał trochę na zasadzie dzielenia się moich przemyśleń przy tworzeniu swojego projektu, na jakie problemy trafiłem i jak je rozwiązuje. Sam za dużo doświadczenia jeszcze nie mam (niedługo ledwie stanę się juniorem(oby)) więc bardzo chętnie powymieniam się uwagami.

Jeśli komuś będzie chciało się to czytać to pewnie w przyszłości coś jeszcze napisze ;)

somekind

Yyy, Ty nawet juniorem nie jesteś? A na forum piszesz mądrzej od wielu seniorów. Coś kręcisz.

jarekr000000

Mam nadzieję, że nie propagujesz SQLException. Trzeba opakowac w RuntimeException, żeby ktoś kto wywołuje findUserByUsername nie musiał tego obsługiwać. Strzelam, że tak robisz, ale sie upewniam.

CountZero

Też zacząłem używać i jeszcze mam mieszane uczucia. A jak zwracasz dane z kontrolera gdy masz Eithery zamiast prostych danych?

Burdzi0

Fajny wpis. Smaczny. Muszę się zainteresować bardziej Either. Pisz więcej ;)

danek

@CountZero: mam przygotowany generyczny sposób na to. Opiszę to następnym razem. TLDR: enumy błędów mają wspólny interaface z metodami getMessage() i getHttpCode() i mam jeden resolver który sprawdza czy Either jest left czy right i odpowiednio tworzy ResponseEntity

danek

@jarekr000000: logger i leci jako IllegalStateException

danek

@somekind: dwa staże po 3 mc i teraz kończę okres próbny w firmie

jarekr000000

Z tym loggerem to można się zastanawiać - jeśli i tak gdzieś w końcu masz finalną obsługę i catch. log and rethrow prowadzi do zaśmiecania logów. Poza tym ładnie.

jarekr000000

Czyli właśnie widzimy legendarną, ale faktycznie spotykaną jednostkę - junior tylko z nazwy - w tą pozytywną stronę.

CountZero

Ok, chętnie zobaczę czy mamy coś podobnego.

jarekr000000

Czasem mam taki pattern, że sprowadzam obie strony do ResponseEntity i dostaje Either<ResponseEntity, ResponseEntity>, i na koniec return either.getOrElseGet(Function.identity());

danek

to u mnie ta metoda wygląda tak ResponseEntity resolve(Either<? extends ResponseError, ?> input) gdzie ResponseError to ten interface o którym wspomniałem wcześniej

CountZero

@jarekr000000: I wtedy sygnatura metody wygląda tak public ResponseEntity getUser() ? @danek Czyli u Ciebie jest public Either<SpecificError, Success> getUser()? A jeśli w odpowiedzi OK musisz dodać jakieś headery albo coś innego związanego z HTTP, to co wtedy?

danek

logika biznesowa zwraca Either<SpecificError, Success> a warstwa nazwijmy ją kontrolerów mapuje to odpowiednio na ResponseEntity. Nie miałem takiego przypadku ale gdyby trzeba to w tym miejscu bym doklejał jakieś headery

CountZero

Rozumiem, jak coś wstawisz na ten temat na mikro to poczytam :).

danek

jak nie zapomne to zawołam

karolinaa

brawo bardzo ładnie. już bałam się końcówki, że na końcu stwierdzisz że jednak wyjątki albo zrobisz coś głupiego, ale bardzo ładnie. Z minusów, niestety trzeba nauczyć się nowego API i trochę innego sposobu myślenia. - a co to za minus nauczyć się nowej fajnej rzeczy

danek

@karolinaa: bo często to jest argument przeciwko nowym bibliotekom w projekcie: "kto to potem będzie utrzymywał?"

karolinaa

@danek ta - zróbmy to brzydko, już na wstępie brnąć w dług technologiczny to potem łatwiej będzie to utrzymać XDF

jarekr000000

@CountZero: mam Either<ServiceError, User> getUser(....) i funkcje Either<Response, T>( Either<E extends ServiceError, T> response). Potem podobną do prawej sttony, mapping na json robi... i koniec.

kkojot

@danek zobacz jeszcze to https://www.baeldung.com/spring-vavr, nie musisz robić return Option.of(repository.findOne(id));. Też w ten sposób robię sobie teraz projekcik i też mam jeszcze mieszane uczucia. Jeżeli mam logikę, która uderza w kilka serwisów, a z każdego z nich dostaję zawsze Either albo Option, to później w controllerze muszę się sporo napocić z mapami, flatmapi czy nawet foldami, żeby zwrócić odpowiedź :) BTW w Option vavrowym jest bardzo fajna metoda .toEither(left error..), późno ją znalazłem, ale jest bardzo pomocna.

danek

@kkojot: a kto powiedział ze to repository to springowe repository? ;) To mój własny interface który też zwraca Option ale nie chciałem komplikować przykładu. Option ma toEither i z niego korzystam wewnątrz metod, a na zewnątrz zwracam już całego Eithera. Nie mapuje tego w kontrolerze tylko na bieżąco (w przykładzie metoda checkPasswordFor też zwraca Either dlatego jest tam flatMap). W sumie też opiszę to dokładniej jako osobny wpis bo podobny problem jest np z walidacją (też trzeba składać jedno w drugie, zwłaszcza jeśli jest np kilkuetapowa)

DisQ

Do walidacji warto spojrzeć na Validation. Troszkę inne działanie ponieważ nie jest monadą, ale warto poznać bo może znacznie ułatwić kod

danek

@DisQ: no wlasnie kiedyś już się przyglądałem temu, ale póki co średnio mi się podobało

Desu

@danek: masz jakieś repo pisane w takim stylu?

Grzegorz Kotfis

@danek Polecam spojrzeć na repo przygotowane przez Jakuba Pilimona DDD by examples.
Kawał dobrej wiedzy!

danek

@Desu: zaryzykuje i niech będzie, że to https://github.com/krasnoludkolo/ebet2 ;) Problem jest ze to jest taka trochę moja piaskownica i ma w sobie kilka dziwnych pomysłów i miejscami kodu pisanego na szybko. No ale działa

danek

@Grzegorz Kotfis: nie wiem czy jestem mentalne gotowy na DDD :P

Grzegorz Kotfis

@danek rozumiem :) Możesz spojrzeć tylko na funkcyjne podejście które tutaj omawiamy

DisQ

@Grzegorz Kotfis: troszkę się czepiając, po co w CollectingBookOnHold w metodach find rzucacie wyjątek? Takie podejście może zachęcać do wrzucania wszystkiego w jeden wielki blok Try i sterowanie kodem przez wyjątki

Grzegorz Kotfis

@DisQ nie jestem autorem repozytorium. Aktualnie sam się z nim zapoznaję a bardziej z podejściem.

DisQ

To sorki, zrozumiałem, że jesteś jednym z autorów ;)

Grzegorz Kotfis

@DisQ spoko. Ale myślę, że jakbyś pingnął wiadomość do autora to z chęcią odpowie. Niedawno byłem u niego na warsztatach z EventStormingu i zachęcał do kontaktu jeśli pojawią się jakieś wątpliwości.

Desu

@danek: dzięki wielkie. A jakie masz zdanie o używaniu Option w repozytoriach? Bo zainteresowało mnie to podejście i natknąłem się na ten post https://tuhrig.de/anti-patter[...]ionals-for-data-repositories/. Nie programuję w Javie jak coś, tylko PHP, tam nie ma takich udogodnień ;)

danek

@Desu: Autor postu miesza trochę repository z listą wynikową jakiegoś zapytania (gdzie nie będzie nulli bo nie ma być skąd), drugie to chyba zakłada, że musisz kiedyś wyjąć wartość z Option/Optonala z czym nie do końca się zgadzam. U mnie jedynym momentem kiedy to robię jest wyslanie odpowiedzi, więc nie ma w ogole tego problemu. A jak nie masz Optionali to sam sobie je napisz, nie jest to jakas zaawansowana magia ;)

Desu

Wiem właśnie od niedawna sklejam taką mini bibliotekę na własne potrzeby, problem też jest taki, że nie ma typów generycznych :(

MuadibAtrides

@jarekr000000: czy dobrze rozumiem ten mechanizm w javie (optional/either): zwraca się enuma z kodem błędu i jakiś mapper w Javie ogarnia czy ma zwrócić to co jest zarejestrowane na kod błędu lub to co gdy zwrócimy prawidłowy wynik. Dodatkowo Wynik i kod błędu jest owrapowany w jakiś interfejs / klase, który jest wspólny dla tych 2 bytów aby móc na tym operować jako na jednym bycie, żeby nie robić ifologii?

jarekr000000

@MuadibAtrides: trochę tak. Tylko, że nie ma żadnego specjalnego mappera, ani mechanizmu w javie. To zwykła klasa. Można dopisać sobie podobne i używać.

MuadibAtrides

"Oczywiście nie neguje wyjątków całkowicie. Są miejsca gdzie powinny one zostać np łapie i odpowiednio rzucam dalej SQLException jakby coś złego było z bazą (bo to faktycznie jest sytuacja wyjątkowa)" - ja byłem uczony i się zgadzam z podejśćiem - Wyjątki są na wyjątkowe sytuacje, a if należy stosować tam gdzie ta sytuacja jest oczekiwana. Np. Jeśli spodziewam się, że w słowniku może nie być klucza to robimy ifa i sprawdzamy czy w słowniku jest klucz - jeśli zawsze ma być w słowniku klucz to przy pobieraniu powinien pójść wyjątek bo to sytuacja wyjątkowa.

danek

@MuadibAtrides: well, mi się wydaję, że brak oczekiwanej wartości w jakimś zbiorze danych, nie jest aż tak wyjątkową sytuacją. Wyjątki zostawiłbym na sytuacje gdzie coś się spieprzyło na warstwie sprzętowej, a wszelkie sytuacje wynikające z logiki działania aplikacji obsługiwał normalnie, bo inaczej z wyjątku robi się takie GOTO

jarekr000000

@danek jak robisz słownik (mapę) to brak wartości faktycznie najlepiej wyrazić przez Option, ale jak ten słownik użyjesz jako konfigurację programu to może się okazać, że brak jakiegoś parametru to już panika i powód exceptionu. To taka naturalne przeobrażenie problemu - Option/Either warstwy niższej może stać sie Exceptionem (Runtime) warstwy wyższej. Ale nie musi. Jeśli ten słownik jest naszą bazą danych i to np. użytkownik coś tam wyszukuje (i czasem się pomyli), wtedy do końca możemy ciągnąć tego Option.

jarekr000000

@Grzegorz Kotfis: @danek co do repu to IMO da się tam pare rzeczy ładniej zrobić. placeOnHold chyba ten if nie jest potrzebny. Dodatkowo @NonNull mnie razi - to powinien być default i powtarzanie tego wszędzie to szum. Podobnie domyślny final - w javie ma sens... ale pisanie wszedzie final mnie wkurzało. I tak stałem się kotliniarzem.

tamtamtu

nawet nie junior, rozumie ze exceptiony sa zle do sterowania flow i uwaza rownoczesnie ze wyjatki nie sa zle i nalezy je czasem stosowac - za duzo tego dobrego - ktos tu nas chyba ciekawie troluje ;)

danek

@tamtamtu: bardzo mi miło, ale to nie trolling ;)

jarekr000000

@Desu: że jak nie masz type check w kompilacji to oczywiście monady itp. można sobie wsadzić ( Działają ultra ułomnie). Trochę jakby przekonywać ślepego do zalet edytora z kolorowaniem składni.

Desu

@tamtamtu: możesz uzasadnić, dlaczego exceptiony (czasami) są złe? Dla wielu osób taka konstrukcja wydaje się być prostsza i bardziej czytelna. Argument exceptions are for exceptional cases do nich nie trafia, bo nie obrazuje żadnych downside’ów. Jaka konkretnie krzywda im się dzieje, gdy użyją excepitona zamiast tego podejścia?

Tenonymous

@tamtamtu: patrząc po zalewie tematów o treści "nima pracy dla programistuf w tym kraju1!!!" i zestawieniem tego z prezentowaną przez kandydata wiedzą[snake z tutoriala na GH jako highlevel achievement], to jednak nie zapędzałbym się tak z tym, że nie-junior umie to i tamto.;)

tamtamtu

@Desu - bo niszcza przykladowo wydajnosc - jesli stworzysz algorytm w ktorym uzyjesz wyjatkow do kontroli dzilania aplikacji bedzie on zwyczajnie powolny (oczywiscie nie mowie o takim algorytmie gdzie exception zostanie wywolany raz - raczej tysiace i wiecej razy). W wyjatkach nie ma nic zlego jesli korzysta sie z nich zgodnie z ich przeznaczeniem.

Desu

@tamtamtu: a poza wydajnością, bo założymy ze jest on wywoływany właśnie ten raz?

jarekr000000

@Desu - szukaj pod hasłem referential transparency. Exceptiony tworzą osobny flow (poza normalnym dla wartości). Już to komplikuje analizę kodu. Utrudnia też testowanie - Eithery, Optionale pozwalają używać testów parametrycznych, bo to tylko jedne z możliwych wartości. Exceptiony wymuszają osobne testy (kod) na tą ścieżke. No i dodatek - exceptiony mogą być tylko przekazane w górę. Eithery możesz przekazywać jako argument do funkcji -> https://4programmers.net/Mikroblogi/View/25657

Desu

@jarekr000000: dzięki, za dobre wypunktowanie. A da się w Eitherach jakoś odtworzyć to, co dają exceptiony, czyli przekazanie "ścieżki błędu"? Nie wiem jak w Javie, ale w PHP exception może przyjąć poprzedni exception, dzięki czemu możesz w miarę sprawnie dojść do tego, co poszło nie tak. Czy na przykład na samym końcu mam jakiś konkretny left (FooError), ale on mógł być spowodowany jakimś leftem (BarError) wcześniej. Chyba, że to w samej idei jest poroniony pomysł i powinno być obsłużone inaczej.

danek

@Desu either zawiera to co mu dasz. Możesz zrobić sobie zagnieżdżone obiekty jako left i będziesz miał tak cała "historię"

jarekr000000

@Desu - to nie ma sensu. Generalnie powinno się robić odwrotnie - metoda typu fileOpen daje kod błędu (lub plik) - Either. A do piero jak wiesz, że to się nie miało prawa wydarzyć to robisz z tego exception, ze stosem wywołań. Tu chodzi też o tą wydajnośc, czyli żeby nie tworzyć / nie zapamietywać stosu wywołań jeśli to nie jest konieczne.

tamtamtu

@Desu: jesli wyjatek jest uzywany jako wyjatek (czyli cos co niby nie powinno sie pojawic) i jest wywolywany 1 (czy tam pare razy) to nie ma w nim nic zlego. Fanatycy uwazajacy go za zlo wcielone sie myla - ale to zazwyczaj Ci sami co uwazaja singleton za cos zlego oraz traktor za dupiany pojazd - bo duzo pali i jest stosunkowo wolny...

danek
2019-03-18 07:46

Za każdym razem

LukeJL

czasem ma się wrażenie, że postęp w oprogramowaniu dokonuje się głównie za pomocą wymyślaniu i promocji buzzwordów XD Może w rzeczywistości jedyny postęp w IT to ten hardware'owy, a oprogramowanie jako takie w ogóle się nie rozwinęło od lat 90, tylko po prostu obrosło w nowe buzzwordy?

danek

Dużo konceptów powstało już dawno, teraz jedynie powstają nowe narzędzia, a jakoś to sprzedać trzeba

LukeJL

niekoniecznie nawet sprzedać, bo wiele startupów żeruje na pieniądzach od inwestorów albo z UE. Takie firmy nawet nic sprzedawać nie muszą.