Czy zagnieżdżone ify to zawsze zło?

Czy zagnieżdżone ify to zawsze zło?
M4
  • Rejestracja:ponad 6 lat
  • Ostatnio:ponad 5 lat
  • Postów:32
0

Muszę zaimplementować kilka scenariuszy i nie wiem jaki sposób będzie poprawniejszym - zagnieżdżone ify czy interfejsy funkcyjne?

Kopiuj
boolean doSomething(User user, Long jakiesId) {	
		Optional<User> foundedUser = userRepository.findByLogin(user.getLogin());
		if(foundedUser.isPresent()) {
			if(checkEmailsCompatibility(user)) {
				if(!isAlreadyAssigned(user, jakiesId)) {
				      ......
				}else {
					throw new AlreadyAssignedException(jakiesId);
				}
			}else {
				throw new NotCompatibleEmailAddressesException(user.getLogin());
			}
		}else {
			............
		}
		return true;
	}

Kod wydawał mi się czytelny do czasu kiedy nie musiałem dla każdego niespełnionego warunku dodać else throw new Exception... No i to if(foundedUser.isPresent()) bardzo śmierdzi.
Nie do końca wiem jaka jest poprawna praktyka rzucania wyjątków. Powinienem to robić już w metodach sprawdzających? W sensie jeśli mam ifWhatever() to dla niespełnionego warunku rzucać Exception czy dopiero poziom wyżej, jak w kodzie który wkleiłem?

Z tymi ifami to jak z tym dowcipem Pana Grabowskiego. dla zainteresowanych

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
13

Ify to ogólnie zło.
Krok1. Użyj funkcji map i getOrElse, aby pozbyć się tego isPresent. Profit.
Krok2. Zamień Optional na vavr/Option. Profit.
Krok3. Pozamieniaj metody check,is... tak aby zwracały Either<Błąd,User>. Płacz, że nic sie nie kompiluje.
Krok4. Na Option zrób toEither, i teraz lecisz flatMapem wszystko. Nie do powstrzymania - jak Kubica.
Krok5. Nie masz żadnego ifa.


jeden i pół terabajta powinno wystarczyć każdemu
Zobacz pozostałe 3 komentarze
EL
Mądrego to i dobrze posłuchać.
DamianSn
@m4ck: @jarekr000000 ten wykład prowadził :)
M4
wiem, wiem. tamten komentarz miał mieć formę żartu :D
DamianSn
No to mnie wkręciłeś, hahaha
Madness
Aż mi się przypomniała scena z filmu Doktor Strange, gdzie tytułowy bohater spotyka Ancient One i mówi: https://www.youtube.com/watch?v=PnTcG5t3TWw
Akihito
  • Rejestracja:ponad 8 lat
  • Ostatnio:27 dni
  • Lokalizacja:Śląsk
  • Postów:248
3

Nie lepiej jedna metode walidacyjna? Ktora bedzie skladac sie z tych mniejszych metod? Jakby user mial 10 pol do sprawdzenia to bys 10x sie zagniezdzal? Oczywiscie podejrzewam ze roziwazanie jarker00000 jest najlepsze :D. Ale nie znajac monad ja bym zrobil to tak ze zrobil sobie funkcje validujaca i skladajaca sie z tych podfunckji validujacych ktore by w przypadku validacji sypaly exceptionami. Tak wiec unikneloby sie zagniezdzenia ifow bo bys mial po jednym ifie w kazdej z metod :)

Zobacz pozostałe 2 komentarze
cerrato
Ale to nie jest trop, a jedynie pytanie OP, czy te if'y to taka tragedia. W sumie to nikt tego oficjalnie w tym wątku nie poparł, a jedynie są podawane sposoby uniknięcia tej konstrukcji. Być może okaże się, że jednak nie jest ona "najgorsza" i w sumie to nawet całkiem OK ;)
jarekr000000
@cerrato javowe (i nie tylko) ify to konstrukcja błedogenna. Jeśli się da tanim kosztem wykpić z ifów to warto.
cerrato
Zgadzam się z Tobą w pełni. Chciałem jedynie zaznaczyć, że tak właściwie to OP zapytał "czy IF to zawsze zło", ale de facto dyskusja trochę odbiegła od tematu pytania. Nie piszecie teraz o tym, CZY ify są wporzo, a raczej dajecie sposoby, JAK można się ich pozbyć :P
M4
właściwie to masz rację cerrato, aczkolwiek to ja zle skonstruowałem pytanie. odpowiedzi które dostałem są jak najbardziej celnymi i właśnie takich oczekiwałem :)
cerrato
jak tak, to OK i już będę siedzieć cicho ;)
M4
  • Rejestracja:ponad 6 lat
  • Ostatnio:ponad 5 lat
  • Postów:32
