Czy używasz TDD? oraz pytania do TDD.

Czy używasz TDD? oraz pytania do TDD.
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0

Zauważyłem że Twoje testy w projekcie mają room na improvement. Starałem Ci się to pokazać, ale widocznie mi się nie udało. Wiesz swoje lepiej. Uważam że Twoje testy mogłyby być lepszę, mógłbym Ci pokazać kilka sztuczek albo miejsc w których mógłbyś poprawić te testy, ale widzę że wolisz polemikę bardziej niż faktyczne rozważania o tym jak możnaby poprawić te testy.

Twój poprzedni post opiera się na zasadności zmian. Mówisz że moja "zmiana" lambd na regexpy nie jest zasadna, tak samo usunięcie listy jednokierunkowej. Mówisz też że "nic nie zrobiłem", i wreszcie stosujesz argumentum ad absurdium, czyli kod zbyt dobrze dopasowany do testów, czyli coś o czym Kent Beck pisze w swojej książce "TDD By Example" w rozdziale o usuwaniu duplikacji.

Pamiętaj że ja nic nie refaktorowałem i nie zmieniłem (tzn. nie przerabiałem istniejącego kodu). Powiedziałem Ci - po prostu usunąłem Core/, i napisałem najprostszą implementację przechodzącą test, tak jak moim zdaniem powinno się pracować w TDD. Żaden inny kod nie był konieczny żeby testy przeszły.

  • Narzekałes że użyłem regexpów - mówiłem Ci, zmiana na lambdy i predicate'y jest trywialna, wszędzie gdzie wsklejam regexpa, wystarczy wsadzić lambdę z predykatem. Mówisz że to co ja napisałem to jest "implementacja regexpów w regexpach". To dla mnie jasny sygnał że próbujesz myśleć o tej aplikacji z perspektywy implementacji. Stworzyłeś sobie klasy Matcher z chainowanym interfejsem. To co ma znaczenie, to to czy zbudowanie konkretnych metod zmatchuje konkretny string. To czy pod spodem jest regexp czy predykaty to jest szczegół implementacyjny, i nie ma specjalnego znaczenia. Ale nadal - możesz bardzo łatwo zamienić te regexpy na te lambdy, nadal to nie zmieni faktu że pozostała większość logiki jest nieprzetestowana. Niektóre metody z Twojego buildera w ogóle nie są wołane w testach.
  • Narzekałes że nie ma listy jednokierunkowej - po pierwsze, to jest mikrooptymalizacja, nigdzie nie usuwasz elementów, więc dostęp po array'u jest okej. A nawet jakbyś wyciągnął profiler, i faktycznie wykazał że użycie listy jednokierunkowej w tym miejscu jest zasadne, to powinno to być kwestią podmiany implementacji kolekcji (mianowicie new List<>() na new LinkedList())
  • Narzekałeś na to że kod jest zbyt dopasowany do testów - ciężko żeby return true; nie był doskonale dopasowany do Assert.True() - bo tyle wymagają niektóre Twoje metody żeby testy przeszły. To jest dla mnie jasny sygnał że te testy po prostu nie są zbyt silne żeby wymusić poprawną implementację.

Jesli nie podoba Ci się implementacja która w ten sposób powstała, to to jest jeszcze kolejny sygnał, że testy nie są dobre. Bo kod który w ten sposób powstał to jest ilustracja tego co faktycznie było przetestowane. Więc w Twojej "idealnej" implementacji bez regexpów i z NextOperation, przetestowane było tylko to co widzisz w moim forku. Wszystko pozostałe (praktycznie cała logika, exception, etc.) nie było. Żeby w stylu TDD przerobić mój kod w Twój, musiałbys napisać więcej testów które wymusiłyby te zimany. Np testy pod exception message, które wymusiłby dodanie message'ów do exceptionów.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
1

Wiesz swoje lepiej.
Żeby w stylu TDD przerobić mój kod w Twój, musiałbys napisać więcej testów które wymusiłyby te zimany. Np testy pod exception message, które wymusiłby dodanie message'ów do exceptionów.

Zgodziłem że testy są dziurawe i nie pokrywają m.in tego, co nie jest na "happy-path".
Jak i również zgadzam się że patologią jest że return true w niektórych przypadkach przechodził.

Narzekałes że nie ma listy jednokierunkowej - po pierwsze, to jest mikrooptymalizacja

Nikt nic o optymalizacji i wydajność nie piszę.

Napisałem wyłącznie że propertisy były szczegółem implementacji, a zatem jeżeli zmienisz implementacje, to one wylecą.

Wspominam o tym, bo pisałeś w taki sposób jakby to był błąd że propertisy wyleciały, a testy są na zielono.

Narzekałes że użyłem regexpów - mówiłem Ci, zmiana na lambdy i predicate'y jest trywialna...

