Czy abstrakcje muszą zmniejszać czytelność kodu?

19

Dobry programista, z którym współpracuję, powiedział mi ostatnio o trudnościach we wdrażaniu funkcji w odziedziczonym przez nas kodzie; powiedział, że problemem jest to, że kod jest trudny do przestrzegania. Po tym zagłębiłem się w produkt i zdałem sobie sprawę, jak trudno było zobaczyć ścieżkę kodu.

Używał tak wielu interfejsów i abstrakcyjnych warstw, że próba zrozumienia, gdzie zaczyna się i kończy, była dość trudna. Sprawiło, że pomyślałem o czasach, w których oglądałem poprzednie projekty (zanim byłem tak świadomy zasad czystego kodu) i bardzo trudno było mi się poruszać w projekcie, głównie dlatego, że moje narzędzia do nawigacji kodu zawsze znajdowały się w interfejsie. Znalezienie konkretnej implementacji lub sytuacji, w której coś zostało podłączone w architekturze typu wtyczki, wymagałoby dodatkowego wysiłku.

Wiem, że niektórzy programiści ściśle odrzucają pojemniki do wstrzykiwania zależności z tego właśnie powodu. Tak bardzo dezorientuje ścieżkę oprogramowania, że ​​trudność nawigacji kodu rośnie wykładniczo.

Moje pytanie brzmi: kiedy ramy lub wzorzec wprowadza tak wiele ogólnych kosztów, czy warto? Czy jest to objaw źle zaimplementowanego wzoru?

Wydaje mi się, że programista powinien spojrzeć na szerszy obraz tego, co abstrakcje wnoszą do projektu, aby pomóc im przetrwać frustrację. Zazwyczaj jednak trudno jest sprawić, by zobaczyli tak duży obraz. Wiem, że nie udało mi się sprzedać potrzeb MKOl i DI z TDD. Dla tych programistów korzystanie z tych narzędzi zbyt mocno ogranicza czytelność kodu.

Martin Blore
źródło

Odpowiedzi:

17

To naprawdę bardziej długi komentarz do odpowiedzi @kevin cline.

Chociaż same języki niekoniecznie powodują lub zapobiegają temu, myślę, że jest coś w jego przekonaniu, że w pewnym stopniu jest to związane z językami (lub przynajmniej społecznościami językowymi). W szczególności, nawet jeśli możesz napotkać ten sam problem w różnych językach, często przybiera on raczej różne formy w różnych językach.

Na przykład, gdy natkniesz się na to w C ++, są szanse, że jest to mniej wynikiem zbyt dużej abstrakcji, a bardziej wynikiem zbytniej sprytności. Na przykład programista ukrył kluczową transformację, która zachodzi (której nie można znaleźć) w specjalnym iteratorze, więc to, co wygląda na kopiowanie danych z jednego miejsca do drugiego, naprawdę ma wiele skutków ubocznych, które nie mają nic do zrobić z tym kopiowaniem danych. Żeby było interesująco, jest to przeplatane z danymi wyjściowymi, które powstają jako efekt uboczny tworzenia obiektu tymczasowego w trakcie rzutowania jednego typu obiektu na inny.

Z drugiej strony, gdy napotkasz go w Javie, znacznie bardziej prawdopodobne jest, że zobaczysz jakiś wariant dobrze znanego „świata korporacyjnego hello”, w którym zamiast jednej trywialnej klasy, która robi coś prostego, otrzymujesz abstrakcyjną klasę podstawową oraz konkretną klasę pochodną, ​​która implementuje interfejs X i jest tworzona przez klasę fabryczną w środowisku DI itp. 10 wierszy kodu wykonujących prawdziwą pracę jest pochowanych pod 5000 liniami infrastruktury.