0

o takim rozwiązaniu nie pomyślałem a faktycznie jest bardzo czytelne. jak monady mnie przerosną to na pewno wykorzystam. dzięki! :)

Michał Sikora
Michał Sikora
Proponuję nie nazywać monad monadami tylko pojemnikami na wartości i od razu strach znika. :)
cerrato
Święte słowa. Mi monada się kojarzy z gonada i przez to zawszę się lekko czerwienię i zawstydzam, kiedy ktoś o nich wspomina.
M4
@cerrato: hahahah, zwariowana:D
DQ
Najłatwiej to nie przejmować się czy coś jest monadą czy nie, ta wiedza w Javie jest tak de facto zbędna. O wiele bardziej przydatna jest praktyczna umiejętność korzystania czy to z Option czy z Either
jarekr000000
Dodaję do tego, że to takie pojemniki, z których raczej nic nie wyciągamy. Jeśli już to na samym końcu. Maximum: flatMap, map. Minimim: get. Pisząc w ten sposób trudniej popełnić błąd.
wiciu
  • Rejestracja:ponad 11 lat
  • Ostatnio:12 dni
  • Postów:1205
2

Ify to nie zawsze zło. To normalna konstrukcja. W kodzie powinieneś unikać przede wszystkim "drabiny" ifów (chociaż w pojedynczych przypadkach może to być ok) oraz wielokrotnych zagnieżdżeń (nie ważne, czy to będą ify, czy coś innego - np. callbacki), bo takie rzeczy powodują spadek czytelności kodu i trudność w jego utrzymaniu. U Ciebie to już zaczyna się pojawiać. Można to rozwiązać tak, jak Jarek napisał, przepisując to na kod funkcyjny. Nie wiem, czy zawsze będzie to idealne rozwiązanie. Alternatywą może być przerzucenie całego kodu odpowiedzialnego za walidację w osobne miejsce (np. do oddzielnej metody). Wtedy będziesz miał jednego ifa lub jedno wywołanie metody i w lambdzie możesz sobie przekazać kod, który musisz wykonać.

edytowany 1x, ostatnio: wiciu
M4
  • Rejestracja:ponad 6 lat
  • Ostatnio:ponad 5 lat
  • Postów:32
0

NO PRZECIEŻ TO SIĘ NIE KOMPILUJE!! Dostaję Type mismatch: cannot convert from Either<Object,User> to Either<Exception,User> Próbowałem też z Try<> ale tam to w ogóle tak namieszałem, że potrzebowałem git reset żeby kod działał chociaż trochę.

Kopiuj
Either<Exception, User> checkEmailsCompatibility(User user){
		return userRepository.findByLogin(user.getLogin())
				.filter(u -> u.getEmail().equalsIgnoreCase(user.getEmail()))
				.map(u -> Either.right(u)).orElse(Either.left(new NotCompatibleEmailAddressesException(user.getLogin())));
	}
edytowany 1x, ostatnio: m4ck
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
1

Java. type inference. Fail.
Zamień
.map(u -> Either.right(u)).
na
.map(u -> Either.<Exception,User>right(u)).

Przy okazji:

Either<Exception,T> to w zsadzie Try<T>.
Ale przeważnie lepszy jest
Either<E,T> gdzie E to twoja klasa, która NIE dziedziczy z Exceptiona. Bo wtedy nie musi potencjalnie pamiętać StackTrace - profit.
Na grzyba Ci stack trace - jak to wcale nie jest błąd programu, tylko zwykły problem błędnych danych wejściowych.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 2x, ostatnio: jarekr000000
M4
  • Rejestracja:ponad 6 lat
  • Ostatnio:ponad 5 lat
  • Postów:32
0

... nie wierzę. dzięki!

FI
filemonczyk
neo na poczatku tez nie wierzyl
V-2
  • Rejestracja:około 8 lat
  • Ostatnio:10 miesięcy
  • Postów:671
13

Nawet przy skromniejszej refaktorce, zakładającej zachowanie starych, dobrych ifów - wystarczyłoby je poodwracać, żeby zredukować zagnieżdżenie.

