O pchaniu wszędzie polimorfizmu

5

Jakiś czas temu była dyskusja pod którymś postem na mikro na temat tego, czy control flow ma być explicite, czy też należy zastępować wszelkie conditionale polimorfizmem. Ktoś twierdził, że polimorfizm podnosi jakość kodu, że kod bez polimorfizmu jest write-only, a kod z polimorfizmem może być fajny. Co więcej nawet nie jest to element debaty OOP vs. FP, gdyż polimorfizm miał być także podstawą FP.

Cóż, wygląda na to, że nawet twórcy języków programowania już nie zgadzają się z takim podejściem. Jeśli chodzi o C#: https://github.com/dotnet/csharplang/discussions/3107 W 2020 roku ówczesny członek teamu C# napisał wprost: przechodzą "from internal dispatch to external dispatch", gdzie "internal dispatch to jest po ichniemu polimorfizm, a "external dispatch" to jest switch.

Moje zdanie pozostaje niezmienne: polimorfizm jest OK, potrzebny, przydatny, pod warunkiem, że używa się go z umiarem. Polimorfizm ma swoje koszta, po prostu zaciemnia kod i sprawia, że control flow staje się trudny do prześledzenia, przez co kod staje się nieczytelny.

Co więcej, nadużywanie polimorfizmu może stwarzać problemy z logiką. Np. pamiętam kiedyś próbowałem zrobić hobbystycznie grę i podszedłem do tego obiektowo, miałem tablicę Efektów (buffy, debuffy i inne takie), na interfejsie IEffect była metoda Apply, no i mieliśmy foreach(var effect in effects) effect.Apply(). Stwarzało to liczne trudności. Przykładowo: Mamy obliczanie obrażeń. Na obliczanie obrażeń wpływa, czy broniący się blokuje oraz czy atakujący ma buffa pozwalającego częściowo ominąć zbroję przeciwnika. Pokazało się, że Apply'owanie ich obu po kolei nie ma sensu. Nie wchodząc w szczegóły, należało użyć zupełnie innego wzoru arytmetycznego jeśli oba buffy wpływały na wynikowe obrażenia i zupełnie innego wzoru, jeśli tylko jeden z nich wpływał.

Nie działało:

var damage = blabla;
foreach(var effect in Effects)
    damage = effect.Apply(damage);

Działało:

var damage = blabla;
if(attacker.ArmorBypass != null && defender.Block == null)
    damage = /*wzór*/
else if(attacker.ArmorBypass == null && defender.Block != null)
    damage = /*inny wzór*/
else if(attacker.ArmorBypass != null && defender.Block != null)
    damage = /*jeszcze inny wzór, który jest RÓŻNY od po prostu applyBlock(applyArmorBypass(damage))*/

Co gorsza, używanie polimimorfizmu ukryło błąd. Gdyby cała logika liczenia obrażeń od początku była w funkcji liczącej obrażenia, a nie rozkichana po klasach implementujących buffy i debuffy, to dużo trudniej byłoby nie zauważyć problemu, że się wzory matematyczne nie komponują.

Ktoś powie, "a co jeśli chcesz dodać kolejnego buffa, bez polimorfizmu musisz zmieniać wszystkie metody, które liczą cokolwiek, na co ten buff ma mieć wpływ". Bardzo dobrze, tak właśnie ma być - bo wtedy i tylko wtedy od razu widać, czy nowy buff nie psuje przypadkiem już istniejących buffów / debuffów, których funkcjonalność się zazębia.

Oczywiście nie wszystko także jest sens robić switchami i ifami. Polimorfizm była potrzebny. Ale polimorfizmu używać należy wtedy, jeśli się musi, a domyślnie pisać jawną logikę, nie na odwrót.

Co do języków funkcyjnych, nie wiem. Haskell oczywiście ma polimorfizm, są typeclassy. Rust jest już może mniej funkcyjny, ale bardziej funkcyjny jednak niż C#, Rust ma traity. Jednak zarówno Haskell, jak i Rust mają sumy rozłączne (algebraiczne typy danych). WYDAJE MI SIĘ, z naciskiem na "wydaje mi się", że w Ruście czy Haskellu polimorfizmu za pomocą typeclass czy traitów używa się tylko od czasu do czasu, natomiast absolutnie nikt nie uważa za zdrożne pisania wyrażeń switchopodobnych operujących na tych algebraicznych typach danych.