Miałem inne oczekiwania co do tego co zrobisz. Tak jak pisałem - sądziłem że faktycznie tylko usuniesz kod i pokażesz mi że wszystkie testy są nadal zielone.
Gdybym wiedział że tak naprawdę chodzi o to, aby napisać od 0 / przerobić ten kod, to bym nawet nie pisał, bo po co?

Jesli nie podoba Ci się implementacja która w ten sposób powstała, to to jest jeszcze kolejny sygnał, że testy nie są dobre.

Nie piszę czy jest ładna czy też nie, a że po prostu z definicji jest krótsza, bo wywołuje bibliotekę pod spodem.

Ale to jest to, co pisałem wyżej - miałem inne oczekiwania co do tego co nazywasz "wywaleniem kodu".

Pamiętaj że ja nic nie refaktorowałem i nie zmieniłem (tzn. nie przerabiałem istniejącego kodu). Powiedziałem Ci - po prostu usunąłem Core/, i napisałem najprostszą implementację przechodzącą test, tak jak moim zdaniem powinno się pracować w TDD.

Tu można debatować na ile jest to refaktor, bo w dużym stopniu jest.

Więc w Twojej "idealnej" implementacji bez regexpów i z NextOperation

Nie jest to idealna implementacja i nic takiego nie napisałem, jest po prostu moja. Jedyna fajna rzecz tutaj to czytelne API, imo. Gdyby była taka libka w BCL i miała perf. podobny do "compiled regex", to używałbym zamiast Regexów.

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
1
jarekr000000 napisał(a):

Jak autor wybiera zwracanie X i rzucanie wyjątkiem (obsługiwalnym) to czuje się jakbym wszedł do toalety w restauracji, gdzie właściciel nie zamontował umywalki, bo i tak większość ludzi rąk nie myje, i preferuje okazjonalne rzucenie exceptionem (do przodu, albo do tyłu) kilka dni po wizycie.

Wybierając trzeba brać pod uwagę to, czy można uzależniać ludzi od jakiegoś swojego, a nie natywnego dla języka sposobu zwracania błędów. Wybranie tego nienatywnego może skutkować tym, że ludzie sobie ładnie te resulty w exceptiony opakują i wyrzucą.

To jest znacznie głębszy problem niż tylko "wybór autora jednej biblioteki", to się rozbija o lata edukacji... Przecież nawet to forum pełne jest architektów, którzy twierdzą, że wyjątki są dobre.

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
1
somekind napisał(a):

Wybierając trzeba brać pod uwagę to, czy można uzależniać ludzi od jakiegoś swojego, a nie natywnego dla języka sposobu zwracania błędów. Wybranie tego nienatywnego może skutkować tym, że ludzie sobie ładnie te resulty w exceptiony opakują i wyrzucą.

To jest znacznie głębszy problem niż tylko "wybór autora jednej biblioteki", to się rozbija o lata edukacji... Przecież nawet to forum pełne jest architektów, którzy twierdzą, że wyjątki są dobre.

Tu pisze z perspektywy użytkownika, bo jak autor biblioteki wybrał "natywne" wyjątki - to co prawda, mogę to sobie opakowac w Result, ale to jest sytuacja jak w żarcie o matematykach - podpalam dom i sprowadzam problem do postaci dla której jest już znane rozwiązanie.
W drugą stronę (w tym konkretnym przypadku wybranie Result<T> ) ma to jednak więcej sensu - niech sobie maniacy opakowują i rzucają wyjątki, ale przynajmniej można ich całkiem nie mieć jak ktoś nie chce.
Powiedziałbym, że tu można zastosować Principle of Least Power, gdbyby nie to, że to kolejna mętna zasada inżynierska i nie wiadomo co do końca znaczy.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0

Zgadzam się, że kiedy mówimy o jednym module, to faktycznie Result<T> vs wyjątki to może być wzburzona rozmowa. Dla mnie, wtedy to jest szczegół implementacyjny, i w gruncie rzeczy nie ma żadnego znaczenia.

Ale jeśli mówmy o dwóch modułach współpracujących ze sobą, np nasza logika i jakaś biblioteka, która przyjmuje lambdę/callback/implementację interfejsu, to nie możemy tak o sobie zwrócić Result<>, bo przecież nie mamy kontroli nad interfejsem tej biblioteki. Wtedy, wyjątki to jest wtedy jedyny sposób komunikowania się ze sobą.

Przykład pseudokodu

Kopiuj
import parser from "external library";

try {
  parser.parse(string, new Listener {
    void onSomething() {
      if (this.someCondition) {
        throw new MyException();
      }
    }
  });
  return 1;
} catch (MyException e) {
  return 2;
}

class MyException extends Exception {}

Tzn. okej, nie jedyny, bo teoretycznie możnaby zapisać np wynik w zmiennej i ją zwrócić, więc są alternatywy. Ale chodzi o to że wtedy użycie Result<> już tutaj odpada.

