Czy używasz TDD? oraz pytania do TDD.

0

Cześć!
Słuchając wykładów Uncle Boba natrafiłem na TDD. Bob w dużym skrócie pokazywał co to jest, i pisał stos z użyciem TDD. Liznąłem tematu i mam pytania.
Polecicie jakieś materiały tłumaczące co to dokładnie jest i jak tego używać?
Dlaczego warto stosować TDD, i czy się opłaca biorąc pod uwagę czas poświęcony na tworzenie testów?
Czy używacie TDD? [Ankieta]
Czy TDD używać dopiero od jakiegoś poziomu w programowaniu, czy to nie ma znacznia?

Z tego co widzę to TDD może mieć duże zalety, ale dopiero wtedy gdy dobrze ta technika zostanie opanowana.

5

Materiałów nie polecę.
Zaczynanie od pisania testów (czy TDD czy nie) w wielu sytuacjach potrafi znacząco skrócić czas implementacji oraz debugowania, więc w ostatecznym rozrachunku jest to oszczędność czasu. Ale nie każdy kod jest sens tworzyć w ten sposób.
Poziom startowy nie ma znaczenia, nawet do kalkulatora w konsoli można pisać testy.

6

Ja zawsze zaczynam od testu, nawet jeżeli jest to crud, tylko wtedy test jest integracyjny, a nie jednostkowy. Jeżeli mam moduł/komponent/whatever aplikacji, który zawiera logikę to dodatkowo testuję go jednostkowo. Niemniej zawsze zaczynam od testu.