Przykładowo, gdybyśmy w Haskellu mieli kod tego rodzaju:

data Expression = Addition Expression Expression | Substraction Expression Expression | Value Int

compute :: Expression -> Int
compute (Addition e1 e2) = (compute e1) + (compute e2)
compute (Substraction e1 e2) = (compute e1) - (compute e2)
compute (Value val) = val

To chyba nikt nie uważałby takiego kodu za zdrożny i nie kazał zamienić tego na jakieś typeclassy, nawet jeśli można?

I analogiczne w Ruście (W Ruście to nawet mielibyśmy tutaj match, który działa podobnie do C#-powego switch) - też chyba każdy uważałby taki kod za OK i nie kazałby tego przepisać na polimorfizm z użyciem traitów.

Ale tutaj bardziej się pytam, niż twierdzę - nie znam prawie w ogóle 'dobrych praktyk' w tamtych językach, więc w sumie nie wiem, czy jest tam presja, zeby pchać wszędzie polimorfizm, czy też kod taki jak wyżej byłby OK?

2

Ale tutaj bardziej się pytam, niż twierdzę - nie znam prawie w ogóle 'dobrych praktyk' w tamtych językach, więc w sumie nie wiem, czy jest tam presja, zeby pchać wszędzie polimorfizm, czy też kod taki jak wyżej byłby OK?

W kontekscie Haskella, to taki kod jest jak najbardziej ok

1
YetAnohterone napisał(a):

Co do języków funkcyjnych, nie wiem. Haskell oczywiście ma polimorfizm, są typeclassy. Rust jest już może mniej funkcyjny, ale bardziej funkcyjny jednak niż C#, Rust ma traity. Jednak zarówno Haskell, jak i Rust mają sumy rozłączne (algebraiczne typy danych). WYDAJE MI SIĘ, z naciskiem na "wydaje mi się", że w Ruście czy Haskellu polimorfizmu za pomocą typeclass czy traitów używa się tylko od czasu do czasu, natomiast absolutnie nikt nie uważa za zdrożne pisania wyrażeń switchopodobnych operujących na tych algebraicznych typach danych.

Nie tylko w Ruscie czy Haskellu. Można narzekać na Oderskiego iż robi dziwne rzeczy ze Scalą, ale słyszałem plotę iż powiedział iż w Scali nie ma sensu implementować wizytatora bo przecież mamy dobry pattern matching. Szkoda tylko iż sumy typów były takie kulawe w Scali2. No ale teraz jest Scala 3 i wreszcie wygląda to po ludzku

I tak, zgadzam się iż dobry pattern matching jest w stanie w wielu miejscach zastąpić polimorfizm. Ja ma skręt do hobbistycznego pisania parsarów i 15 lat temu jak to próbowałem robić zgodnie ze sztuką na wizytatorze to była to rzeźnia. Na pattern matchingu jest o wiele prościej. Mimo iż jest to niezgodne z doktryną Javy. Przynajmniej Javy sprzed 10 lat. Bo teraz już mnie średnia interesuje co mówią liderzy community Javy

Pytanie czy istnieje gdzies jakaś granica. Np pattern matching jest dobry do 20 przypadków a powyżej jest już zbyt zagmatwany i lepiej użyć czegoś innengo. Przyznam szczerze iż jak pisałem parser dla WhiteSpace'a (tego jeżyka ezoterycznego) to miałem pattern matching powyżej 20 przypadków i wyglądało to średnio.
No ale jeśli uda nam się zbudowac zagnieżdzoną Sumę typów to będziemy mieć to będziemy mieć pattern machong w pattern machingu i znów można rozbić to na kilka metod tak żeby żadna nie miałą więcej niż 20 czy 10 przypadków

3

@YetAnohterone Czytam Twój post... i na moje, to po prostu powiedziałeś: "Jeśli używa się czegoś nieodpowiednio i bez umiaru, to wynik jest słaby". No i w sumie to się zgadzam z tym.

YetAnohterone napisał(a):

Polimorfizm ma swoje koszta, po prostu zaciemnia kod i sprawia, że control flow staje się trudny do prześledzenia, przez co kod staje się nieczytelny.

Zgadzam się że polimorfizm ma pewne koszta, ale nie zgodzę się że control flow staje się trudny do prześledzenia przez co kod staje się nieczytelny.

Np. jak korzystasz z List<String> list; list.add("text"); i nie wiesz dokładnie czy to jest LinkedList czy ArrayList, to ten kod staje się trudniejszy do prześledzenia? No raczej nie - można by argumentować, że kod jest bardziej elastyczny i prostszy do zrozumienia wtedy (mniej szczegółów trzeba zmieścić w głowie).

Problem o którym mówisz, czyli to że ktoś dowala wszędzie niepotrzebne abstrakcje i polimorfizm - to jest prawdziwy problem. Ale to nie jest problem z polimorfizmem, tylko ze słabym designem softu (który występuje nie ważne czy masz polimorfizm czy nie). Jakiś nieudany experyment, brak wiedzy, brak wystarczających iteracji, brak dobrych testów, etc.

Chcąc zbudować dobre abstrakcje musisz znać dobrze domenę w której pracujesz oraz mieć use-case'y pod ręką. Bez tego nawet najlepszy programista wytworzy czasem słabe abstrakcje, i efekt będzie podobny. Sam wiele razy wszedłem do jakieś aplikacji - stwierdziłem "nic z tego nie rozumiem", i chciałem nałożyć abstrakcje na to i wynik był podobny - jeszcze większe zaciemnienie. Odpowiedzialnym wyjściem wtedy jest zrobić revert.

2
Riddle napisał(a):

@YetAnohterone Czytam Twój post... i na moje, to po prostu powiedziałeś: "Jeśli używa się czegoś nieodpowiednio i bez umiaru, to wynik jest słaby". No i w sumie to się zgadzam z tym.

Istnieje szkoła, która twierdzi, że należy starać się zastępować wszelkie instrukcje warunkowe polimorfizmem. Polemizuję z tą właśnie szkołą.

1
YetAnohterone napisał(a):

@Riddle: A masz chociażby tutaj: https://codeopinion.com/openclosed-principle-violation/ podobno "conditional code" to jest smell, ponieważ łamie open/closed principle. Co prawda przykład tam podany jest dość skrajny, ale samo twierdzenie jest ogólniejsze od przykładu.

To moim zdaniem jest złe źródło informacji i Open/Close.

Czytamy tam:

Out of all the SOLID principles, I’ve found this one to causes the most confusion and is the hardest to identify for developers. When following any of the SOLID principles, they all have a bit of cross over that help you identify when violating one of them. Violating Open/Close principle usually means you are also violating Single Responsibility. Here are a few smells/tips to for identifying when you might be violating Open/Closed principle:

  • Conditional Code
  • Multiple unit tests due to multiple execution paths
  • Violation of Single Responsibility

No sorry, ale to jest stek bzdur. Autor tego artykułu nie zrozumiał Open/Close, zastosował coś innego w swoim kodzie, nie wyszło mu, i teraz wypisuje takie głupoty.

Open/Close tak na prawdę jest bardzo prosty. Ta zasada mówi o tym, że jeśli masz jakiejś miejsce Foo w kodzie, do którego często dodajesz nowe rzeczy, ale w sposób który w gruncie rzeczy nie zmienia struktury Foo; to wprowadzasz tam zmiany które nie są konieczne. Zmian tych można by uniknąć, sprawiając że Foo będzie otwarty na rozszerzenie (dzięki polimorfizmowi) i zamknięty na modyfikację (przez co nie będzie zmian w kodzie, mniej konfliktów, mniej edycji, łatwiejsze rozumienie o historii edycji Foo), etc. Ale O/C nie mówi że trzeba to robić wszędzie. Nie mówi nawet że musisz to robić; a jedynie że jeśli to zrobisz, to Twoje życie jako programisty tej klasy będzie trochę łatwiejsze.

Na pewno conditional code to nie jest code smell. W niektórych miejscach conditional stwarza okazję do wydzielenia tego; ale code smell na 1000% to na pewno nie jest.

2

Polimorfizm jest problematyczny, bo ludzie traktowali go jako święty graal programowania, gdzie to technika jak każda inna, która ma swoje wady i zalety. IMO należy podziękować Uncle Bobowi i innym szarlatanom za to co się wydarzyło

Ale polimorfizmu używać należy wtedy, jeśli się musi, a domyślnie pisać jawną logikę, nie na odwrót.

Subclassing to łatwe dodanie nowej implementacji kosztem ciężkiego utrzymania zmian po stronie użycia. Sum typy mają zupełnie inaczej, bo łatwo dodać nowego ifa w jednym miejscu użycia. Te dwa rozwiązania mają inne wady i zalety i po prostu trzeba do tego podchodzić z głową

IMO polimorfizm ma ten problem, że jak masz dobrą abstrakcję to jest super. Problemem jest jej znalezienie. Sum typy zawsze działają i są proste do zrozumienia i zaprojektowania, ale jest takie uczucie, że mogłoby być lepiej

Ktoś powie, "a co jeśli chcesz dodać kolejnego buffa, bez polimorfizmu musisz zmieniać wszystkie metody, które liczą cokolwiek, na co ten buff ma mieć wpływ".

A co jak trzeba dodać nową metodą a w kodzie mam 100x klas implementujących interfejs? Ten sam problem, tylko akolici OOP jakoś lubią go ignorować. Rozwiązaniem wszystkiego jest "dobry design" ale czasami ciężko na niego wpaść.

0

Zgadzam się z tezą ogólną, tj. nadmierny polimorfizm szkodzi, ale nie zgadzam się z tezą szczególną, czyli, że szkodzi w twoim przypadku.

Atak i obrona nie powinny być nullami. Załóżmy, że armorByPass i block to liczby naturalne - czyli powinny być równe zeru gdy obiekt nie wykazuje tych własności. Wtedy:

var blockedDamage = Math.max(0, defender.Block - attacker.ArmorByPass);
damage = damage - blockedDamage;

Zero if'ów, zero walki z trudnymi wartościami, po prostu prosty wzór.

0
slsy napisał(a):

Polimorfizm jest problematyczny, bo ludzie traktowali go jako święty graal programowania, gdzie to technika jak każda inna, która ma swoje wady i zalety.

Czyli narzędzie jest złe, bo ludzie źle go używali?

2
Riddle napisał(a):

Czyli narzędzie jest złe, bo ludzie źle go używali?

Myślę, że to co napisał @snowflake2137 dobrze oddaje sprawę. Polimorfizm nie jest zły, jest na tyle dobry w tylu sytuacjach, że istnieje duże parcie na to, żeby używać go wszędzie co prowadzi do słabego kodu

5
var damage = blabla;
foreach(var effect in Effects)
    damage = effect.Apply(damage);

Działało:

var damage = blabla;
if(attacker.ArmorBypass != null && defender.Block == null)
    damage = /*wzór*/
else if(attacker.ArmorBypass == null && defender.Block != null)
    damage = /*inny wzór*/
else if(attacker.ArmorBypass != null && defender.Block != null)
    damage = /*jeszcze inny wzór, który jest RÓŻNY od po prostu applyBlock(applyArmorBypass(damage))*/

Nie rozumiem argumentacji. Przecież równie dobrze effect.Apply mogłoby przyjmować attacker i defender:

effect.Apply(attacker, defender);

Po prostu coś jest zależne od kilku rzeczy naraz. W środku mogą być ify, to nie przeczy polimorfizmowi.

Ogólnie mam wrażenie, że to kolejna dyskusja, gdzie ktoś próbuje mówić o polimorfizmie, a mówi o szczegółach implementacyjnych czy o jakichś swoich wpadkach niezwiązanych z ideą polimorfizmu.

Jeśli już, to w drugim przypadku widzę polimorfizm (skoro kod się inaczej zachowuje w zależności od tego, co mają w sobie attacker i defender). W pierwszym przypadku po prostu masz po prostu kompozycję funkcji, która się nie udała.

0

Polecam przeczytać artykuł gościa imieniem Jez Humble, z 2011r: https://www.thoughtworks.com/insights/blog/dvcs-continuous-integration-and-feature-branches

Zwłaszcza kawałek dookoła:

But like all powerful tools, there are many ways you can use them, and not all of them are good.

2

Dobry temat.
Odpowiedź na pytanie zależy od celu użycia danej konstrukcji.

  1. Internal. Jeżeli oprogramowujemy jakiś algorytm, lokalny, konkretny w danym kontekście to nie bawimy się w polimorfizm.
    W językach funkcyjnych użyjemy właśnie ADT i pattern matchingu.

W javie właśnie pojawiło się sealed class (nie jest to sealed z C#, bo to w javie nazywa się final) i będziemy bawić się w bieda javowy pattern matching.
Przykład szukanie w drzewie

//kod ChatGPT
public sealed interface Tree permits Leaf, Node {

    record Leaf() implements Tree {}

    record Node(int value, Tree left, Tree right) implements Tree {}
}



    public static boolean search(Tree tree, int target) {
        return switch (tree) {
            case Leaf -> false;
            case Node(int value, Tree left, Tree right) -> {
                if (value == target) {
                    yield true;
                } else if (target < value) {
                    yield search(left, target);
                } else {
                    yield search(right, target);
                }
            }
        };
    }

// oczywiście Tree mogło by być parametryzowane typem value (zamiast int) i byłby polimorfizm :-) (tylko inny)
  1. Extensions jeżeli budujemy mechanizm, który potencjalnie będzie rozszerzany (bo tak chcemy) to dokładnie wtedy projektujemy sposób rozszerzania i być może użyjemy polimorfizmu.

Dobry przykład to Iterable, mamy metody, które łażą po elementach kolekcji i np. wyszukują max, min itp.

     //kod ChatGPT (w życiu bym zmiennej nie użył)
    public static <T extends Comparable<? super T>> T max(Iterable<T> iterable) {
        Iterator<T> iterator = iterable.iterator();
        if (!iterator.hasNext()) {
            throw new IllegalArgumentException("Iterable is empty");
        }

        T maxElement = iterator.next();
        while (iterator.hasNext()) {
            T currentElement = iterator.next();
            if (currentElement.compareTo(maxElement) > 0) {
                maxElement = currentElement;
            }
        }

        return maxElement;
    }

I teraz jeśli tylko ktoś zapewni, że własne elementy są Comparable, a własne kolekcje Iterable to może tego kodu użyć.

Oczywiście można by teraz napisać max i min dla drzewa, które będzie specyficzne dla drzewa i nie będzie działało na żadnej innej kolekcji (bo tak - bo np. mamy drzewo uporządkowane i możemy optymalniej je napisać), natomiast nadal możemy być polimorficzni względem elementów (Comparable).

Jeśli chodzi o same instrukcje warunkowe to rozumiem tręd polegający na ich eliminacji w miarę możliwości. Instrukcje warunkowe, szczególnie jeśli są statementami są wysoce błędogennymi elementami w kodzie, każdy refaktoring potrafi je subtelnie rozwalić. Niestety subtype polimorfizm to też nie jest idealne rozwiązanie.
Im bardziej ogólny typ, zakładamy (Iterable , Comparable są dobrym przykładem) tym faktycznie łatwiej napisać polimorficzny kod, który jest poprawny, bo często niewiele da się zrobić (te interfejsy niewiele mają metod).

Ale im bardziej specyficzny interfejs roszerzamy,( im więcej ma metod, im bardziej skomplikowane typy te metody zwracaja/przyjmują) tym więcej nieoczekiwanych dziwactw mogą zrobić klasy pochodne, i coraz trudniej ( w miarę przyrostu metod) jest napisać kod na nieoczekiwane zachowania tych klas odporny.

Ten sam problem jest zresztą z typeclass (Haskell/Scala) - im bardziej skomplikowany interfejs tym więcej praw narzuca, a nie mamy mechanizmu, żeby tych praw pilnować. (Turing to świnia).

TLDR;
w mikroscali rządzi CLOSED i switche, w makro OPEN i polimorfizm ma uzasadnienie.

0

Ten pattern matching Który zaprezentowałeś wygląda Bardzo jak po prostu inna składania na instrukcje warunkowe. Można ostrożnie stwierdzić że nie ma takiej rzeczy którą da się zrobić pattern matchingiem, a nie da się ifem.

3
Riddle napisał(a):

Ten pattern matching Który zaprezentowałeś wygląda Bardzo jak po prostu inna składania na instrukcje warunkowe. Można ostrożnie stwierdzić że nie ma takiej rzeczy którą da się zrobić pattern matchingiem, a nie da się ifem.

A była gdzieś jakaś teza tego typu (że nie da się ifem)?

0

Jest niby statyczny polimorphism, ale nie mam pojęcia jak on działa, w dynamicznym akurat każda klasa ma vtable i tam dana methoda ma index w tej tablicy.
To jest dobre rozwiązanie, w niektórych przypadkach jak nie wiesz co będzie trzeba uruchomić.

Wszędzie bym nie pchał, ale jedynie tam, gdzie nie wiadomo co jest potrzebne i to się okazuje dopiero w trakcie, też dynamiczny polimorphism marnuje conajmniej 1 cykl procesora, więc jak ktoś głupi benchmark zrobi, to strasznie to wypada 100% wolniej niż bez.
Ale to się używa, żeby jedną funkcję wywołać to zwykle jest to margines rzędu 0.00001% całego spowolnienia i jest to dopuszczalne.

Python to wolny język, a całe AI się robi w nim, bo tam jakby wywołujesz funkcje, które są bardzo szybkie, a sam język wolny, ale 99,999% jest backendem, a tylko niewielki procent frontendem, którym sterujesz.

2
.GodOfCode. napisał(a):

Jest niby statyczny polimorphism, ale nie mam pojęcia jak on działa, w dynamicznym akurat każda klasa ma vtable i tam dana methoda ma index w tej tablicy.
To jest dobre rozwiązanie, w niektórych przypadkach jak nie wiesz co będzie trzeba uruchomić.

Jest wiele rodzajów polimorfizmu, najprostszy jaki znasz to pewnie makro w C (wiem, straszne g**no).
Generalnie polimorfizm to działanie tego samego kawałka kodu źródłowego dla różnych typów.
Tu masz o różnych typach polimorfizmu w C++:
https://fluentprogrammer.com/polymorphism-in-cpp/#:~:text=Run%20time%20polymorphism%20is%20also,(implicit%20or%20explicit)%20casting.

I nieprawdą jest, że każda klasa ma vtable w dynamicznym. Jak ostatnio sprawdzałem to vtable nie jest w żaden sposób specyfikowana w C++.
To szczegół implementacyjny, wiele (być może większość) kompilatorów C++ vtable używa, ale nie musi.
Możesz sobie zrobić własne C++ bez vtable (np. trzymać w obiektach wskaźniki na funkcje).

Wszędzie bym nie pchał, ale jedynie tam, gdzie nie wiadomo co jest potrzebne i to się okazuje dopiero w trakcie, też dynamiczny polimorphism marnuje conajmniej 1 cykl procesora, więc jak ktoś głupi benchmark zrobi, to strasznie to wypada 100% wolniej niż bez.
Ale to się używa, żeby jedną funkcję wywołać to zwykle jest to margines rzędu 0.00001% całego spowolnienia i jest to dopuszczalne.

Z ciekawostek - w javie polimorficzne wywołanie bywa zamieniane na statyczne, albo wręcz inlinowane i nie masz różnicy w czasach wykonania.
To fakt, że faktycznie subtype polimorfizm może kosztować, tak samo jak każdy branch może kosztować (bo gdzieś jest decyzja), ale nie jest to przynajmniej cykl, czasem może być pół cykla albo jedna setna, czasem 100, czasem nic. Zależy od języka, kompilatora, CPU, fragmentu kodu.
W ogólności dobry kompilator może zamienić polimorficzne wywołanie na ify.... (w jvm - jit tak czasem robi).

1

Tak, switche z exhaustive checkami ponad wizytatory.

Dlaczego? m.in dlatego, że debug jest o wie, wiele prostszy/szybszy.

1

W przypadku logiki biznesowej unikam polimorfizmu, bo nie chcę rozrzucać powiązanej logiki po kilku klasach w różnych miejscach. Co więcej, taki kod trudniej się czyta, rozumuje i utrzymuje. Ale to co jest najgorsze to usuwanie i wycofywanie się z nietrafionych pomysłów.

IMO polimorfizm jest dobry na obrzeżach aplikacji, zwłaszcza tam gdzie jest marginalna logika, i tam gdzie potrzebne są fejkowe providery.

0

jeśli chodzi o wydajność to od wieków java (a przynajmniej te standardowe wersje od suna i później oracle'a) ma class hierarchy analysis (do 'pół-statycznego' wykrywania wywołań niewirtualnych, tzn. podczas ładowania kolejnych klas ta analiza jest odpalana ponownie i może następować dynamiczna deoptymalizacja) i dynamiczne profilowanie z dewirtualizacją (do pokrycia przypadków niedających się załatwić poprzednią analizą). to powoduje, że płaci się tylko za wywołania faktycznie polimorficzne i np. ręczne dodawanie słówka final do metod (żeby oznaczyć, że nie da się ich nadpisać) nic nie daje, bo jvm sam sobie to już wcześniej obliczył (że metoda nie jest nadpisywana).

co do obiektowego polimorfizmu to chętnie korzystam z niego w testach. w kodzie produkcyjnym też korzystam, ale w praktyce znacznie rzadziej, bo w sumie implementowanie logiki biznesowej w typowej biznesowej krowie zwykle nie wymaga dużych ilości polimorfizmu.

w kodzie używam zarówno obiektowego polimorfizmu jak i pattern matchingu, a konkretne podejście wybieram w sumie na intuicję (chociaż są pewne reguły na zdecydowanie kiedy podejście obiektowe będzie nietrafione). podejścia skrajne, czyli tylko obiektowy polimorfizm albo tylko pattern matching, uważam za nieoptymalne.

wizytatorów nie tworzę i nie polecam ich tworzyć jeśli ktoś klepie apkę końcową i ma do dyspozycji zarówno obiektówkę jak i pattern matching - oba te rozwiązania są wygodniejsze niż wizytator. z drugiej strony, moim zdaniem wizytator ułatwia zachowanie wstecznej kompatybilności (ale już nie pamiętam swoich przemyśleń), więc jeśli ktoś klepie szeroko używaną bibliotekę to warto rozważyć wizytatora. prawdopodobnie w bibliotece standardowej javy (czyli języka mocno nastawionego na kompatybilność wsteczną) wizytatory będą dalej rozbudowywane (te z java.lang.model). uwaga: struktura wizytatora nie musi 1:1 odpowiadać hierarchii wizytowanych typów. można sobie np. używać pod spodem hierarchii sealed types, a w wizytatorze wystawiać tylko publiczne interfejsy. dla przykładu https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/FileVisitor.html umożliwia wizytowanie typu https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Path.html który jest interfejsem. tak czy siak, wizytator tak rzadko ma przewagę nad jednocześnie obiektowym polimorfizmem jak i pattern matchingiem, że (z punktu widzenia klepacza biznesowej krowy) można go olać (tzn. olać tworzenie wizytatorów na własne potrzeby) i wybierać między obiektowym polimorfizmem, a pattern matchingiem.

1

Mam wrażenie, że co jakiś czas wypływa temat obrzucenia błotem jakiejś dobrej praktyki.
Nie wiem w jakim celu, może ktoś chcę w ten sposób pokazać, że ma skile czy coś, ciężko stwierdzić.
DRY jest złe (pewnie copy-paste jest lepszy), a teraz polimorfizm jest atakowany.
Tak samo jak ktoś kto jest anty DRY, nie rozumie tej zasady, tak samo mam wrażenie, że autor nie rozumie polimorfizmu.
I tak samo jak są odstępstwa od DRY i nie powinno się stosować totalnego DRY, tak samo polimorfizm nie służy zupełnie do zastępowania ifów.
Polimorfizm służy do tego, żeby z ifów nie robiło się bagno z czasem rozwoju aplikacji i do tego, żeby klasy miały swoje przeznaczenie, zajmowały się jakimś kontekstem, a nie robiło się klas robiących wszystko.

1

Po prostu świat kodu zmierza w kierunku jak najprostszego kodu i podoba mi się ten nurt i wspieram od lat

1

Brak polimorfizmu prowadzi do legacy bagna, a nie prostoty kodu, prostota jest na początku, a z każdą zmianą i dodanym kolejnym ifem robi sie coraz większe bagno.

1

No ogólnie nie można przesadzać ale często to bagno po prostu nie nadchodzi bo kod jest prosty. Nie mówię o drabinkach ifów itd. tylko zbędnych abstrakcjach na zaś gdy to zaś nie nadchodzi

0

A Ja mówię właśnie o drabinkach ifów i metodach, które je mają, to jest shit spowodowany tym, że ktoś nie umie pisać polimorficznie i nie rozumie jego zastosowania.
Jakoś rzadko się spotykam z tym, że bagno nie nadchodzi, przeważnie musze to bagno sprzątać i wrzucać właśnie polimorfizm zamiast drabiny ifów, żeby w ogóle kod w końcu zaczął poprawnie działać.

4

logika biznesowa koniec końców gdzieś i tak musi być, złożoności nie da się w nieskończoność wynosić

i teraz jeżeli pytasz mnie czy wolę jeden plik algorithm.file który ma w sobie tego switcha czy if ladder i który widzę nierzadko cały na raz

zamiast approach1.file approach2.file approach3.file approach4.file approach5.file approach6.file które potrzebuje przeglądnąć

... to wole to 1

0

problem w tym, że KISS, wspiera właśnie polimorfizm, bo to dzięki niemu metody mogą być małe, a kod ma określoną ścieżkę wykonania, którą można łatwo podąrzyć i prześledzić jego działanie, drabiny ifów tego nie dają.
Poza tym Ja wolę kilka małych plików i sobie po nich skakać, a nie mieć przed oczami kupy kodu, w której nie idzie się połapać.
Patrzysz z punktu widzenia pisania teraz, a nie łatwości jego rozwijania, debugowania itd.
Wiadomo, że łatwiej zrobić switcha, ale takie lenistwo, nie przynosi korzyści z czasem rozwoju kodu.

1
omenomn2 napisał(a):

Patrzysz z punktu widzenia pisania teraz, a nie łatwości jego rozwijania, debugowania itd.

no właśnie wręcz przeciwnie :D

Najwięcej czasu na debugu traciłem w OOP heavy projektach, gdzie w dodatku są zabawy w kasty srasty i inne instanceof

Poza tym Ja wolę kilka małych plików i sobie po nich skakać, a nie mieć przed oczami kupy kodu, w której nie idzie się połapać.

Ale czemu w twoim świecie jeden plik implikuje słaby / nieczytelny kod?

Przecież z takim podejściem to nigdy to nie będzie miało sensu :D

0

no właśnie wręcz przeciwnie :D

właśnie nie.

Ale czemu w twoim świecie jeden plik implikuje słaby / nieczytelny kod?

Nie mam swojego świata.
Choćby dlatego, że nie chce mi się scrolować po tysiącu, czy kilku tysiącach linijek kodu i wygodniej jest przeklikać się do konkretnego małego pliku i nie musieć go scrolować 5 min. żeby coś w nim znaleźć.

2

Nie mam swojego świata.

Zawsze występuje pewnego rodzaju bańka - codebasy na które miałeś ekspozycje.

Choćby dlatego, że nie chce mi się scrolować po tysiącu, czy kilku tysiącach linijek kodu i wygodniej jest przeklikać się do konkretnego małego pliku i nie musieć go scrolować 5 min. żeby coś w nim znaleźć.

a ja wole mieć 1 plik 1k LoC niż 5x 200 pomiędzy którymi muszę skakać, a nawet z topowymi IDE typu VS / Rider to i tak jest jakiś cognitive load podczas analizy.

PS:

Temat wałkowany wielokrotnie :P Kiedy gruboziarnisty kod jest lepszy?

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.