Dodatkowo to jest spoko, bo możę być tak, że używamy kilku bibliotek po sobie - np parser woła callbackiem cache, cache woła callbackiem coś jeszcze, i to coś jeszcze może się nie udać - i chcemy poinformować logikę naszą - ciężko byłoby "wrócić" chainem callbacków z informacją o tym, a taki wyjątek nam załatwia sprawę "sam". Tym większa zaleta wyjątków im częściej staramy się stosować DI.

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
0
jarekr000000 napisał(a):

Tu pisze z perspektywy użytkownika, bo jak autor biblioteki wybrał "natywne" wyjątki - to co prawda, mogę to sobie opakowac w Result, ale to jest sytuacja jak w żarcie o matematykach - podpalam dom i sprowadzam problem do postaci dla której jest już znane rozwiązanie.
W drugą stronę (w tym konkretnym przypadku wybranie Result<T> ) ma to jednak więcej sensu - niech sobie maniacy opakowują i rzucają wyjątki, ale przynajmniej można ich całkiem nie mieć jak ktoś nie chce.

No tak, tylko to jest możliwe w całkiem nowej bibliotece, bo dla istniejącej to będzie srogie breaking change, wymagające potężnej migracji w kodzie klienckim zanim będzie znowu używalny.
A poza tym zawsze pozostaje pytanie - jaki Result<>? Swój własny, czy z jakiejś opensourcowej biblioteki? A jeśli tak, to z której? A jak będzie kilka z różnych źródeł, to kto będzie obsługiwał kod mapujący między nimi?

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
1

@somekind:

to kto będzie obsługiwał kod mapujący między nimi?

caller? jak inaczej to sobie wyobrażasz?

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
0
WeiXiao napisał(a):

caller? jak inaczej to sobie wyobrażasz?

No bardzo prosto. Zespoły biznesowe stwierdzą, że to platforma i zrzucą zadanie na nas. Najgorsze, że nawet będą mieli rację.
A po co mi dodatkowa robota? A komu to potrzebne? A dlaczego?

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2
Riddle napisał(a):

Ale jeśli mówmy o dwóch modułach współpracujących ze sobą, np nasza logika i jakaś biblioteka, która przyjmuje lambdę/callback/implementację interfejsu, to nie możemy tak o sobie zwrócić Result<>, bo przecież nie mamy kontroli nad interfejsem tej biblioteki. Wtedy, wyjątki to jest wtedy jedyny sposób komunikowania się ze sobą.

To chyba największy antypattern. Próbowałeś tak zrobić w JS z callbackiem. Albo z dowolnym async w Javie?
Tu oczywiście problem robi ktoś kto projektuje bibliotekę, tak, że nie można zwrtócić Result<T>, tylko trzeba kombinować z wyjątkami. A żeby było śmieszniej często i tak to kombinowanie nic nie da (patrz wyżej). Co więcej może być tak, że w jednej wersji biblioteki będzie działać, a w kolejnej nie, bo autor wywoła tego callbacka async, albo w innym miejscu (i wtedy wyjątek leci np. poza try/ catch). To zresztą pokazuje jak porypane są wyjątki do obsługi flow - szczególnie jak używasz callbacków.

somekind napisał(a):
jarekr000000 napisał(a):

No tak, tylko to jest możliwe w całkiem nowej bibliotece, bo dla istniejącej to będzie srogie breaking change, wymagające potężnej migracji w kodzie klienckim zanim będzie znowu używalny.

Akurat mówimy o projektowaniu API. Między innymi po to, aby w kolejnych wersjach było mniej powodów do naprawiania/zmieniania.

A poza tym zawsze pozostaje pytanie - jaki Result<>? Swój własny, czy z jakiejś opensourcowej biblioteki? A jeśli tak, to z której? A jak będzie kilka z różnych źródeł, to kto będzie obsługiwał kod mapujący między nimi?

A jak język jest dynamiczny i nie ma typów w czasie kąpilacji? Tyle pytań...
Technicznie to taki problem dokładnie istnieje w Javie i Kotlinie, nie ma standardowego Either/Result.... i nie ma wielkiego problemu, bo taka konwersja
a) jest trywialna - można sobie szybko utilsa napisać
b) takie konwersje są dostępne w ramach bibliotek (czasem z problemami - konkretnie w Kotlinie na gotowo mam tylko mapowanie stratne (przez Optionala) - ale jak to jest problem to punkt a)
c) aż tak dużo takich bibliotek nie ma ( w Kotlinie mam dwa Eithery do wyboru)

Częstszy jest problem pierdyliardów implementacji Monad IO, i reaktywnych strumieni. (Mono, Source, ZIO, IO itd) - to występuje w Scali, i w nieco mniejszym stopniu w Kotlinie. I znowu - konwersje to żaden problem (chociaż głównie dlatego, że jest Reactive Stream API (Publisher) - każda z bibliotek pozwala na "konwersje" z/do Publishera -> problem solved.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):
Riddle napisał(a):