Rzucenie wyjątku przerywa bieg działania metody, Nie ma zatem potrzeby dodawać po nim jeszcze else.

W miejsce:

Kopiuj
if (checkEmailsCompatibility(user)) {
    if (!isAlreadyAssigned(user, jakiesId)) {
        ......
    } else {
        throw new AlreadyAssignedException(jakiesId);
    }
} else {
    throw new NotCompatibleEmailAddressesException(user.getLogin());
}

Wystarczyłoby więc zrobić:

Kopiuj
if (!checkEmailsCompatibility(user)) {
    throw new NotCompatibleEmailAddressesException(user.getLogin());
}
if (isAlreadyAssigned(user, jakiesId)) {
    throw new AlreadyAssignedException(jakiesId);
}
......

Fail fast. I już nie ma zbędnych zagnieżdżeń. Jest za to czytelniej. Czy czytelniej niż z konstrukcją typu:

Kopiuj
.filter(u -> u.getEmail().equalsIgnoreCase(user.getEmail()))
.map(u -> Either.right(u)).orElse(Either.left(new NotCompatibleEmailAddressesException(user.getLogin())))

itp., to już temat na osobne rozważania ;)


Nie ma najmniejszego powodu, aby w CV pisać "email" przed swoim adresem mailowym, "imię i nazwisko" przed imieniem i nazwiskiem, ani "zdjęcie mojej głowy od przedniej strony" obok ewentualnego zdjęcia. W drugiej firmie której już pracuję mam palących marihuanę programistów [...] piszą kod "leniwie", często nie wysilając się, rozwlekając ten kod, unikając np. programowania funkcyjnego (mówię tutaj o lambdach w javie).
edytowany 2x, ostatnio: V-2
KK
Pomijając zbędne używanie exceptionów tutaj, to dla mnie te ify są czytelniejsze. Ale może to jeszcze kwestia przyzwyczajenia.
V-2
Zgadzam się co do wyjątków - ale równie dobrze mogłoby być zamiast nich zwracanie jakiejś wartości reprezentującej (negatywny) rezultat walidacji, bez zmiany dla struktury. Boreturn tak samo "wysiada" z metody. @kkojot
V-2
Co do czytelności, styl functional vs. imperatywny to oczywiście kwestia przyzwyczajenia, ale dochodzi tu problem taki, że Javy nie zaprojektowano z myślą o functional programming. Stosowanie tego stylu w Javie często jest, moim zdaniem, siodłaniem krowy i prowadzi do potworków budzących mieszane uczucia estetyczne. Dobrze wiedzieć, że można - ja bym tu tego nie robił. CC @jarekr000000
jarekr000000
Zupełnie się zgadzam, że styl funkcyjny w javie często słabo wygląda. I może być mniej czytelny. Za to jest dużo bezpieczniejszy (expressions vs statements). Musieć tak wybierać jest mega kijowo. Często sie ten kod funkcyjny daje uładnić, tylko trzeba więcej wysiłku niż w stmts włożyć.
Julian_
  • Rejestracja:około 8 lat
  • Ostatnio:ponad 4 lata
  • Postów:1703
0
wiciu napisał(a):

W kodzie powinieneś unikać przede wszystkim "drabiny" ifów (...) oraz wielokrotnych zagnieżdżeń...

Dodam jeszcze, żeby unikać ifów na więcej niż kilka linijek.
U mnie czasem piszą ify na 2 ekrany. ;(

PI
Zbluzgaj ich za to :D
LP
  • Rejestracja:około 7 lat
  • Ostatnio:około miesiąc
  • Postów:366
1

Zasadniczne pytanie w całej tej historii: po co te ify? Należy programować w taki sposób aby nie było możliwe wywołanie doSomething w kontekście gdy isAlreadyAssigned zwróci true.

orchowskia
  • Rejestracja:około 6 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Zielona Góra
  • Postów:83
2

Jak widać jest dużo rozwiązań tego samego problemu :D Dodam że taką walidację można też machnąć dekoratorem:
new CheckEmailCompatibility(new AlreadyAssigned(user, id), user).result(). Aczkolwiek to chyba troszeczkę przerost formy nad treścią w tym przypadku :) Choć dodanie kolejnego warunku staje się dość proste

Akihito
Chociaz za chwile stukna 2 lata expa to ciagle jakos dekoratora nie czaje xD.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
1
lubie_programowac napisał(a):