Niektóre z nich zależą od środowiska przynajmniej w takim samym stopniu jak język - bezpośrednia praca z takimi oknami, jak X11 i MS Windows, jest znana z przekształcania trywialnego programu „hello world” w ponad 300 linii prawie nieczytelnych śmieci. Z czasem opracowaliśmy różne zestawy narzędzi, aby nas również od tego odizolować - ale 1) te zestawy narzędzi same w sobie są dość trywialne, i 2) wynik końcowy jest nadal nie tylko większy i bardziej złożony, ale także zwykle mniej elastyczny niż w trybie tekstowym (np. nawet jeśli drukuje tylko tekst, przekierowanie go do pliku jest rzadko możliwe / obsługiwane).

Aby odpowiedzieć (przynajmniej częściowo) na pierwotne pytanie: przynajmniej kiedy go zobaczyłem, nie chodziło raczej o słabą implementację wzorca niż o zastosowanie wzorca, który byłby nieodpowiedni do danego zadania - większość często próbuje zastosować jakiś wzorzec, który może być przydatny w programie, który jest nieuchronnie ogromny i złożony, ale po zastosowaniu do mniejszego problemu staje się również ogromny i złożony, nawet jeśli w tym przypadku wielkości i złożoności naprawdę można było uniknąć .

Jerry Coffin
źródło
7

Uważam, że jest to często spowodowane brakiem podejścia YAGNI . Wszystko, co przechodzi przez interfejsy, mimo że istnieje tylko jedna konkretna implementacja i nie ma obecnych planów wprowadzenia innych, jest doskonałym przykładem zwiększania złożoności, której nie potrzebujesz. Prawdopodobnie jest to herezja, ale podobnie myślę o częstym stosowaniu zastrzyku uzależnienia.

Carson63000
źródło
+1 za wzmiankę o YAGNI i abstrakcjach z pojedynczymi punktami odniesienia. Podstawową rolą tworzenia abstrakcji jest uwzględnienie wspólnego punktu wielu rzeczy. Jeśli abstrakcja jest przywoływana tylko z jednego punktu, nie możemy mówić o uwzględnieniu typowych rzeczy, taka abstrakcja po prostu przyczynia się do problemu yoyo. Rozszerzyłbym to, ponieważ dotyczy to wszystkich rodzajów abstrakcji: funkcji, rodzajów, makr, cokolwiek ...
Calmarius
3

Cóż, za mało abstrakcji, a kod jest trudny do zrozumienia, ponieważ nie można wyodrębnić, które części mają co zrobić.

Zbyt dużo abstrakcji i widzisz abstrakcję, ale nie sam kod, a następnie utrudnia śledzenie rzeczywistego wątku wykonania.

Aby osiągnąć dobrą abstrakcję, należy KISS: zapoznaj się z moją odpowiedzią na te pytania, aby wiedzieć, jak postępować, aby uniknąć tego rodzaju problemów .

Myślę, że unikanie głębokiej hierarchii i nazewnictwa jest najważniejszym punktem, na który należy zwrócić uwagę w opisywanym przypadku. Gdyby abstrakcje były dobrze nazwane, nie musiałbyś wchodzić zbyt głęboko, tylko do poziomu abstrakcji, gdzie musisz zrozumieć, co się dzieje. Nazewnictwo pozwala określić, gdzie jest ten poziom abstrakcji.

Problem pojawia się w kodzie niskiego poziomu, gdy naprawdę potrzebujesz całego procesu, aby go zrozumieć. Zatem enkapsulacja za pomocą wyraźnie izolowanych modułów jest jedyną pomocą.

Klaim
źródło
3
Cóż, za mało abstrakcji, a twój kod jest trudny do zrozumienia, ponieważ nie możesz wyodrębnić, które części co robią. To enkapsulacja, a nie abstrakcja. Możesz izolować części w konkretnych klasach bez dużej abstrakcji.
Oświadczenie
Klasy nie są jedynymi abstrakcjami, których używamy: funkcje, moduły / biblioteki, usługi itp. W swoich klasach zwykle abstraktujesz każdą funkcjonalność za funkcją / metodą, która może wywoływać inne metody, które abstrakują się nawzajem.
Klaim
1
@Statement: Hermetyzowanie danych jest oczywiście abstrakcją.
Ed S.
Hierarchie przestrzeni nazw są jednak naprawdę fajne.
JAB
2