Ale jeśli mówmy o dwóch modułach współpracujących ze sobą, np nasza logika i jakaś biblioteka, która przyjmuje lambdę/callback/implementację interfejsu, to nie możemy tak o sobie zwrócić Result<>, bo przecież nie mamy kontroli nad interfejsem tej biblioteki. Wtedy, wyjątki to jest wtedy jedyny sposób komunikowania się ze sobą.

To chyba największy antypattern. Próbowałeś tak zrobić w JS z callbackiem. Albo z dowolnym async w Javie?
Tu oczywiście problem robi ktoś kto projektuje bibliotekę, tak, że nie można zwrtócić Result<T>, tylko trzeba kombinować z wyjątkami. A żeby było śmieszniej często i tak to kombinowanie nic nie da (patrz wyżej). Co więcej może być tak, że w jednej wersji biblioteki będzie działać, a w kolejnej nie, bo autor wywoła tego callbacka async, albo w innym miejscu (i wtedy wyjątek leci np. poza try/ catch). To zresztą pokazuje jak porypane są wyjątki do obsługi flow - szczególnie jak używasz callbacków.

To prawda, ale jaki inaczej chciałbyś to ogarć? Nie możesz edytować interfejsu nie Twojej biblioteki, więc jaki masz pomysł na obsługę tego?

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2
Riddle napisał(a):

To prawda, ale jaki inaczej chciałbyś to ogarć? Nie możesz edytować interfejsu nie Twojej biblioteki, więc jaki masz pomysł na obsługę tego?

Nic nie zrobie :-(. Po prostu wybierając biblioteki staram się szukać takich, które mają normalną obsługę błędów, a nie wyjątki.
A projektując API można o tym pamiętać. Bo z Result<T> exception łatwo zrobić, ale w drugą stronę są już problemy.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):
Riddle napisał(a):

To prawda, ale jaki inaczej chciałbyś to ogarć? Nie możesz edytować interfejsu nie Twojej biblioteki, więc jaki masz pomysł na obsługę tego?

Nic nie zrobie :-(. Po prostu wybierając biblioteki staram się szukać takich, które mają normalną obsługę błędów, a nie wyjątki.

No, nie sądze że to jest jakiekolwiek wyjscie.

Dajmy jakiś Parser z wizytorem. Przekazujesz mu swojego listenera, i parser iteruje drzewo AST. I w pewnym momencie ty chcesz ze swojego listenera zwrócić do swojej logiki jakiś błąd, response. Nie możesz zwrócić Result<T>, bo to wymagałoby zmiany sygnatury biblioteki. Biblioteka też nie może mieć generyka ani nic takiego, bo jak zauważyłeś być sync i nie koniecznie musi zwrócić od razu, no i dodatkowo może być tak skonstruowana że przez specyfikę domeny w której pracuje może się zwyczajnie nie dać nie zwrócić z listenera.

Dla przykładu: chcemy mieć aplikację, która umie podmienić minki w stringach w jakimś pliku typu markdown, na obrazki. ściągamy parser markdown który ma wizytor który łazi po AST, i umie też podmieniać node'y. Nasza aplikacja wczytuje z bazy mapę minek na obrazki, iterujemy parserem po treści, i natrafiamy na minkę która ma niepoprawny format, więc chcemy jakoś dać znać logice że trafiliśmy na coś takiego, żeby poinformowała usera. Jeśli mamy taki listener, to nie widzę sposobu jak z niego zwrócić Result<T>. Wygląda na to że wyjątek to jedyne sensowne wyjście (oprócz oczywiście zapisania zmiennej i potem odczytania jej, tylko wtedy też musielibyśmy powiedzieć jakoś listenerowi żeby przestał parsować, bo to już nie ma sensu, taki break, niektóre parsery mają taką funkcje).

Dlatego mówiłem, że tak długo jak pracujemy w ramach jednego moduły to faktycznie Result<T> jest spoko alternatywą dla wyjątków, ale jak chcemy stosować DI pomiędzy dwoma modułami, to wtedy już się (chyba?) nie da.

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2

@Riddle
Nie ogarniam. Dlaczego zakładasz, że nie można tej biblioteki zrobić od razu (publicznie) dobrze - tak, żeby w odpowiednich miejscach był Result tylko najpierw musi istnieć wersja jakoś zrypana.
W takim Rust Result jest prawie domyślny.
W scali mamy Either, ZIO i jakoś ludzie stosują.
(ZIO to troszkę więcej niż rezult - powiedzmy AsyncResult)

Dlatego mówiłem, że tak długo jak pracujemy w ramach jednego moduły to faktycznie Result<T> jest spoko alternatywą dla wyjątków, ale jak chcemy stosować DI pomiędzy dwoma modułami, to wtedy już się (chyba?) nie da.

To jest dla mnie całkowita zagadka. Co ma do tego DI? Czy są jakieś inne podobne ograniczenia. Np. jak masz DI to możesz zwracać unsigned int, ale nie możesz zwykłego int?

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):

