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.
źródło
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ą.
źródło
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 (
Message
np. 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
Message
lub aParticle
pomimo 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
Być może jest to objaw wyboru niewłaściwego języka programowania.
źródło
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.
źródło
Przestrzegałbym również przed użyciem funkcji IDE, które ułatwiają abstrakcyjne rzeczy.
źródło