Zasadniczne pytanie w całej tej historii: po co te ify? Należy programować w taki sposób aby nie było możliwe wywołanie doSomething w kontekście gdy isAlreadyAssigned zwróci true.

Dawaj. Pokaż.
(to nie jest złośliwe - dobry pomysł)


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
vpiotr
  • Rejestracja:prawie 14 lat
  • Ostatnio:prawie 3 lata
0

TL;DR
Do OP: nie zawsze.
Ale w kodzie który podałeś jest taki fragment:

Kopiuj
            }else {
                throw new NotCompatibleEmailAddressesException(user.getLogin());
            }

Który po angielsku nazwałbym "dangling else". Czyli taka sytuacja która wydaje Ci się że powinna zajść jak coś będzie nie tak z adresem email, ale ktoś po drodze może zmienić kod i ta sytuacja "else" będzie już oznaczać coś zupełnie innego.
Tutaj jeszcze jest w miarę jasne co ten else ma robić. Ale co jeśli będzie to mniej czytelne? Dojdzie do ewaluacji pewnej logiki rozmytej. Albo osoba poprawiająca uzna że musi to poprawić albo nie. Nie wiadomo i jest to subiektywne. Jeśli wydzielisz z tego kodu funkcje będzie to bardziej oczywiste.

Gworys
  • Rejestracja:około 6 lat
  • Ostatnio:prawie 6 lat
  • Postów:139
0

Either<> potrafi zaciemnić obraz logiki, często lepiej jest zwrócić własny typ albo użyć specyfikacji, która również zalicza się do deklaracji.

W jego przypadku można by napisać:

Kopiuj
var specyfication = new AssignedUserSpecyfication(login);
var asignedUser = specyfication.satisfyingItemFrom(this.UserRepository); 


Unhandled Exception: System.MissingMethodException: Constructor on type 'System.Exception' not found.
edytowany 2x, ostatnio: Gworys
ME
  • Rejestracja:ponad 7 lat
  • Ostatnio:prawie 3 lata
  • Postów:47
0
jarekr000000 napisał(a):

Ify to ogólnie zło.
Krok1. Użyj funkcji map i getOrElse, aby pozbyć się tego isPresent. Profit.
Krok2. Zamień Optional na vavr/Option. Profit.
Krok3. Pozamieniaj metody check,is... tak aby zwracały Either<Błąd,User>. Płacz, że nic sie nie kompiluje.
Krok4. Na Option zrób toEither, i teraz lecisz flatMapem wszystko. Nie do powstrzymania - jak Kubica.
Krok5. Nie masz żadnego ifa.

Lekki odkop, ale natknąłem się na taki problem, co jeśli mam ciąg odpytań z repository? np:

Kopiuj
    public Either<UserError, User> findById(Long id) {
        return Option.of(findById(id))
                .toEither(UserError.NOT_FOUND);
    }
Kopiuj
    public Either<TicketError, User> findById(Long id) {
        return Option.of(findById(appId, deviceId))
                .toEither(TicketError.NOT_FOUND);
    }

i w serwisie:

Kopiuj
    public Either<ResponseError, SomeResponse> doBusinessLogic(long ticketId, long userId) {
        return userRepository.findById(userId)
                .flatMap(user -> ticketRepository.findById(ticketId)
                        .flatmap(ticket -> doLogic(user, ticket))
                );
    }
```

w takim przypadku dostaje info że `Incompatible equality contraint: TicketError and ResponseError`

`ResponseError` to interface które implementują konkretne Errory

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

ale jeśli z warstwy repository zwracam interface `ResponseError` w `Either` to problem znika:

```java
    public Either<ResponseError, User> findById(Long id) {
        return Option.of(findById(appId, deviceId))
                .toEither(TicketError.NOT_FOUND);
    }