Nie ogarniam. Dlaczego zakładasz, że nie można tej biblioteki zrobić od razu (publicznie) dobrze - tak, żeby w odpowiednich miejscach był Result tylko najpierw musi istnieć wersja jakoś zrypana.

No ale to polegasz na tym, że autor biblioteki wyznaje podobne zasady i praktyki co Ty. Nie wiem czy to jest zasadne założenie.

Może w takim Rust, faktycznie jest to zasadne założenie, skoro jest tak jak mówisz że Result jest de-facto standardem. Ale w innych jezykach, gdzie nie jest to tak powszechne?

jarekr000000 napisał(a):

To jest dla mnie całkowita zagadka. Co ma do tego DI? Czy są jakieś inne podobne ograniczenia. Np. jak masz DI to możesz zwracać unsigned int, ale nie możesz zwykłego int?

No mam na myśli to, że korzystanie z Result<T> polega na takiej cesze, że przekazując polimorficzny handler, jesteś w stanie z niego zwrócić co chcesz - i nie zawsze masz ten komfort (bo np autor biblioteki tego nie przewidział). Wyjątki są obejściem na to.

Miang
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 1792
0
Riddle napisał(a):
jarekr000000 napisał(a):
Riddle napisał(a):

To prawda, ale jaki inaczej chciałbyś to ogarć? Nie możesz edytować interfejsu nie Twojej biblioteki, więc jaki masz pomysł na obsługę tego?

Nic nie zrobie :-(. Po prostu wybierając biblioteki staram się szukać takich, które mają normalną obsługę błędów, a nie wyjątki.

No, nie sądze że to jest jakiekolwiek wyjscie.

Dajmy jakiś Parser z wizytorem. Przekazujesz mu swojego listenera, i parser iteruje drzewo AST. I w pewnym momencie ty chcesz ze swojego listenera zwrócić do swojej logiki jakiś błąd, response. Nie możesz zwrócić Result<T>, bo to wymagałoby zmiany sygnatury biblioteki. Biblioteka też nie może mieć generyka ani nic takiego, bo jak zauważyłeś być sync i nie koniecznie musi zwrócić od razu, no i dodatkowo może być tak skonstruowana że przez specyfikę domeny w której pracuje może się zwyczajnie nie dać nie zwrócić z listenera.

callback

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2
Riddle napisał(a):

No ale to polegasz na tym, że autor biblioteki wyznaje podobne zasady i praktyki co Ty. Nie wiem czy to jest zasadne założenie.

Już pisałem. W miarę możliwości szukam oczywiście biblioteki, której autor wyznaje podobne zasady i praktyki. Nie wiem, co w tym dziwnego.
Mam używać bibliotek, które wprowadzają mi zamęt w kodzie? Bo?

Oczywiście jest to jeden z powodów dla których na jvm preferuje Scalę (bo jest 95% szans, że biblioteka będzie miała jakąś ludzką obsługę błedów).

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):
Riddle napisał(a):

No ale to polegasz na tym, że autor biblioteki wyznaje podobne zasady i praktyki co Ty. Nie wiem czy to jest zasadne założenie.

Już pisałem. W miarę możliwości szukam oczywiście biblioteki, której autor wyznaje podobne zasady i praktyki. Nie wiem, co w tym dziwnego.
Mam używać bibliotek, które wprowadzają mi zamęt w kodzie? Bo?

To już jest trochę osobny temat. Ja tylko napisałem wyżej, że wyjątki są pomocne wtedy, kiedy korzystamy z biblioteki która nie pozwala zwracać z litenerów tego co chcemy. Tutaj się zgadzamy, chyba, że się nie da?

Rozumiem, że teraz odchodzisz od tematu, mówiąc że lepiej z takich bibliotek nie korzystać? No oczywiście nikt Ci nie karze korzystać z bibliotek które nie pasują do Twoich dobrych praktyk oczywiście; ale moim zdaniem to nie ma aż takiego znaczenia, bo jak mówiłem, to jest szczegół implementacyjny.

Jak ja dodaje biblioteki do swoich projektów, to staram sie je opakować w taki interfejs żeby mi się fajnie z nich korzystało; i jak Ty lubisz Result<T> to mozęsz opakować bibliotekę która rzuca wyjątki w Result<T> i pozostała część TWojej aplikacji możę jej wtedy normalnie używać. Nikt Ci nie każde robić tight-coupling na bibliotekę która Ci się nie podoba.

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
1
Riddle napisał(a):

Jak ja dodaje biblioteki do swoich projektów, to staram sie je opakować w taki interfejs żeby mi się fajnie z nich korzystało; i jak Ty lubisz Result<T> to mozęsz opakować bibliotekę która rzuca wyjątki w Result<T> i pozostała część TWojej aplikacji możę jej wtedy normalnie używać. Nikt Ci nie każde robić tight-coupling na bibliotekę która Ci się nie podoba.