Dla mnie jest to problem sprzężenia i związany z szczegółowością projektu. Nawet najbardziej luźna forma sprzęgania wprowadza zależności między rzeczami. Jeśli jest to zrobione dla setek do tysięcy obiektów, nawet jeśli wszystkie są względnie proste, zastosuj się do SRP, a nawet jeśli wszystkie zależności płyną w kierunku stabilnych abstrakcji, powstanie podstawa kodu, którą bardzo trudno uzasadnić jako wzajemnie powiązaną całość.

Istnieją praktyczne rzeczy, które pomagają ocenić złożoność bazy kodu, które nie są często omawiane w teoretycznej SE, jak na przykład, jak głęboko w stos wywołań można dostać się przed końcem i jak głęboko musisz zejść, zanim to możliwe, z bardzo pewny siebie, zrozum wszystkie możliwe działania niepożądane, które mogą wystąpić na tym poziomie stosu wywołań, w tym w przypadku wyjątku.

I z własnego doświadczenia wynika, że ​​o wiele łatwiejsze jest rozumowanie bardziej płaskich systemów z płytszymi stosami wywołań. Skrajnym przykładem może być system podmiot-komponent, w którym komponenty są tylko surowymi danymi. Tylko systemy mają funkcjonalność, a podczas wdrażania i korzystania z ECS uznałem, że jest to najłatwiejszy system w historii, jak dotąd, do rozumienia, kiedy złożone bazy kodu obejmujące setki tysięcy linii kodu w zasadzie zagotowują się do kilkudziesięciu systemów, które zawierają całą funkcjonalność.

Zbyt wiele rzeczy zapewnia funkcjonalność

Alternatywą wcześniej, gdy pracowałem w poprzednich bazach kodów, był system z setkami do tysięcy przeważnie małych obiektów, w przeciwieństwie do kilkudziesięciu dużych systemów z niektórymi obiektami używanymi tylko do przekazywania wiadomości z jednego obiektu do drugiego ( Messagenp. Obiekt, który miał swój własny interfejs publiczny). Zasadniczo to uzyskuje się analogicznie po przywróceniu ECS z powrotem do punktu, w którym komponenty mają funkcjonalność, a każda unikalna kombinacja komponentów w encji daje własny typ obiektu. I to będzie miało tendencję do uzyskiwania mniejszych, prostszych funkcji odziedziczonych i zapewnianych przez nieskończone kombinacje obiektów, które modelują pomniejsze pomysły (Particle obiekt vs.Physics System, np.). Jednak ma również tendencję do generowania złożonego wykresu wzajemnych zależności, który utrudnia rozumowanie o tym, co dzieje się z poziomu ogólnego, po prostu dlatego, że w bazie kodu jest tak wiele rzeczy, które mogą faktycznie coś zrobić, a zatem mogą zrobić coś złego - - typy, które nie są typami „danych”, ale typami „obiektów” z powiązaną funkcjonalnością. Typy, które służą jako czyste dane bez powiązanej funkcjonalności, nie mogą pójść źle, ponieważ same nie mogą nic zrobić.

Czyste interfejsy nie pomagają tak bardzo w problemie ze zrozumieniem, ponieważ nawet jeśli to sprawia, że ​​„zależności kompilacji w czasie” są mniej skomplikowane i zapewnia więcej miejsca na zmiany i ekspansję, nie czyni to „zależności środowiska wykonawczego” i interakcji mniej skomplikowanymi. Obiekt klienta nadal wywołuje funkcje na konkretnym obiekcie konta, nawet jeśli są wywoływane IAccount. Polimorfizm i abstrakcyjne interfejsy mają swoje zastosowania, ale nie rozdzielają rzeczy w sposób, który naprawdę pomaga w uzasadnieniu wszystkich skutków ubocznych, które mogą wystąpić w danym momencie. Aby osiągnąć ten rodzaj skutecznego oddzielenia, potrzebujesz bazy kodu, która zawiera znacznie mniej elementów zawierających funkcjonalność.

