Mam problemy z tym, co uważam za zbyt abstrakcyjne w bazie kodu (a przynajmniej radzenie sobie z tym). Większość metod w bazie kodu została wyodrębniona w celu przyjęcia najwyższego rodzica A w bazie kodu, ale dziecko B tego rodzica ma nowy atrybut, który wpływa na logikę niektórych z tych metod. Problem polega na tym, że tych atrybutów nie można sprawdzić w tych metodach, ponieważ dane wejściowe są abstrakcyjne do A, a A oczywiście nie ma tego atrybutu. Jeśli spróbuję stworzyć nową metodę obsługi B w inny sposób, zostanie ona wywołana w celu powielenia kodu. Sugestia mojego kierownika technicznego polega na stworzeniu wspólnej metody, która przyjmuje parametry logiczne, ale problem polega na tym, że niektórzy ludzie postrzegają to jako „ukryty przepływ kontroli”, gdzie wspólna metoda ma logikę, która może nie być widoczna dla przyszłych programistów , a także ta wspólna metoda stanie się nadmiernie złożona / skomplikowana jeden raz, jeśli konieczne będzie dodanie przyszłych atrybutów, nawet jeśli zostanie ona podzielona na mniejsze wspólne metody. Zwiększa to również sprzężenie, zmniejsza spójność i narusza zasadę pojedynczej odpowiedzialności, na co zwrócił uwagę ktoś z mojego zespołu.
Zasadniczo duża część abstrakcji w tej bazie kodu pomaga ograniczyć powielanie kodu, ale utrudnia rozszerzanie / zmienianie metod, gdy są zmuszone do pobierania najwyższej abstrakcji. Co powinienem zrobić w takiej sytuacji? Jestem w centrum winy, chociaż wszyscy inni nie mogą się zgodzić, co uważają za dobre, więc w końcu mnie to boli.
Odpowiedzi:
Nie wszystkie duplikacje kodu są sobie równe.
Załóżmy, że masz metodę, która pobiera dwa parametry i dodaje je razem
total()
. Załóżmy, że masz innąadd()
. Ich implementacje wyglądają zupełnie identycznie. Czy powinny zostać połączone w jedną metodę? NIE!!!Zasada Don't-Repeat-Yourself lub DRY nie polega na powtarzaniu kodu. Chodzi o rozpowszechnianie decyzji, pomysłów, więc jeśli kiedykolwiek zmienisz swój pomysł, musisz przepisać wszędzie, gdzie rozpowszechniasz ten pomysł. Blegh. To straszne. Nie rób tego Zamiast tego użyj DRY, aby pomóc Ci podejmować decyzje w jednym miejscu .
Ale DRY może zostać zepsute w nawyk skanowania kodu w poszukiwaniu podobnej implementacji, która wydaje się być kopią i wklejaniem gdzie indziej. To martwa mózg forma SUSZENIA. Do diabła, możesz to zrobić za pomocą narzędzia do analizy statycznej. Nie pomaga, ponieważ ignoruje sens DRY, który polega na zachowaniu elastyczności kodu.
Jeśli zmienią się moje wymagania sumowania, być może będę musiał zmienić
total
implementację. To nie znaczy, że muszę zmienićadd
implementację. Jeśli jakiś Goober zgniótł je razem w jedną metodę, teraz czeka mnie niepotrzebny ból.Ile bólu Z pewnością mógłbym po prostu skopiować kod i stworzyć nową metodę, gdy jej potrzebuję. Więc nic wielkiego, prawda? Malarky! Jeśli nic więcej nie kosztuje mnie dobre imię! Dobre imiona są trudne do zdobycia i nie reagują dobrze, gdy majstrujesz przy ich znaczeniu. Dobre nazwy, które wyraźnie wyjaśniają cel, są ważniejsze niż ryzyko, że skopiujesz błąd, który, szczerze mówiąc, łatwiej jest naprawić, gdy twoja metoda ma prawidłową nazwę.
Tak więc radzę przestać pozwalać reakcjom szarpiącym kolana na podobny kod wiązać bazę kodu w węzłach. Nie twierdzę, że możesz zignorować fakt, że istnieją metody, a zamiast tego skopiuj i wklej willy nilly. Nie, każda metoda powinna mieć cholernie dobrą nazwę, która popiera jeden pomysł, o którym chodzi. Jeśli jego implementacja będzie pasować do implementacji innego dobrego pomysłu, to teraz, komu, do diabła, to obchodzi?
Z drugiej strony, jeśli masz
sum()
metodę, która ma identyczną lub nawet inną implementację niżtotal()
, ale za każdym razem, gdy zmieniają się twoje łączne wymagania, musisz zmienić,sum()
to jest duża szansa, że są to ten sam pomysł pod dwiema różnymi nazwami. Kod byłby nie tylko bardziej elastyczny, gdyby zostały scalone, ale korzystanie z niego byłoby mniej skomplikowane.Jeśli chodzi o parametry boolowskie, to nieprzyjemny zapach kodu. Kontrola przepływu nie tylko stanowi problem, ale gorzej pokazuje, że w złym momencie włączyłeś abstrakcję. Abstrakcje mają uprościć, a nie skomplikować. Przekazywanie booli do metody kontrolującej jej zachowanie jest jak tworzenie tajnego języka, który decyduje, którą metodę naprawdę wywołujesz. Ow! Nie rób mi tego. Nadaj każdej metodzie własną nazwę, chyba że masz jakiś uczciwy sposób na pogarszanie się polimorfizmu .
Teraz wydajesz się wypalony abstrakcją. Szkoda, ponieważ abstrakcja jest cudowną rzeczą, gdy jest dobrze wykonana. Używasz go dużo, nie myśląc o tym. Za każdym razem, gdy prowadzisz samochód bez konieczności rozumienia mechanizmu zębatkowego, za każdym razem, gdy używasz polecenia drukowania, nie myśląc o przerwach w systemie operacyjnym, i za każdym razem, gdy myjesz zęby, nie myśląc o każdym włosiu.
Nie, problemem, z którym się wydajesz, jest zła abstrakcja. Abstrakcja stworzona, by służyć innym celom niż twoje potrzeby. Potrzebujesz prostych interfejsów do złożonych obiektów, które pozwolą ci zaspokoić twoje potrzeby bez konieczności rozumienia tych obiektów.
Pisząc kod klienta korzystający z innego obiektu, wiesz, jakie są Twoje potrzeby i czego potrzebujesz od tego obiektu. Tak nie jest. Dlatego kod klienta jest właścicielem interfejsu. Kiedy jesteś klientem, nic nie jest w stanie powiedzieć, jakie są twoje potrzeby oprócz ciebie. Udostępniasz interfejs, który pokazuje, jakie są twoje potrzeby i żądasz, aby wszystko, co zostanie ci przekazane, spełniło te potrzeby.
To jest abstrakcja. Jako klient nie wiem nawet, z kim rozmawiam. Po prostu wiem, czego potrzebuję. Jeśli to oznacza, że musisz coś zawinąć, aby zmienić jego interfejs, zanim w porządku. Nie obchodzi mnie to. Po prostu rób to, co muszę zrobić. Przestań to komplikować.
Jeśli muszę zajrzeć do abstrakcji, aby zrozumieć, jak z niej korzystać, abstrakcja nie powiodła się. Nie powinienem wiedzieć, jak to działa. Po prostu to działa. Nadaj mu dobre imię, a jeśli zajrzę do środka, nie powinienem być zaskoczony tym, co znajdę. Nie każ mi zaglądać do środka, żeby pamiętać, jak go używać.
Kiedy nalegasz, aby abstrakcja działała w ten sposób, liczba poziomów za nią nie ma znaczenia. Tak długo, jak nie patrzysz za abstrakcją. Nalegasz, aby abstrakcja była zgodna z twoimi potrzebami, nie dostosowując się do niej. Aby to zadziałało, musi być łatwe w użyciu, mieć dobre imię i nie przeciekać .
Takie podejście zrodziło Dependency Injection (lub po prostu odniesienie do podania, jeśli jesteś starą szkołą taką jak ja). Działa to dobrze, preferując kompozycję i przekazywanie zamiast dziedziczenia . Takie podejście ma wiele nazw. Moim ulubionym jest powiedz, nie pytaj .
Mógłbym cię utopić w zasadach przez cały dzień. I wygląda na to, że twoi współpracownicy już są. Ale o to chodzi: w przeciwieństwie do innych dziedzin inżynierii, to oprogramowanie ma mniej niż 100 lat. Wszyscy wciąż to rozgryzamy. Więc nie pozwól, aby ktoś z dużą ilością zastraszających, brzmiących książek nauczył cię pisania trudnego do odczytania kodu. Słuchaj ich, ale nalegaj, aby miały sens. Nie bierz niczego na wiarę. Ludzie, którzy w jakiś sposób kodują tylko dlatego, że powiedziano im to w ten sposób, nie wiedząc, dlaczego robią największe bałagany ze wszystkich.
źródło
Zwykłe powiedzenie, że wszyscy tu czytamy i brzmi:
To nie jest prawda! Twój przykład to pokazuje. Proponuję zatem nieco zmodyfikowane oświadczenie (zachęcamy do ponownego użycia ;-)):
W twoim przypadku występują dwa różne problemy:
Oba są powiązane:
Shape
można go obliczyćsurface()
w wyspecjalizowany sposób.Jeśli wyodrębnisz operację, w której występuje ogólny ogólny wzór zachowania, masz dwie możliwości:
Ponadto takie podejście może skutkować abstrakcyjnym efektem sprzężenia na poziomie projektu. Za każdym razem, gdy chcesz dodać jakieś nowe wyspecjalizowane zachowanie, musisz je wyodrębnić, zmienić abstrakcyjnego rodzica i zaktualizować wszystkie pozostałe klasy. To nie jest rodzaj propagacji zmian, jakiego można by się spodziewać. I to nie jest tak naprawdę w duchu abstrakcji, która nie zależy od specjalizacji (przynajmniej od projektu).
Nie znam twojego projektu i nie mogę pomóc więcej. Być może jest to naprawdę bardzo złożony i abstrakcyjny problem i nie ma lepszego sposobu. Ale jakie są szanse? Objawy nadmiernej generalizacji są tutaj. Może nadszedł czas, aby spojrzeć na to jeszcze raz i rozważyć kompozycję nad generalizacją ?
źródło
Ilekroć widzę metodę, w której zachowanie włącza typ jej parametru, od razu zastanawiam się, czy metoda rzeczywiście należy do parametru metody. Na przykład zamiast metody takiej jak:
Zrobiłbym to:
Przenosimy zachowanie do miejsca, które wie, kiedy go użyć. Tworzymy prawdziwą abstrakcję, w której nie musisz znać rodzajów ani szczegółów wdrożenia. W twojej sytuacji bardziej sensowne może być przeniesienie tej metody z oryginalnej klasy (którą wywołam
O
), aby wpisaćA
i przesłonić ją w tekścieB
. Jeśli metoda jest wywoływanadoIt
dla jakiegoś obiektu, przejdźdoIt
doA
i zastąp go innym zachowaniem wB
. Jeśli istnieją bity danych, z którychdoIt
pierwotnie wywoływano, lub jeśli metoda jest używana w wystarczającej liczbie miejsc, możesz opuścić oryginalną metodę i przekazać:Możemy jednak zanurkować nieco głębiej. Spójrzmy na sugestię, aby zamiast tego użyć parametru boolowskiego i zobaczmy, czego możemy się dowiedzieć o sposobie myślenia twojego współpracownika. Jego propozycja polega na:
Wygląda to okropnie podobnie jak
instanceof
w moim pierwszym przykładzie, z tą różnicą, że przekazujemy tę kontrolę na zewnątrz. Oznacza to, że musielibyśmy to nazwać na jeden z dwóch sposobów:lub:
Po pierwsze, punkt wywoławczy nie ma pojęcia, jaki ma typ
A
. Czy zatem powinniśmy przepuszczać booleany do samego końca? Czy to naprawdę wzór, który chcemy w całej bazie kodu? Co się stanie, jeśli istnieje trzeci typ, który musimy uwzględnić? Jeśli tak się nazywa ta metoda, powinniśmy przenieść ją na typ i pozwolić systemowi wybrać dla nas implementację polimorficzną.Po drugie, musimy już znać rodzaj
a
punktu wywoławczego. Zwykle oznacza to, że albo tworzymy tam instancję, albo bierzemy instancję tego typu jako parametr. Stworzenie metody,O
która zajmujeB
tutaj, działałoby. Kompilator będzie wiedział, którą metodę wybrać. Kiedy przeprowadzamy takie zmiany, powielanie jest lepsze niż tworzenie niewłaściwej abstrakcji , przynajmniej dopóki nie dowiemy się, dokąd naprawdę zmierzamy. Oczywiście sugeruję, że tak naprawdę nie jesteśmy skończeni bez względu na to, co zmieniliśmy do tego momentu.Musimy przyjrzeć się bliżej relacjom między
A
iB
. Mówi się nam ogólnie, że powinniśmy preferować kompozycję zamiast dziedziczenia . Nie jest to prawdą w każdym przypadku, ale jest to prawdą w zaskakującej liczbie przypadków, gdy zagłębimy się w.B
DziedziczyA
, co oznacza, że wierzymy, żeB
jestA
.B
powinien być używany tak samoA
, z tym wyjątkiem, że działa trochę inaczej. Ale jakie są te różnice? Czy możemy nadać różnicom bardziej konkretną nazwę? Czy to nieB
jestA
, ale tak naprawdęA
maX
to może byćA'
alboB'
? Jak wyglądałby nasz kod, gdybyśmy to zrobili?Jeśli przenieśliśmy metodę
A
jak sugerowano wcześniej, moglibyśmy wstrzyknąć instancjęX
doA
i delegować tę metodę doX
:Możemy realizować
A'
iB'
, i pozbyćB
. Ulepszyliśmy kod, nadając nazwę koncepcji, która mogła być bardziej niejawna, i pozwoliliśmy sobie ustawić to zachowanie w czasie wykonywania zamiast czasu kompilacji.A
stało się również mniej abstrakcyjne. Zamiast relacji rozszerzonego dziedziczenia wywołuje metody na obiekcie delegowanym. Ten obiekt jest abstrakcyjny, ale bardziej skupia się tylko na różnicach w implementacji.Jest jeszcze jedna rzecz do obejrzenia. Cofnijmy się do propozycji twojego współpracownika. Jeśli we wszystkich witrynach z rozmowami wyraźnie znamy rodzaj
A
naszego połączenia, powinniśmy wykonywać połączenia takie jak:Założyliśmy wcześniej podczas komponowania że
A
maX
to alboA'
alboB'
. Ale może nawet to założenie jest nieprawidłowe. Jest to jedyne miejsce, gdzie ta różnica międzyA
iB
sprawy? Jeśli tak, to może możemy przyjąć nieco inne podejście. Nadal mamyX
, że jest alboA'
alboB'
, ale nie należy doA
.O.doIt
Troszczy się tylko o to, więc przekażmy to tylkoO.doIt
:Teraz nasza strona połączeń wygląda następująco:
Po raz kolejny
B
znika, a abstrakcja przechodzi w bardziej skupionąX
. Tym razemA
jest jeszcze prostszy, wiedząc mniej. To jest jeszcze mniej abstrakcyjne.Ważne jest ograniczenie duplikacji w bazie kodu, ale musimy najpierw rozważyć, dlaczego duplikacja ma miejsce. Duplikacja może być oznaką głębszych abstrakcji, które próbują się wydostać.
źródło
Abstrakcja przez dziedziczenie może stać się dość brzydka. Hierarchie klas równoległych z typowymi fabrykami. Refaktoryzacja może stać się bólem głowy. A także późniejszy rozwój, miejsce, w którym się znajdujesz.
Istnieje alternatywa: punkty rozszerzenia , ścisłe abstrakty i wielopoziomowe dostosowywanie. Powiedz jedno dostosowanie klientów rządowych na podstawie tego dostosowania dla konkretnego miasta.
Ostrzeżenie: Niestety działa to najlepiej, gdy wszystkie (lub większość) klas są rozszerzone. Brak opcji dla ciebie, może w małych.
Ta rozszerzalność polega na tym, że klasa podstawowa rozszerzalnego obiektu zawiera rozszerzenia:
Wewnętrznie istnieje leniwe mapowanie obiektu na obiekty rozszerzone według klasy rozszerzenia.
W przypadku klas GUI i komponentów ta sama rozszerzalność, częściowo z dziedziczeniem. Dodawanie przycisków i tym podobne.
W twoim przypadku sprawdzanie poprawności powinno sprawdzać, czy jest rozszerzone i sprawdzać się względem rozszerzeń. Wprowadzenie punktów rozszerzenia tylko dla jednego przypadku dodaje niezrozumiały kod, co nie jest dobre.
Nie ma więc rozwiązania, próba pracy w obecnym kontekście.
źródło
„ukryta kontrola przepływu” wydaje mi się zbyt skomplikowana.
Każda konstrukcja lub element wyjęte z kontekstu mogą mieć tę cechę.
Abstrakcje są dobre. Ulepszam je za pomocą dwóch wskazówek:
Lepiej nie streszczać zbyt wcześnie. Poczekaj na więcej przykładów wzorów przed zatarciem. „Więcej” jest oczywiście subiektywne i specyficzne dla trudnej sytuacji.
Unikaj zbyt wielu poziomów abstrakcji tylko dlatego, że abstrakcja jest dobra. Programiści będą musieli zachować te poziomy w głowie, aby uzyskać nowy lub zmieniony kod, gdy zgłębią bazę kodu i przejdą 12 poziomów w głąb. Pragnienie dobrze wyodrębnionego kodu może prowadzić do tak wielu poziomów, że jest to trudne dla wielu ludzi. Prowadzi to również do baz kodowych utrzymywanych tylko przez ninja.
W obu przypadkach „więcej” i „zbyt wiele” nie są stałymi liczbami. To zależy. To sprawia, że jest to trudne.
Podoba mi się również ten artykuł Sandi Metz
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
powielanie jest znacznie tańsze niż zła abstrakcja
i
wolę powielanie niż zła abstrakcja
źródło