Spoko. Tak robię. Ale pisałem dlaczego opakowanie czegoś co polega na wyjątkach w Result<T> wcale nie jest takie spoko. Niestety nie ma symetrii - jeśli biblioteka używa Result to ktoś, kto kocha wyjątki łatwo użyje. Ale w drugą stronę są już nieciekawe problemy - od tego, że w przypadku callbacków, może być niejasne gdzie wyjątek łapać, do głupich problemów wydajnościowych.
Rzucanie tysięcy exceptionów (w odróżnieniu od Result-ów) na sekundę bywa problematyczne (zalezy od jvm i kodu).

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):
Riddle napisał(a):

Jak ja dodaje biblioteki do swoich projektów, to staram sie je opakować w taki interfejs żeby mi się fajnie z nich korzystało; i jak Ty lubisz Result<T> to mozęsz opakować bibliotekę która rzuca wyjątki w Result<T> i pozostała część TWojej aplikacji możę jej wtedy normalnie używać. Nikt Ci nie każde robić tight-coupling na bibliotekę która Ci się nie podoba.

Spoko. Tak robię. Ale pisałem dlaczego opakowanie czegoś co polega na wyjątkach w Result<T> wcale nie jest takie spoko. Niestety nie ma symetrii - jeśli biblioteka używa Result to ktoś, kto kocha wyjątki łatwo użyje. Ale w drugą stronę są już nieciekawe problemy - od tego, że w przypadku callbacków, może być niejasne gdzie wyjątek łapać, do głupich problemów wydajnościowych.
Rzucanie tysięcy exceptionów (w odróżnieniu od Result-ów) na sekundę bywa problematyczne (zalezy od jvm i kodu).

Wstęp

Pełna zgoda. Tylko chciałbym rzucić światło na jeden element. Wydaje mi się że broniąc Result<>ów, tak na prawdę bronisz pewnej cechy którą lubisz w bibliotekach.

Rozwinięcie

Mówisz o problemie, w którym nie da się przemapować wyjątków na Result<>, biblioteka może zawołać listener później, i nie jest jasne gdzie dodać try/catch. Mówisz też, że używając Result<> takich problemów nigdy nie ma, bo biblioteka go po prostu zwróci. Tutaj zgoda.

Tylko patrz, to o czym mówisz, tak na prawdę to nie jest dysputa Result<> vs wyjątki. To tak na prawdę jest dysputa "czy biblioteka powinna robić coś po tym jak zwróci swoj wynik". Czyli tak jakby, czy biblioteka jest klamrowa (innymi słowy, zaczyna się, robi coś, i zwraca. i po zwróceniu już nic nie robi). To co jest niejawne, to to, że skorzystanie z Result<> wymusza zwrócenie wartości z wyniku wszystkich listenerów, i potem się już nie zawołają (czyli w pewnym sensie, jak funkcja korzysta z Result<>, to mamy pewnośc że jest klamrowa). Z wyjątkami takiego wymuszenia nie ma, bo można je rzucić skądkolwiek - i to rodzi problemy, takie własnie jak to że nie wiadomo gdzie zapiąć try/catch, i problemy z asynchronicznym wołaniem, i wszystko o czym mówiłeś.

Więc jak się dokopać do serca problemu, to wychodzi na to że tak na prawdę nie jest tak że Result dobry, wyjątki złe; tylko wychodzi po prostu na to, że podoba Ci się pomysł, że biblioteka nie powinna robić nic dodatkowego po zwróceniu (a przynajmniej nic, co wymaga obsługi w kodzie), to miałem na myśli we wstępie że bronisz klamrowości bibliotek. I ja się z tym absolutnie zgadzam - to jest jaknajbardziej super cecha bibliotek. Biblioteki które takie nie są, są mega słabe, i zgadzam się ze wszystkimi arugmentami które wyciągnąłeś. Idealnie by było, gdyby wszystkie biblioteki takie były.

Jedyna różnica polega więc na tym, że z Result<> klamrowość funkcji jest zagwarantowana, a z wyjątkami nie. Innymi słowy, jak widzimy że funkcja zwraca Result to możemy być pewni że funkcja jest klamrowa. Jak mamy wyjątki, to funkcja może być klamrowa, ale nie koniecznie. Więc pod tym względem, faktycznie Result<> jest lepszy.

Tylko że ta gwarancja klamrowości nie jest za darmo - ona kosztuje to, że autor biblioteki musi to przewidzieć i dodać. I wyobrażam sobie, że jak znajdziemy bibliotekę która ma interfejs taki że nic nie zwraca (albo zwraca od razu) a dopiero potem woła listener (czyli taką którą nazwałeś że jest zrypana i wprowadza zamęt w kodzie - ja ją nazwę że jest nie-klamrowa); to już samo zapewnienie żeby te listenery się wołały przed zwróceniem byłoby bardzo trudne do napisania. I to chyba jest powód czemu nie lubisz takich bibliotek. Bo jeśli autor biblioteki nie zapewni klamrowości, to zrzuca ten obowiązek na Ciebie. A Ty chciałbyś korzystać z klamrowych funkcji (tzn. każdy by chciał - ja też), więc dla Ciebie by było lepiej żeby autor biblioteki o to zadbał. Więc to jest taki trade.