Więcej danych, mniej funkcjonalności

Dlatego uważam, że podejście ECS, nawet jeśli nie zastosujesz go całkowicie, jest niezwykle pomocne, ponieważ zamienia to, co byłyby setkami obiektów, w surowe dane dzięki nieporęcznym systemom, bardziej grubo zaprojektowanym, które zapewniają wszystkie funkcjonalność. Maksymalizuje liczbę typów „danych” i minimalizuje liczbę typów „obiektów”, a zatem absolutnie minimalizuje liczbę miejsc w systemie, które mogą się nie udać. Rezultatem końcowym jest bardzo „płaski” system bez złożonego wykresu zależności, tylko systemy do komponentów, nigdy odwrotnie, i nigdy do innych komponentów. Zasadniczo jest to o wiele więcej surowych danych i znacznie mniej abstrakcji, co skutkuje scentralizowaniem i spłaszczeniem funkcjonalności bazy kodu do kluczowych obszarów, kluczowych abstrakcji.

30 prostszych rzeczy niekoniecznie musi być prostszych do uzasadnienia niż jedna bardziej złożona rzecz, jeśli te 30 prostszych rzeczy są ze sobą powiązane, podczas gdy złożona rzecz stoi sama. Tak więc moją propozycją jest przeniesienie złożoności z interakcji między obiektami i bardziej na bardziej masywne obiekty, które nie muszą wchodzić w interakcje z niczym innym, aby osiągnąć masowe oddzielenie, do całych „systemów” (nie monolitów i boskich obiektów, pamiętajcie o tym i nie klasy z 200 metodami, ale coś znacznie wyższego poziomu niż a Messagelub a Particlepomimo posiadania minimalistycznego interfejsu). I faworyzuj bardziej proste, stare typy danych. Im bardziej na nich polegasz, tym mniej sprzężenia otrzymasz. Nawet jeśli jest to sprzeczne z niektórymi pomysłami na SE, okazało się, że to bardzo pomaga.


źródło
0

Moje pytanie brzmi: czy warto, jeśli struktura lub wzorzec wprowadza tak duże koszty ogólne? Czy jest to objaw źle zaimplementowanego wzoru?

Być może jest to objaw wyboru niewłaściwego języka programowania.

Kevin Cline
źródło
1
Nie rozumiem, jak to ma coś wspólnego z wybranym językiem. Abstrakcje to koncepcja niezależna od języka na wysokim poziomie.
Ed S.
@Ed: Niektóre abstrakcje są łatwiejsze do zrealizowania w niektórych językach niż w innych.
kevin cline
Tak, ale to nie znaczy, że nie można napisać doskonale utrzymywalnej i łatwo zrozumiałej abstrakcji w tych językach. Chodzi mi o to, że twoja odpowiedź w żaden sposób nie odpowiada na pytanie ani nie pomaga PO.
Ed S.
0

Słabe zrozumienie wzorców projektowych stanowi główną przyczynę tego problemu. Jednym z najgorszych, jakie widziałem w tym jo-jo i skakaniu między interfejsami bez bardzo konkretnych danych pomiędzy nimi, było rozszerzenie Oracle Grid Control.
Szczerze mówiąc, wyglądało to tak, jakby ktoś miał abstrakcyjną metodę fabryczną i orgazm wzorników dekoracyjnych w całym moim kodzie Java. I sprawiło, że poczułem się równie pusty i samotny.

Jeff Langemeier
źródło
-1

Przestrzegałbym również przed użyciem funkcji IDE, które ułatwiają abstrakcyjne rzeczy.

Christopher Mahan
źródło