Przykład z życia. Musiałem napisać kilka funkcji AWS Lambda (w C#) triggerowanych przez API Gateway. Zamiast testować funkcję wysyłając requesty HTTP Postmanem to napisałem sobie testy w xUnit, które robią to samo. Dzięki temu testy i kod aplikacji mam w jednym miejscu.

2

"Test Driven Development: By Example" ew. po PL - "TDD. Sztuka tworzenia dobrego kodu" - jak przeczytasz to będziesz wszystko wiedział

1

Mnie TDD zawsze spowalnia. Próbowałem takiego podejścia i się nie sprawdziło. Źle mi z tym jak się kod nie kompiluje. Chyba, że miałem złe podejście.

2
Suchy702 napisał(a):

Polecicie jakieś materiały tłumaczące co to dokładnie jest i jak tego używać?

Z książek to "TDD. Sztuka tworzenia dobrego kodu" by Kent Beck, oraz "Growing object-oriented software guided by tests".

Dlaczego warto stosować TDD, i czy się opłaca biorąc pod uwagę czas poświęcony na tworzenie testów?

TDD pozwala wytworzyć dużo bardziej rzetelne testy niż "takie zwykłe" jak się je pisze. Nie wspominając o tym, że stosując TDD dostaje się 99-100% coverage'a "za darmo", i to takie zdrowe coverge.

Czy TDD używać dopiero od jakiegoś poziomu w programowaniu, czy to nie ma znacznia?

Kiedy uczę ludzi (amatorów) to od razu zaczynam od TDD, i większość ludzi przyjmuje to naturalne. Wbrew pozorom zaawansowanym programistom ciężko to pokazać, dlatego że już mają dużo wyrobionych złych nawyków.

Z tego co widzę to TDD może mieć duże zalety, ale dopiero wtedy gdy dobrze ta technika zostanie opanowana.

No nie.

Albo praktykujesz TDD, albo nie (i tylko Ci się wydaje że to robisz). W piewszym przypadku jest to pomocne, w tym drugim oszukujesz sam siebie.

2

IMO, TDD widzę w takim sensie, że staram się pisać kod TESTOWALNY; a szczegóły techniczne, czy zaczynam od testu, są mniej istotne.

0

Idealne TDD takie jak u Boba to utopia, przynajmniej z dwóch powodów.

Po pierwsze to żaden programista nie będzie pisał kodu w taki sposób, żeby kod spełniał minimum konieczne do przechodzenia testów. Z test -> kod -> test -> kod -> .... robi się test -> (kod1) -> test -> (kod2 + wiedza z kod1), bo pisząc kod zawsze mamy w głowie to jak będzie się rozwijał z prostych powodów: jesteśmy leniwi i totalne przepisanie wszystkiego co każdy nowy test to duży narzut umysłowy a przecież jesteśmy leniwi

Po drugie to szeroko znana wiedza na temat TDD przyjmuje jako aksjomat, że mamy kod i określone testy do niego, co nie jest prawdą. Kod to graf i bywa używany w różnych kontekstach. Przykładowo chcemy zaimplementować endpoint /doSomething. Klepanie kodu zaczynamy od napisania testów funkcjonalnych/intregracyjnych/whatever, które uderzają w ten endpoint. Równolegle powstają klasy/funkcje np. A wołane przez kontroler /doSomething i B wołane przez A, których nie testujemy w izolacji, bo nie chcemy betonować się do implementacji; obchodzi nas tylko i wyłącznie /doSomething. Po jakimś czasie piszemy jakiś nowy endpoint, który w jakiś sposób woła A i/lub B. Ponieważ te klasy/funkcje są wołane w dwóch różnych kontekstach to chcemy je przetestować w izolacji, żeby jasno powiedzieć za co odpowiadają a za co nie. Co robić w takiej sytuacji: wywalić cały kod z ich metod i napisać go od nowa, póki testy endpointu /doSomething oraz nowe testy nie zaczną przechodzić? Brzmi fajnie, ale wracamy do sytuacji z pierwszego punktu: brzmi fajnie, ale komu by się chciało

0
slsy napisał(a):

Idealne TDD takie jak u Boba to utopia, przynajmniej z dwóch powodów.

Po pierwsze to żaden programista nie będzie pisał kodu w taki sposób, żeby kod spełniał minimum konieczne do przechodzenia testów. Z test -> kod -> test -> kod -> .... robi się test -> (kod1) -> test -> (kod2 + wiedza z kod1), bo pisząc kod zawsze mamy w głowie to jak będzie się rozwijał z prostych powodów: jesteśmy leniwi i totalne przepisanie wszystkiego co każdy nowy test to duży narzut umysłowy a przecież jesteśmy leniwi

Not true, ja tak piszę praktycznie wszystkie programy - prywatne i w pracy. Tak również napisałem edytor na 4p.

Dla mnie każdy nowy test to mniej więcej 1-10 sekund. Oczywiście zdarzają się case'y, gdzie trzeba przemyśleć dokładnie spodziewany wynik, ale to raczej wyjątki.

PS: I uprzedzam od razu pytania: "ale to pewnie tylko unity które nic nie testują", "ale pewnie się wykonują 45 minut", "ale to pewnie nic nie warte testy", "ale to pewnie same mocki", bla. bla. bla. Odsyłam do książek, które wymieniłem wyjżej, tam jest wszystko opisane. Wyszła książka "You don't know JS", czekam aż wyjdzie książka "You can't write tests".

Po drugie to szeroko znana wiedza na temat TDD przyjmuje jako aksjomat, że mamy kod i określone testy do niego, co nie jest prawdą. Kod to graf i bywa używany w różnych kontekstach. Przykładowo chcemy zaimplementować endpoint /doSomething. Klepanie kodu zaczynamy od napisania testów funkcjonalnych/intregracyjnych/whatever, które uderzają w ten endpoint. Równolegle powstają klasy/funkcje np. A wołane przez kontroler /doSomething i B wołane przez A, których nie testujemy w izolacji, bo nie chcemy betonować się do implementacji; obchodzi nas tylko i wyłącznie /doSomething. Po jakimś czasie piszemy jakiś nowy endpoint, który w jakiś sposób woła A i/lub B. Ponieważ te klasy/funkcje są wołane w dwóch różnych kontekstach to chcemy je przetestować w izolacji, żeby jasno powiedzieć za co odpowiadają a za co nie. Co robić w takiej sytuacji: wywalić cały kod z ich metod i napisać go od nowa, póki testy endpointu /doSomething oraz nowe testy nie zaczną przechodzić? Brzmi fajnie, ale wracamy do sytuacji z pierwszego punktu: brzmi fajnie, ale komu by się chciało

To się sprowadza do nie odpowiedniego podzielenia kodu na moduły.

To o czym piszesz to zjawisko w którym masz monolit, ale próbujesz go testować jak graf. Nie ma tak. Albo masz kod jak graf i testujesz go jak graf, albo masz monolit i testujesz go jak monolit. There is no middle ground.

3
Riddle napisał(a):

Not true, ja tak piszę praktycznie wszystkie programy - prywatne i w pracy. Tak również napisałem edytor na 4p.

Załóżmy, że piszesz parser JSONa. Uznałeś, że pierwszym przypadkiem (albo jednym z pierwszych) będzie sparsowanie prostego obiektu np. {"a": "3"}. Następnym krokiem będą zagnieżdżone obiekty tj. {"a": {"b": 3}}. Okazuje się, że pierwszy przypadek zaklepałeś jako prosty parser w stylu języka regularnego tj. jedzie od lewej do prawej i tyle, bo jest najprościej i najszybciej a testy przechodzą. Niestety do drugiego przypadku trzeba użyć parsera do gramatyki bezkontekstowej; potrzebujemy jakiegoś stosu lub rekurencji i okazuje się, że pierwotny kod musi pójść na śmietnik. Pytanie: czy napisałbyś taki kod wiedząc, że nie ma on nic wspólnego z ostateczną wersją? Właśnie tak wygląda wspomniany już wcześniej przykład ze stosem: fajnie, ale bez sensu

Riddle napisał(a):

To o czym piszesz to zjawisko w którym masz monolit, ale próbujesz go testować jak graf. Nie ma tak. Albo masz kod jak graf i testujesz go jak graf, albo masz monolit i testujesz go jak monolit. There is no middle ground.

Za bardzo nie rozumiem twojej nomenklatury tj. czym jest monolit w tym kontekście. Ja zakładam, że kod może się rozwinąć w dowolnym kierunku i część, która nie była przetestowania w izolacji może się taką stać. Przykładowo mamy REST API, ale ktoś uznał, że chce gRPC albo GraphQL. Logika generalnie jest taka sama, różni się tylko kontroler. To co możemy zrobić:

  • traktujemy kontroler jako osobny moduł i testujemy z mockami. Działa, ale mało testujemy
  • implementujemy takiego GraphQLa poprzez użycie RESTa. Stare testy testują RESTa jak należy. Nowe testy testują jakieś proste przypadki, żeby upewnić się, że ta prosta translacja REST -> GraphQL działa. Jest ok, ale może być problem z performancem. Dodatkowo, możemy mieć w planach wywalenie RESTa (np. za rok a GraphQL zostanie) przez co w takim podejściu będziemy mieli na wieczność kod, który nie jest potrzebny
  • piszemy osobne zestawy testów dla obu protokołów: zadziała, ale upierdliwe i spada nam zwinność, bo każda zmiana w jednym miejscu afektuje dwa flow. Do tego te testy będą bardzo podobne
  • piszemy testy dla wspólnej logiki. Testy RESta zostają lub zostają uszczuplone, bo ciężar został przeniesiony niżej. Nowe testy GraphQL są prostsze niż stare dla RESTa, bo teraz logika jest wspólna i testujemy logikę a nie endpointy HTTP.
0
slsy napisał(a):
Riddle napisał(a):

Not true, ja tak piszę praktycznie wszystkie programy - prywatne i w pracy. Tak również napisałem edytor na 4p.

Załóżmy, że piszesz parser JSONa. Uznałeś, że pierwszym przypadkiem (albo jednym z pierwszych) będzie sparsowanie prostego obiektu np. {"a": "3"}. Następnym krokiem będą zagnieżdżone obiekty tj. {"a": {"b": 3}}. Okazuje się, że pierwszy przypadek zaklepałeś jako prosty parser w stylu języka regularnego tj. jedzie od lewej do prawej i tyle, bo jest najprościej i najszybciej a testy przechodzą. Niestety do drugiego przypadku trzeba użyć parsera do gramatyki bezkontekstowej; potrzebujemy jakiegoś stosu lub rekurencji i okazuje się, że pierwotny kod musi pójść na śmietnik. Pytanie: czy napisałbyś taki kod wiedząc, że nie ma on nic wspólnego z ostateczną wersją? Właśnie tak wygląda wspomniany już wcześniej przykład ze stosem: fajnie, ale bez sensu

Tak, zrobiłbym tak. Błąd który popełniasz to nie zrozumienie zasady "minimal code pass the test".

A minimum które wystarczy żeby przeszedł Twój przypadek to jest return singletonMap("a", 3);.

Widać że nie stosujesz TDD bo myślisz o zbyt dużych zmianach implementacji w porównaniu do zbyt małej ilości testów.

Riddle napisał(a):

To o czym piszesz to zjawisko w którym masz monolit, ale próbujesz go testować jak graf. Nie ma tak. Albo masz kod jak graf i testujesz go jak graf, albo masz monolit i testujesz go jak monolit. There is no middle ground.

Za bardzo nie rozumiem twojej nomenklatury tj. czym jest monolit w tym kontekście. Ja zakładam, że kod może się rozwinąć w dowolnym kierunku i część, która nie była przetestowania w izolacji może się taką stać. Przykładowo mamy REST API, ale ktoś uznał, że chce gRPC albo GraphQL. Logika generalnie jest taka sama, różni się tylko kontroler. To co możemy zrobić:

  • traktujemy kontroler jako osobny moduł i testujemy z mockami. Działa, ale mało testujemy
  • implementujemy takiego GraphQLa poprzez użycie RESTa. Stare testy testują RESTa jak należy. Nowe testy testują jakieś proste przypadki, żeby upewnić się, że ta prosta translacja REST -> GraphQL działa. Jest ok, ale może być problem z performancem. Dodatkowo, możemy mieć w planach wywalenie RESTa (np. za rok a GraphQL zostanie) przez co w takim podejściu będziemy mieli na wieczność kod, który nie jest potrzebny
  • piszemy osobne zestawy testów dla obu protokołów: zadziała, ale upierdliwe i spada nam zwinność, bo każda zmiana w jednym miejscu afektuje dwa flow. Do tego te testy będą bardzo podobne
  • piszemy testy dla wspólnej logiki. Testy RESta zostają lub zostają uszczuplone, bo ciężar został przeniesiony niżej. Nowe testy GraphQL są prostsze niż stare dla RESTa, bo teraz logika jest wspólna i testujemy logikę a nie endpointy HTTP.

Nie mam czasu żeby to tłumaczyć tutaj, ale jeszcze bardziej pokazałeś że nie wiesz jak odpowiednio dzielić kod na moduły.

Wszystkie problemy które opisałeś da się rozwiązać, jeśli tylko stosuje się TDD w odpowiedni sposób. Możesz nie wiedzieć jaki to sposób, jeśli nie stosujesz TDD, ale to nie znaczy że takiego nie ma.

Już kończę rozmowę tutaj. Przeczytaj dwie książki które podlinkowałem, podałeś dobre pytania, ale na każde z nich jest odpowiedź, jeśli tylko masz wystarczająco dużo doświadczenia w tej metodyce. Mi zajęło spokojnie z 4 lata praktyki, żeby się tego nauczyć.

Jeśli chcesz kontynuować temat, to załóż proszę wątek "Jak napisać parser JSON'a w TDD", to chętnie pomogę, pokażę tricki, wytłumaczę jak to można zrobić dobrze.

2
Riddle napisał(a):

A minimum które wystarczy żeby przeszedł Twój przypadek to jest return singletonMap("a", 3);.

Inne przypadki

{}
{"a": "b"}
{"a",} -> błąd
{"a": "b"}
{"a": "b", } -> błąd bo przecinek
{"a": "b", "a": "b"} -> błąd bo duplikat
{"a": } -> błąd bo nie ma niczego po :

IMO takiego kodu nie da się przetestować bez prawdziwego parsera.

Riddle napisał(a):

Wszystkie problemy które opisałeś da się rozwiązać, jeśli tylko stosuje się TDD w odpowiedni sposób. Możesz nie wiedzieć jaki to sposób, jeśli nie stosujesz TDD, ale to nie znaczy że takiego nie ma.

Już kończę rozmowę tutaj. Przeczytaj dwie książki które podlinkowałem, podałeś dobre pytania, ale na każde z nich jest odpowiedź, jeśli tylko masz wystarczająco dużo doświadczenia w tej metodyce. Mi zajęło spokojnie z 4 lata praktyki, żeby się tego nauczyć.

Jeśli chcesz kontynuować temat, to załóż proszę wątek "Jak napisać parser JSON'a w TDD", to chętnie pomogę, pokażę tricki, wytłumaczę jak to można zrobić dobrze.

Mógłbyś się bardzie wysilić? Argumenty w stylu "nie znasz się" i "to jest w książce" pasują do innego polskiego forum o tematyce technicznej.

Riddle napisał(a):

Nie mam czasu żeby to tłumaczyć tutaj, ale jeszcze bardziej pokazałeś że nie wiesz jak odpowiednio dzielić kod na moduły.

Mógłbyś rozwinąć? Podejście z rozdzielonym kodem pomiędzy logikiem i serwisem to chyba standard a ja mówię o takie sytuacji

1
slsy napisał(a):

IMO takiego kodu nie da się przetestować bez prawdziwego parsera.

Da się, myślę że żeby te przypadki testowe przeszły, wystarczyłoby 3-5 linijek kodu.

Mówię Ci: załóż wątek "Jak napisać parser JSON'a w TDD" to Ci pokażę. Tutaj nie zaśmiecajmy tego.

Sam napisałem kiedyś parser XML'a w taki sposób, iteratywnie dodając logikę i testy, i pierwsze 100 testów to był żart, a nie parser. Więć wiem co mówię - da się. Idzie szybko, bezpiecznie, jeden test za drugim, i na końcu masz minimum kodu spełniającego testy i 99-100% coverage.

Rozumiem, że może Ci być ciężko sobie to wyobrazić, bo jeszcze tego nie umiesz, ale to nie znaczy że metodyka TDD jest zła. Może po prostu Ty jeszcze jej nie umiesz.

Riddle napisał(a):

Mógłbyś rozwinąć? Podejście z rozdzielonym kodem pomiędzy logikiem i serwisem to chyba standard a ja mówię o takie sytuacji

Jeśli faktycznie je rozdzielisz to tak. Ale z opisu tego co mówisz, nie były faktycznie rozdzielone.

Wątek jest o tym czy używa się TDD i pytania na jego temat. Nie zamieniaj tego wątku w prywatę o parserze JSON'ów, załóż osobny wątek.

2

Nie lubię TDD, bo mimo to, że wiem co chcę osiągnąć i mogę to wymaganie wyrazić testem jednostkowym, to nie wiem jak chcę to zakodować i zawsze wolę sobie napisać kod. Po prostu uważam, żeby kod był testowalny tj. rozbity odpowiednio na klasy i metody oraz żeby metody zawsze zwracały wartość, nigdy void.

4
NeutrinoSpinZero napisał(a):

Nie lubię TDD, bo mimo to, że wiem co chcę osiągnąć i mogę to wymaganie wyrazić testem jednostkowym, to nie wiem jak chcę to zakodować i zawsze wolę sobie napisać kod.

Testy sprawdzają wejście i wyjście, więc nie ma znaczenia jak to zakodujesz, dopóki kontrakt (czy nawet sygnatura metody) się zgadza.

Po prostu uważam, żeby kod był testowalny tj. rozbity odpowiednio na klasy i metody oraz żeby metody zawsze zwracały wartość, nigdy void.

No i właśnie w tym testy pomagają. Do metod void (łatwo sensownych) testów nie napiszesz.

0
somekind napisał(a):

No i właśnie w tym testy pomagają. Do metod void testów nie napiszesz.

Potrzymaj mi piwo :P


[Fact]
public void Should_call_foo_method()
{
  ...
  myServiceMock.Verify(x => x.Foo()); // gdzie metoda Foo jest void
}
 

:D

2
somekind napisał(a):
NeutrinoSpinZero napisał(a):

Nie lubię TDD, bo mimo to, że wiem co chcę osiągnąć i mogę to wymaganie wyrazić testem jednostkowym, to nie wiem jak chcę to zakodować i zawsze wolę sobie napisać kod.

Testy sprawdzają wejście i wyjście, więc nie ma znaczenia jak to zakodujesz, dopóki kontrakt (czy nawet sygnatura metody) się zgadza.

Jak implementacja bierze dane z d.. zamiast z parametrów to trudniej napisać test jednostkowy. Przykład w Javie to sławne new Date() Czy teraz po nowemu LocalDateTime.now()

0

To jak stosowac TDD kiedy piszemy cos od nowa albo sami zostalo juz dobrze wyjasnione przez @Riddle. Ciekawi mnie co jesli pracujemy w dojrzalym projekcie i musimy dopisac nowy endpoint albo co gorsza zmodyfikowac jakis feature?

3

Jak implementacja bierze dane z d.. zamiast z parametrów to trudniej napisać test jednostkowy. Przykład w Javie to sławne new Date() Czy teraz po nowemu LocalDateTime.now()

Bierzesz i robisz TimeProvider i YOLO.

2
Seken napisał(a):

To jak stosowac TDD kiedy piszemy cos od nowa albo sami zostalo juz dobrze wyjasnione przez @Riddle. Ciekawi mnie co jesli pracujemy w dojrzalym projekcie i musimy dopisac nowy endpoint albo co gorsza zmodyfikowac jakis feature?

Piszesz test który failuje, dopisujesz cześć featurea, test przechodzi, powtórz.

3
Seken napisał(a):

To jak stosowac TDD kiedy piszemy cos od nowa albo sami zostalo juz dobrze wyjasnione przez @Riddle. Ciekawi mnie co jesli pracujemy w dojrzalym projekcie i musimy dopisac nowy endpoint albo co gorsza zmodyfikowac jakis feature?

To, że w projekcie wcześniej nie było testów nie ma znaczenia, zawsze można zacząć je wprowadzać przy okazji nowych ficzerów.

0
scibi_92 napisał(a):

Jak implementacja bierze dane z d.. zamiast z parametrów to trudniej napisać test jednostkowy. Przykład w Javie to sławne new Date() Czy teraz po nowemu LocalDateTime.now()

Bierzesz i robisz TimeProvider i YOLO.

Ale wtedy TimeProvider też jest z parametrów, żeby moc podmienić implementacje dla testów :p

0
NeutrinoSpinZero napisał(a):

żeby metody zawsze zwracały wartość, nigdy void.

Nie rozumiem dlaczego metody powinny zawsze cos zwracac. Przyklad z aplikacji ktora pisze. Mam Klase Fish, jest to rybka ktora plywa sobie w stawie. Oraz metode spoil_energy ktora zmienia jej atrybut energy ponieważ rybka żyjąc traci energię. Ta metoda nic nie zwraca, według ciebie powinna ona zwracać nowy obiekt Fish ze zmienioną wartością energy?

6
Suchy702 napisał(a):

Ta metoda nic nie zwraca, według ciebie powinna ona zwracać nowy obiekt Fish ze zmienioną wartością energy?

Dokładnie tak by było w idealnym, czysto funkcyjnym świecie

0
Suchy702 napisał(a):
NeutrinoSpinZero napisał(a):

żeby metody zawsze zwracały wartość, nigdy void.

Nie rozumiem dlaczego metody powinny zawsze cos zwracac. Przyklad z aplikacji ktora pisze. Mam Klase Fish, jest to rybka ktora plywa sobie w stawie. Oraz metode spoil_energy ktora zmienia jej atrybut energy ponieważ rybka żyjąc traci energię. Ta metoda nic nie zwraca, według ciebie powinna ona zwracać nowy obiekt Fish ze zmienioną wartością energy?

W obiektowym świecie możesz mieć metodę Fish.swim(): void. To by znaczyło że ona:

  1. Albo nic nie robi (co nie ma sensu :D)
  2. Albo zmienia stan ryby
  3. Albo nie zmiania stanu (obiekt jest immutable), tylko robi coś poza programem, np zapisuje plik, wysyła maila, robi request, robi sleep()a, etc.

Jak mówi @KamilAdam, w funkcyjnym świecie punkt 2. nie może być.
Okej, w funkcyjnym jednak nie może być funkcji która zwraca void.

2
KamilAdam napisał(a):
Suchy702 napisał(a):

Ta metoda nic nie zwraca, według ciebie powinna ona zwracać nowy obiekt Fish ze zmienioną wartością energy?

Dokładnie tak by było w idealnym, czysto funkcyjnym świecie

Nawet poza czysto funkcyjnym światem ta nowa rybka jest o wiele lepsza niż void. Void to straszna kupa mszcząca się na refaktoringach.

1
Riddle napisał(a):
Suchy702 napisał(a):
NeutrinoSpinZero napisał(a):

żeby metody zawsze zwracały wartość, nigdy void.

Nie rozumiem dlaczego metody powinny zawsze cos zwracac. Przyklad z aplikacji ktora pisze. Mam Klase Fish, jest to rybka ktora plywa sobie w stawie. Oraz metode spoil_energy ktora zmienia jej atrybut energy ponieważ rybka żyjąc traci energię. Ta metoda nic nie zwraca, według ciebie powinna ona zwracać nowy obiekt Fish ze zmienioną wartością energy?

W obiektowym świecie możesz mieć metodę Fish.swim(): void. To by znaczyło że ona:

  1. Albo nic nie robi (co nie ma sensu :D)
  2. Albo zmienia stan ryby
  3. Albo nie zmiania stanu (obiekt jest immutable), tylko robi coś poza programem, np zapisuje plik, wysyła maila, robi request, robi sleep()a, etc.

Jak mówi @KamilAdam, w funkcyjnym świecie punkt 2. nie może być.

W obiektowym swiecie punkt 2 tez jest slaby. Problem polega na tym, ze OOP w takiej np. javie to jest straszna patologia i najwiekszy syf, ktory niestety zostal skojarzony jako wzorzec obiektowosci.

0
Seken napisał(a):

W obiektowym swiecie punkt 2 tez jest slaby. Problem polega na tym, ze OOP w takiej np. javie to jest straszna patologia i najwiekszy syf, ktory niestety zostal skojarzony jako wzorzec obiektowosci.

Czyli ponieważ ktoś tworzy shit w javie, to znaczy że w obiektówce nie może być mutowalnych obiektów?

Słaby argument.

1

@Riddle:
Druga czesc mojej wypowiedzi to byla taka bardziej dygresja. Moim skromnym zdaniem mutowalnosci powinno sie unikac w wiekszosci przypadkow (to nie znaczy, ze nigdy nie stosowac!). Generuje to spaghetti, dziwne sporadic bugi itd. Dlaczego Twoim zdaniem rozwiazanie @jarekr000000 odnosnie zwrocenia nowej ryby jest gorsze? Juz nawet na etapie testow widac, ze Twoje rozwiazanie ze zmiana stanu jest ciezsze do przetestowania.

0
Seken napisał(a):

@Riddle:
Druga czesc mojej wypowiedzi to byla taka bardziej dygresja. Moim skromnym zdaniem mutowalnosci powinno sie unikac w wiekszosci przypadkow (to nie znaczy, ze nigdy nie stosowac!).

No, zgadzam się.

Generuje to spaghetti, dziwne sporadic bugi itd. Dlaczego Twoim zdaniem rozwiazanie @jarekr000000 odnosnie zwrocenia nowej ryby jest gorsze? Juz nawet na etapie testow widac, ze Twoje rozwiazanie ze zmiana stanu jest ciezsze do przetestowania.

Zależy gdzie. Są miejsca w którym praca ze stanem jest bardziej sensowna niż nie. Np w GUI, setVisible(true),setVisible(false) jest dużo lepszym rozwiązaniem niż .getVisible()/getHidden() który zwraca nowy komponent.

Także to zależy. W logice, w corze, w domenie biznesowej, jak najbardziej stan jest ultimate złem. Ale nie wszędzie. W bazach danych, w gamedev'ie, w GUI właśnie.

To nie jest takie czarno białe. Na tak wysokim poziomie abstrakcji cięzko znaleźć "złotą zasadę" która zawsze działa.

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