Myśle, całą debatę można sprowadzić do pytania: do kogo należy zapewnienie klamrowości funkcji? - do autora biblioteki czy do użytkownika bibliteki? To jest serce problemu, wydaje mi się. Rozumiem, że Ty jesteś w kampie "jasne że po stronie autora biblioteki". Ja tutaj się nie wypowiadam.

Ale idąc dalej - mówiłeś że nie da się przemapować Result<> na wyjątki 1:1 - i faktycznie, nie zawsze się da. A konkretnie - nie da się, wtedy kiedy biblioteki nie są klamrowe. Jeśli są, to zarówno Result=>wyjątki, oraz wyjątki => Result tak samo łatwo przemapować na siebie. Więc kiedy mówisz "Nie da się przemapować wyjątków na Result" - to między wierszami można przeczytać "Nie da się zwrócić Result z nieklamrowej funkcji", no i to jest prawda. Musiałbyś włożyć wysiłek, żeby zrobić ją klamrową - ten sam wysiłek, którego autor biblioteki nie stworzył. A jak mówisz że "używam tylko dobrych bibliotek", to tak na prawdę mówisz "Używam tylko bibliotek, w których autor zadbał o klamrowość funkcji". I to jest jaknajbardziej w porządku. Tylko właśnie po tym widać, że cała debata nie dotyczy Result vs wyjątki, tylko własnie klamrowości. Bo jak już zapewniemy klamrowośc, to mapowanie jednego na drugie jest banalne.

Argument z performancem, to jest fakt, że wyjątki są cięższe.

Podsumowanie

Więc ja mógłbym podsumować całą tą debatę tak:

feature Result<> Wyjątki
performance best worse
współpraca z listenerami bibliotek tylko jeśli autor biblioteki to przewidzi zawsze
klamrowość calli* gwarantowana niegwarantowana
kiedy można użyć tylko w klamrowych funkcjach wszędzie (ale obłsuga jest trudniejsza)
niefajne konsekwencje chyba żadne rzucanie wyjątków po wyjsciu z funkcji

* przez "klamrowość calli", mam na myśli to że funkcja musi wywołać wszystkie listenery przed zwróceniem wyniku (i po zwróceniu nie wywoła ich już).

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2
Riddle napisał(a):

Tylko patrz, to o czym mówisz, tak na prawdę to nie jest dysputa Result<> vs wyjątki. To tak na prawdę jest dysputa "czy biblioteka powinna robić coś po tym jak zwróci swoj wynik". Czyli tak jakby, czy biblioteka jest klamrowa (innymi słowy, zaczyna się, robi coś, i zwraca. i po zwróceniu już nic nie robi). To co jest niejawne, to to, że skorzystanie z Result<> wymusza zwrócenie wartości z wyniku wszystkich listenerów, i potem się już nie zawołają (czyli w pewnym sensie, jak funkcja korzysta z Result<>, to mamy pewnośc że jest klamrowa). Z wyjątkami takiego wymuszenia nie ma, bo można je rzucić skądkolwiek - i to rodzi problemy, takie własnie jak to że nie wiadomo gdzie zapiąć try/catch, i problemy z asynchronicznym wołaniem, i wszystko o czym mówiłeś.

Rozumiem, że przez klamrowość rozumiesz po prostu wywołanie synchroniczne (w opozycji od asynchronicznego).

Więc jak się dokopać do serca problemu, to wychodzi na to że tak na prawdę nie jest tak że Result dobry, wyjątki złe; tylko wychodzi po prostu na to, że podoba Ci się pomysł, że biblioteka nie powinna robić nic dodatkowego po zwróceniu (a przynajmniej nic, co wymaga obsługi w kodzie), to miałem na myśli we wstępie że bronisz klamrowości bibliotek. I ja się z tym absolutnie zgadzam - to jest jaknajbardziej super cecha bibliotek. Biblioteki które takie nie są, są mega słabe, i zgadzam się ze wszystkimi arugmentami które wyciągnąłeś. Idealnie by było, gdyby wszystkie biblioteki takie były.

Zupełnie nietrafione, bo korzystam bardzo chętnie i często z bibliotek z interfejsem asynchronicznym. Tak długo jak oparte jest to o IO, ReactiveStreams i podobne rozwiązania (czyli AsyncResult<T>) korzysta się z tego dobrze. Jak ktoś wpierniczy w to wyjątki, to faktycznie bywa zabawnie, ale przeważnie jak widzę, że coś zwraca Mono<T> to raczej można polegać, że wyjątek jeśli już wystąpi to będzie opakowany w Mono. Tak samo jak metoda zwracająca Optional raczej nie zwróci null (choć wiadomo - gwarancji, że autorowi się coś omskło, albo że nie ma uszkodzenia mózgu nie ma).