```

Pytanie, czy to jest poprawne? Czy ResponseError powinien się znajdować w warstwie Dao/Repository? Jeśli nie to czy jest jakieś lepsze rozwiązanie? @jarekr000000 @danek wiem że wy kumacie czacze :P
edytowany 1x, ostatnio: Merylin
Shalom
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
4

@Merylin wiesz ze istnieje mapLeft? ;)


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
jarekr000000
To właśnie chciałem napisać.
DQ
  • Rejestracja:prawie 10 lat
  • Ostatnio:7 miesięcy
  • Postów:141
3
Merylin napisał(a):

Pytanie, czy to jest poprawne? Czy ResponseError powinien się znajdować w warstwie Dao/Repository? Jeśli nie to czy jest jakieś lepsze rozwiązanie? @jarekr000000 @danek wiem że wy kumacie czacze :P

ResponseError na pewno nie powinien znajdować się w Dao/Repository - one po prostu nie powinny mieć żadnej wiedzy o tym, że istnieje jakiś tam ResponseError bo to nie jest ich odpowiedzialność.

Ogólnie, twoje metody findById które wkleiłeś wyglądają w miarę legitnie (pewnie niektórzy kłóciliby się czy jest sens zwracana Either zamiast Option w przypadku, gdy błąd jest zawsze ten sam, ale to "pomniejsza" sprawa). Gdzieś w aplikacji pewnie powinny się pojawić funkcję mapujące UserError/TicketError w jakiś inny błąd (potencjalnie ResponseError) - tutaj dużo zależy jak to wszystko sobie zamodelujesz i zależy stricte od aplikacji. Mając funkcję (zazwyczaj idące z szczegółowego błędu do bardziej uogólnionego) możesz sobie użyć mapLeft żeby typy się zgadzały.

danek
  • Rejestracja:ponad 10 lat
  • Ostatnio:7 miesięcy
  • Lokalizacja:Poznań
  • Postów:797
2

Przy małej aplikacji pewnie bym się w to aż tak nie bawił, ale przy czymś, co się rozrasta, to tak jak @DisQ mówi, gdzieś trzeba sobie zrobić smutne mapowanie jednego w drugie. Kotlin jest o tyle fajny, że zrobi sie to extensionami.


Spring? Ja tam wole mieć kontrole nad kodem ᕙ(ꔢ)ᕗ
Haste - mała biblioteka do testów z czasem.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
2

Rozwinę to powyższe:

  1. bardzo mała aplikacja
    jeden typ błędu - niezbyt eleganckie, ale wystarcza i nie widzę kłopotów

  2. większa aplikacja - modularne błędy :
    wykorzystanie mapLeft i jakiegoś mapera błędów między warstwami ( a nawet kilku maperów)

  3. eksperyment (lekka alternatywa do poprzedniego)
    błąd w warstie serwisu rest jako Any - nie ma mapera - za to jakiś handler na httpresponse, niby brzydkie, ale niczym się nie różni od klasyki (catch Exception e to też taki Any). Nic większego tak nie zrobiłem, ale mam wrażenie, że jest to kompromis, który można zastosować jeśli obsługa błędów zaczyna przyradzać się w static hyping.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 5x, ostatnio: jarekr000000
danek
to ostatnie nawet może ładnie działać, jak obiekt błędu ma sensowną strukturę. Poleci Od razu przez maper na jsona i w sumie tyle
jarekr000000
Przyznam, że zainspirowali mnie Scalowcy i ZIO, gdzie to Any się często pojawia (w kodzie tutorialowo - uproszczonym) - najpierw wkurzało mnie jako herezja, ale teraz widzę że chyba czasem można.
Charles_Ray
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Postów:1880
0

Jeśli ify wynikają z logiki biznesowej (zlozonosc esencjonalna), to nie widzę w tym nic złego. Czasem lepszy jeden zagnieżdżony if niż jak wjeżdża turbo wzorzec strategii, który ma pod spodem jedna strategie ;p

Odnośnie tych monad i flatmapów - ja uważam to w 99% przypadków za przerost formy :) utrzymywałem taki system, gdzie były same mapy, flatmapy, combiney i było ciężko. Być może mam za niskie IQ i jestem słabym kompilatorem.


”Engineering is easy. People are hard.” Bill Coughran
edytowany 1x, ostatnio: Charles_Ray
danek
nie chce drugi raz wchodzić do tej samej rzeki to przysłowie działa dokładnie odwrotnie niż go użyłeś :D
Charles_Ray
Widać, że polonistą nie jestem xD zmienię, dzięki
Akihito
@Charles_Ray: pare ifow jak nie idze skrocic to spoko :D ale tak to sie zwykle zaczyna 4-5 zagniedzen na start. A po roku wracasz i masz drabine na 30-40 ifow bo juz jak ktoś widzi 15 ifów to dopisze 16 i pomyśli już wiecej nie trzeba :D
Charles_Ray
Nie no, wiadomo... myślmy :)

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.