Przy czym właśnie Mono<T> i ReactiveStreams javowe są niestety niedorobione, bo błąd musi być Exceptionem - niepotrzebna zbrodnia. Ale przynajmnie nie są przekazywane przez throw, tylko jako normalny rezultat.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):
Riddle napisał(a):

Tylko patrz, to o czym mówisz, tak na prawdę to nie jest dysputa Result<> vs wyjątki. To tak na prawdę jest dysputa "czy biblioteka powinna robić coś po tym jak zwróci swoj wynik". Czyli tak jakby, czy biblioteka jest klamrowa (innymi słowy, zaczyna się, robi coś, i zwraca. i po zwróceniu już nic nie robi). To co jest niejawne, to to, że skorzystanie z Result<> wymusza zwrócenie wartości z wyniku wszystkich listenerów, i potem się już nie zawołają (czyli w pewnym sensie, jak funkcja korzysta z Result<>, to mamy pewnośc że jest klamrowa). Z wyjątkami takiego wymuszenia nie ma, bo można je rzucić skądkolwiek - i to rodzi problemy, takie własnie jak to że nie wiadomo gdzie zapiąć try/catch, i problemy z asynchronicznym wołaniem, i wszystko o czym mówiłeś.

Rozumiem, że przez klamrowość rozumiesz po prostu wywołanie synchroniczne (w opozycji od asynchronicznego).

No nie koniecznie, bo biblioteki z interfejsem asynchronicznym też mogą być klamrowe.

Możemy zdefiniować że biblioteka jest klamrowa, wtedy i tylko wtedy gdy rzucane wyjątki da się przemapować 1:1 na Result.

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
0
jarekr000000 napisał(a):

Akurat mówimy o projektowaniu API. Między innymi po to, aby w kolejnych wersjach było mniej powodów do naprawiania/zmieniania.

N tak, tylko jak masz klientów, to trzeba uwzględniać ich zadnie i wymagania.

I jasne, fajnie byłoby mieć uniwersalne podejście, i pozbyć się zwracania wyników przez wyjątki, tylko ja już chyba po prostu nie mam siły edukować.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

@Riddle:

Dla przykładu: chcemy mieć aplikację, która umie podmienić minki w stringach w jakimś pliku typu markdown, na obrazki. ściągamy parser markdown który ma wizytor który łazi po AST, i umie też podmieniać node'y. Nasza aplikacja wczytuje z bazy mapę minek na obrazki, iterujemy parserem po treści, i natrafiamy na minkę która ma niepoprawny format, więc chcemy jakoś dać znać logice że trafiliśmy na coś takiego, żeby poinformowała usera. Jeśli mamy taki listener, to nie widzę sposobu jak z niego zwrócić Result<T>. Wygląda na to że wyjątek to jedyne sensowne wyjście (oprócz oczywiście zapisania zmiennej i potem odczytania jej, tylko wtedy też musielibyśmy powiedzieć jakoś listenerowi żeby przestał parsować, bo to już nie ma sensu, taki break, niektóre parsery mają taką funkcje).

Nie wystarczy przerzucać jakimś "Contextem" z dodatkowymi informacjami? a jeżeli chcesz zastopować dalsze procesowanie to również jakiś Context z informacją nt. dalszego przetwarzania?

np. podejście podobne do tego, co jest w jsie event.stopPropagation()

PS: na ten moment użyłem object jako, ale tam również bym dał jakiś Context który by mógł mieć podpięte dodatkowe informacje z interceptorów.

Kopiuj
var root = new Node("Root");
root.Add(new Node("Item1"));
root.Add(new Node("Item2")
    .Add(new Node("Item2_Item1")
        .Add(new Node("Item2_Item2_Item1"))));

var interceptors = new List<Func<Node, ParsingContext, object>>();

interceptors.Add((node, ctx) =>
{
    if (node.Name == "Item2_Item2_Item1")
    {
        Console.WriteLine("got it");
        ctx.Stop = true;
    }

    return 5; 
});

WalkThruNodes(root, interceptors);
Kopiuj
public static void WalkThruNodes(Node root, List<Func<Node, ParsingContext, object>> Interceptors)
{
    var context = new ParsingContext();
    var queue = new Stack<Node>();
    queue.Push(root);

    while (queue.Count > 0)
    {
        var current = queue.Pop();

        foreach (var interceptor in Interceptors)
        {
            interceptor(current, context);
        }

        if (context.Stop)
            return;

        Console.WriteLine(current.Name);

        foreach (var node in current.SubNodes)
            queue.Push(node);
    }
}

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.