Jestem młodszym programistą wśród seniorów i mam duże problemy ze zrozumieniem ich sposobu myślenia i rozumowania.
Czytam DDD ( Domain-Driven Design ) i nie rozumiem, dlaczego musimy tworzyć tak wiele klas. Jeśli zastosujemy tę metodę projektowania oprogramowania, otrzymamy 20-30 klas, które można zastąpić maksymalnie dwoma plikami i 3-4 funkcjami. Tak, może być bałagan, ale jest o wiele łatwiejszy w utrzymaniu i czytelny.
Ilekroć chcę zobaczyć, co robi jakiś rodzaj EntityTransformationServiceImpl
, muszę śledzić wiele klas, interfejsów, ich wywołań funkcji, konstruktorów, ich tworzenia i tak dalej.
Prosta matematyka:
- 60 linii fałszywego kodu w porównaniu z 10 klasami X 10 (powiedzmy, że mamy zupełnie inną taką logikę) = 600 linii niechlujnego kodu w porównaniu do 100 klas + więcej do zawinięcia i zarządzania nimi; nie zapomnij dodać zastrzyku zależności.
- Czytanie 600 wierszy niechlujnego kodu = jeden dzień
- 100 zajęć = tydzień, wciąż zapomnij, który z nich robi co, kiedy
Wszyscy mówią, że jest łatwy w utrzymaniu, ale po co? Za każdym razem, gdy dodajesz nową funkcjonalność, dodajesz pięć kolejnych klas z fabrykami, jednostkami, usługami i wartościami. Wydaje mi się, że ten rodzaj kodu porusza się znacznie wolniej niż nieporządny kod.
Powiedzmy, że jeśli napiszesz niechlujny kod 50K LOC w ciągu jednego miesiąca, sprawa DDD wymaga wielu recenzji i zmian (nie mam nic przeciwko testom w obu przypadkach). Jeden prosty dodatek może zająć tydzień, jeśli nie więcej.
W ciągu jednego roku piszesz dużo niechlujnego kodu, a nawet możesz wielokrotnie go przepisywać, ale w stylu DDD wciąż nie masz wystarczającej liczby funkcji, aby konkurować z niechlujnym kodem.
Proszę wytłumacz. Dlaczego potrzebujemy tego stylu DDD i wielu wzorów?
UPD 1 : Otrzymałem tak wiele świetnych odpowiedzi, czy możecie gdzieś dodać komentarz lub edytować swoją odpowiedź za pomocą linku do listy lektur (nie jestem pewien, od czego zacząć, DDD, wzorce projektowe, UML, kod ukończenia, refaktoryzacja, Pragmatic,. .. tylu dobrych książek), oczywiście z sekwencją, abym mógł zacząć rozumieć i stać się starszym, jak niektórzy z was.
źródło
Odpowiedzi:
Jest to problem z optymalizacją
Dobry inżynier rozumie, że problem optymalizacji nie ma znaczenia bez celu. Nie można po prostu optymalizacji, trzeba zoptymalizować dla czegoś. Na przykład opcje kompilatora obejmują optymalizację szybkości i optymalizację rozmiaru kodu; są to czasem przeciwne cele.
Chciałbym powiedzieć mojej żonie, że moje biurko jest zoptymalizowane pod kątem dodatków. To tylko stos i bardzo łatwo jest dodawać różne rzeczy. Moja żona wolałaby, gdybym zoptymalizował pod kątem wyszukiwania, tj. Trochę uporządkował moje rzeczy, aby móc znaleźć rzeczy. To oczywiście utrudnia dodawanie.
Oprogramowanie działa w ten sam sposób. Z pewnością możesz zoptymalizować tworzenie produktu - wygeneruj tonę monolitycznego kodu tak szybko, jak to możliwe, nie martwiąc się o jego uporządkowanie. Jak już zauważyłeś, może to być bardzo, bardzo szybkie. Alternatywą jest optymalizacja pod kątem konserwacji - utrudnienie tworzenia, ale modyfikacje łatwiejsze lub mniej ryzykowne. Taki jest cel kodu strukturalnego.
Sugerowałbym, że odnoszący sukcesy program zostanie utworzony tylko raz, ale zmodyfikowany wiele, wiele razy. Doświadczeni inżynierowie widzieli, jak nieustrukturyzowane bazy kodu zaczynają żyć własnym życiem i stają się produktami, które stają się coraz większe i bardziej złożone, aż nawet niewielkie zmiany są bardzo trudne do wprowadzenia bez wprowadzenia dużego ryzyka. Jeśli kod ma strukturę, ryzyko można ograniczyć. Właśnie dlatego zadajemy sobie tyle trudu.
Złożoność wynika z relacji, a nie z elementów
Zauważyłem w twojej analizie, że patrzysz na ilości - ilość kodu, liczbę klas itp. Chociaż są one dość interesujące, prawdziwy wpływ wywierają relacje między elementami, które eksplodują kombinatorycznie. Na przykład, jeśli masz 10 funkcji i nie wiesz, od czego zależy, masz 90 możliwych relacji (zależności), musisz się martwić - każda z dziesięciu funkcji może zależeć od którejkolwiek z dziewięciu pozostałych funkcji i 9 x 10 = 90. Być może nie masz pojęcia, które funkcje modyfikują zmienne lub w jaki sposób dane są przekazywane, więc koderzy mają mnóstwo rzeczy do zmartwienia podczas rozwiązywania konkretnego problemu. Dla kontrastu, jeśli masz 30 klas, ale są one sprytnie ułożone, mogą mieć zaledwie 29 relacji, np. Jeśli są ułożone warstwowo lub ułożone w stos.
Jak to wpływa na przepustowość twojego zespołu? Cóż, jest mniej zależności, problem jest znacznie łatwiejszy do rozwiązania; koderzy nie muszą żonglować milionem rzeczy w swoich głowach, ilekroć dokonają zmiany. Zatem minimalizowanie zależności może znacznie zwiększyć zdolność kompetentnego rozumowania problemu. Dlatego dzielimy rzeczy na klasy lub moduły, a zmienne zakresowe możliwie najściślej i stosujemy zasady SOLID .
źródło
EntityTransformationServiceImpl
środku, musisz dowiedzieć się, jak działa cała rzecz, zanim będziesz w stanie to naprawić - ale jeśli np. Coś jest nie tak z formatem iFormatter
klasą to obsługuje, musisz tylko nauczyć się, jak ta część działa . Plus czytanie własnego kodu po ~ 3 miesiącach jest jak czytanie kodu nieznajomego.) Mam wrażenie, że większość z nas podświadomie o tym myślała, ale może to wynikać z doświadczenia. Nie jestem tego w 100% pewien.Po pierwsze, czytelność i łatwość konserwacji są często w oczach patrzącego.
To, co jest dla ciebie czytelne, może nie być dla twojego sąsiada.
Utrzymywalność często sprowadza się do wykrywalności (łatwość zachowania lub koncepcji odkrytej w bazie kodu), a wykrywalność to kolejna subiektywna rzecz.
DDD
Jednym ze sposobów, w jaki DDD pomaga zespołom programistów, jest sugerowanie określonego (ale wciąż subiektywnego) sposobu organizowania koncepcji i zachowań kodu. Ta konwencja ułatwia odkrywanie rzeczy, a tym samym łatwiejsze utrzymanie aplikacji.
Taki układ nie jest obiektywnie łatwiejszy do utrzymania. Jest to jednak znacznie łatwiejsze do utrzymania, gdy wszyscy rozumieją, że działają w kontekście DDD.
Klasy
Klasy pomagają w utrzymaniu, czytelności, wykrywalności itp., Ponieważ są dobrze znaną konwencją .
W ustawieniach obiektowych klasy są zwykle używane do grupowania ściśle powiązanych zachowań i do enkapsulacji stanu, który należy dokładnie kontrolować.
Wiem, że to brzmi bardzo abstrakcyjnie, ale możesz o tym pomyśleć w ten sposób:
Dzięki Classes niekoniecznie musisz wiedzieć, jak działa w nich kod. Musisz tylko wiedzieć, za co klasa jest odpowiedzialna.
Klasy pozwalają uzasadnić swoją aplikację pod względem interakcji między dobrze zdefiniowanymi komponentami .
Zmniejsza to obciążenie poznawcze podczas wnioskowania na temat działania aplikacji. Zamiast pamiętać o tym, co osiąga 600 linii kodu , możesz pomyśleć o interakcji 30 komponentów .
Biorąc pod uwagę, że te 30 składników prawdopodobnie obejmuje 3 warstwy aplikacji, prawdopodobnie tylko każdy musi mieć na uwadze około 10 składników na raz.
To wydaje się całkiem wykonalne.
Podsumowanie
Zasadniczo to, co robią starsi deweloperzy, to:
Dzielą aplikację na łatwe do uzasadnienia na temat klas.
Następnie organizują je w łatwe do uzasadnienia warstwy.
Robią to, ponieważ wiedzą, że wraz ze wzrostem aplikacji coraz trudniej jest ją rozumieć jako całość. Podział na warstwy i klasy oznacza, że nigdy nie muszą rozumować o całej aplikacji. Muszą tylko rozumować o niewielkiej ich części.
źródło
AddressServiceRepositoryProxyInjectorResolver
kiedy możesz rozwiązać problem bardziej elegancko? Wzory projektowe ze względu na wzorce projektowe prowadzą do niepotrzebnej złożoności i szczegółowości.Po pierwsze, uwaga: ważną częścią DDD nie są wzorce , ale dostosowanie wysiłków rozwojowych do biznesu. Greg Young zauważył, że rozdziały w niebieskiej książce są w niewłaściwej kolejności .
Ale na twoje konkretne pytanie: istnieje o wiele więcej klas, niż można się spodziewać, (a) ponieważ podjęto wysiłek, aby odróżnić zachowanie domeny od hydrauliki i (b) ponieważ podjęto dodatkowy wysiłek, aby zapewnić, że koncepcje w modelu domeny są wyrażone jawnie.
Mówiąc wprost, jeśli masz dwie różne koncepcje w domenie, powinny one być odrębne w modelu, nawet jeśli akurat mają tę samą reprezentację w pamięci .
W efekcie budujesz język specyficzny dla domeny, który opisuje Twój model w języku firmy, tak aby ekspert domeny mógł go zobaczyć i wykryć błędy.
Dodatkowo widzisz nieco więcej uwagi poświęconej oddzielaniu obaw; oraz pojęcie izolowania niektórych zdolności konsumentów od szczegółów wdrożenia. Zobacz DL Parnas . Wyraźne granice umożliwiają zmianę lub rozszerzenie implementacji bez efektu falowania całego rozwiązania.
Motywacja tutaj: w przypadku aplikacji, która jest częścią podstawowej kompetencji firmy (czyli miejsca, w którym czerpie przewagę konkurencyjną), będziesz chciał móc łatwo i tanio zastąpić zachowanie domeny lepszą odmianą. W efekcie masz części programu, które chcesz szybko ewoluować (jak ewoluuje stan w czasie) oraz inne części, które chcesz zmieniać powoli (jak stan jest przechowywany); dodatkowe warstwy abstrakcji pomagają uniknąć przypadkowego połączenia jednego z drugim.
Mówiąc szczerze: niektóre z nich to także Uszkodzenie Mózgu zorientowane obiektowo. Wzory pierwotnie opisane przez Evansa są oparte na projektach Java, w których uczestniczył ponad 15 lat temu; stan i zachowanie są ściśle powiązane w tym stylu, co prowadzi do komplikacji, których wolałbyś unikać; patrz Percepcja i działanie Stuarta Hallowaya lub Inlining Code autorstwa Johna Carmacka.
źródło
W innych odpowiedziach jest wiele dobrych punktów, ale myślę, że pomijają lub nie podkreślają ważnego błędu koncepcyjnego, który popełniacie:
Porównujesz wysiłek, aby zrozumieć cały program.
W przypadku większości programów nie jest to realistyczne zadanie. Nawet proste programy składają się z tak dużej ilości kodu, że po prostu niemożliwe jest zarządzanie nimi wszystkimi w danym momencie. Jedyną szansą jest znalezienie części programu, która jest odpowiednia do danego zadania (naprawa błędu, wdrożenie nowej funkcji) i praca z tym.
Jeśli twój program składa się z ogromnych funkcji / metod / klas, jest to prawie niemożliwe. Musisz zrozumieć setki wierszy kodu, aby zdecydować, czy ta część kodu jest odpowiednia dla Twojego problemu. Dzięki podanym szacunkom łatwo jest spędzić tydzień, aby znaleźć fragment kodu, nad którym musisz popracować.
Porównaj to z bazą kodu z nazwanymi i uporządkowanymi małymi funkcjami / metodami / klasami w pakietach / przestrzeniach nazw, co sprawia, że oczywiste jest, gdzie znaleźć / umieścić daną logikę. Po prawidłowym wykonaniu w wielu przypadkach możesz wskoczyć w odpowiednie miejsce, aby rozwiązać problem, lub przynajmniej w miejsce, z którego rozpalenie twojego debuggera doprowadzi Cię we właściwe miejsce w kilku przeskokach.
Pracowałem w obu rodzajach systemów. Różnica może być z łatwością rzędu dwóch rzędów wielkości w przypadku porównywalnych zadań i porównywalnej wielkości systemu.
Wpływa to na inne działania:
źródło
Ponieważ testowanie kodu jest trudniejsze niż pisanie kodu
Wiele odpowiedzi podało dobre uzasadnienie z perspektywy dewelopera - że konserwację można zmniejszyć, kosztem uczynienia kodu bardziej uciążliwym do pisania.
Należy jednak wziąć pod uwagę jeszcze jeden aspekt - testowanie może być tylko drobnoziarniste, tak jak oryginalny kod.
Jeśli wszystko piszesz w monolicie, jedynym skutecznym testem, jaki możesz napisać, jest „biorąc pod uwagę te dane wejściowe, czy dane wyjściowe są prawidłowe?”. Oznacza to, że wszelkie znalezione błędy mają zasięg „gdzieś w tej gigantycznej kupie kodu”.
Oczywiście możesz wtedy poprosić programistę o zasadzenie się w debugerze i znalezienie dokładnie tego, gdzie zaczął się problem, i pracę nad poprawką. Wymaga to wielu zasobów i jest złym wykorzystaniem czasu programisty. Wyobraź sobie, że masz coraz mniejszy błąd, a programista musi ponownie debugować cały program.
Rozwiązanie: wiele mniejszych testów, z których każdy wskazuje jeden konkretny, potencjalny błąd.
Te małe testy (na przykład testy jednostkowe) mają tę zaletę, że sprawdzają określony obszar bazy kodu i pomagają znaleźć błędy w ograniczonym zakresie. To nie tylko przyspiesza debugowanie, gdy test się nie powiedzie, ale także oznacza, że jeśli wszystkie małe testy zakończą się niepowodzeniem - łatwiej można znaleźć awarię w większych testach (tj. Jeśli nie jest to konkretna testowana funkcja, musi ona znajdować się w interakcji między nimi).
Jak powinno być jasne, wykonanie mniejszych testów oznacza, że twoja baza kodu musi zostać podzielona na mniejsze testowalne fragmenty. Sposób na to, w dużej komercyjnej bazie kodu, często skutkuje kodem, który wygląda jak ten, nad którym pracujesz.
Na marginesie: nie oznacza to, że ludzie nie posuną się „za daleko”. Istnieje jednak uzasadniony powód dzielenia baz kodowych na mniejsze / mniej połączone części - jeśli jest to rozsądne.
źródło
Wielu (większość ...) z nas naprawdę ich nie potrzebuje . Teoretycy i bardzo zaawansowani, doświadczeni programiści piszą książki na temat teorii i metodologii w wyniku wielu badań i ich głębokiego doświadczenia - to nie znaczy, że wszystko, co piszą, dotyczy każdego programisty w ich codziennej praktyce.
Jako młodszy programista dobrze jest czytać książki, takie jak ta, o której wspomniałeś, aby poszerzyć swoje perspektywy i uświadomić sobie pewne problemy. Pozwoli ci to również uniknąć zawstydzenia i pomieszania, gdy Twoi starsi koledzy używają terminologii, której nie znasz. Jeśli znajdziesz coś bardzo trudnego i wydaje się, że nie ma sensu lub nie wydaje się przydatny, nie zabijaj się nad tym - po prostu zanotuj sobie w głowie, że istnieje taka koncepcja lub podejście.
W codziennym rozwoju, chyba że jesteś akademikiem, Twoim zadaniem jest znalezienie wykonalnych, możliwych do utrzymania rozwiązań. Jeśli pomysły, które znajdziesz w książce, nie pomogą Ci osiągnąć tego celu, nie martw się o to w tej chwili, o ile twoja praca jest uważana za zadowalającą.
Może przyjść chwila, gdy odkryjesz, że możesz użyć części tego, o czym czytałeś, ale na początku nie do końca „dostałeś”, a może nie.
źródło
AccountServiceFacadeInjectResolver
(prawdziwy przykład, który właśnie znalazłem w systemie w pracy) - odpowiedź najprawdopodobniej nie.Ponieważ twoje pytanie obejmuje wiele gruntów i wiele założeń, wyróżnię temat twojego pytania:
My nie. Nie ma ogólnie przyjętej reguły, która mówi, że we wzorcach projektowych musi istnieć wiele klas.
Istnieją dwa kluczowe przewodniki dotyczące decydowania, gdzie umieścić kod i jak podzielić zadania na różne jednostki kodu:
Dlaczego te dwa powinny być ważne?
Te dwa aspekty są podstawowymi „sterownikami” do dowolnego wyboru „gdzie umieścić” w dowolnym języku programowania i dowolnym paradygmacie (nie tylko OO). Nie wszyscy są tego wyraźnie świadomi i potrzeba czasu, często lat, aby naprawdę zakorzenić się w automatycznym odczuciu, w jaki sposób wpływają one na oprogramowanie.
Oczywiście te dwa pojęcia nie mówią nic o tym, co należy zrobić . Niektórzy popełniają błędy po stronie zbyt wielu, inni po stronie za mało. Niektóre języki (patrząc na ciebie tutaj, Java) mają tendencję do faworyzowania wielu klas z powodu wyjątkowo statycznej i pedantycznej natury samego języka (nie jest to wyrażenie wartości, ale takie, jakie jest). Staje się to szczególnie zauważalne, gdy porównasz go z dynamicznymi i bardziej ekspresyjnymi językami, na przykład Ruby.
Innym aspektem jest to, że niektórzy ludzie opowiadają się za zwinnym podejściem polegającym na pisaniu tylko kodu, który jest w tej chwili konieczny , a później, w razie potrzeby, dokonują refaktoryzacji później. W tym stylu rozwoju nie stworzyłbyś,
interface
gdy masz tylko jedną klasę implementującą. Po prostu zaimplementujesz konkretną klasę. Jeśli później będziesz potrzebować drugiej klasy, dokonaj refaktoryzacji.Niektóre osoby po prostu nie działają w ten sposób. Tworzą interfejsy (lub, bardziej ogólnie, abstrakcyjne klasy podstawowe) dla wszystkiego, co kiedykolwiek mogłoby być użyte bardziej ogólnie; prowadzi to szybko do wybuchu klasy.
Ponownie są argumenty za i przeciw, i nie ma znaczenia, który ja lub ty wolę. W swoim życiu jako programista napotkasz wszelkie skrajności, od długich metod spaghetti, poprzez oświecone, wystarczająco duże projekty klasowe, aż do niewiarygodnie rozbudowanych schematów klasowych, które są o wiele nadinżynieryjne. W miarę zdobywania doświadczenia wrastasz w role „architektoniczne” i możesz zacząć wpływać na to w pożądanym kierunku. Dowiesz się o złotym środku dla siebie i nadal przekonasz się, że wiele osób się z tobą nie zgadza, cokolwiek zrobisz.
Dlatego najważniejsze jest tutaj zachowanie otwartego umysłu i to byłaby moja podstawowa rada, ponieważ wydaje się, że odczuwasz wielki ból w tej sprawie, sądząc po pozostałej części pytania ...
źródło
Doświadczeni koderzy nauczyli się:
źródło
Odpowiedzi do tej pory, wszystkie są dobre, zaczynając od rozsądnego założenia, że pytającemu brakuje czegoś, co również pyta. Możliwe jest również, że pytający ma w zasadzie rację i warto omówić, w jaki sposób może dojść do takiej sytuacji.
Doświadczenie i praktyka są potężne, a jeśli seniorzy zdobędą doświadczenie w dużych, złożonych projektach, w których jedynym sposobem na kontrolowanie wszystkiego jest wiele
EntityTransformationServiceImpl
, to stają się szybkie i wygodne dzięki wzorom projektowym i ścisłemu przestrzeganiu DDD. Byłyby one znacznie mniej skuteczne przy zastosowaniu lekkiego podejścia, nawet w przypadku małych programów. Jak dziwny, powinieneś się dostosować, a będzie to wspaniałe doświadczenie edukacyjne.Jednak dostosowując się, powinieneś traktować to jako lekcję równowagi między głębokim uczeniem się jednego podejścia, do tego stopnia, że możesz go uruchomić w dowolnym miejscu, w przeciwieństwie do pozostania wszechstronnym i wiedząc, jakie narzędzia są dostępne, niekoniecznie będąc ekspertem w żadnym z nich. Oba mają zalety i oba są potrzebne na świecie.
źródło
Zwiększenie klasy i funkcjonalności zgodnie z przeznaczeniem będzie świetnym podejściem do rozwiązywania problemów w określony sposób, który pomoże w przyszłości rozwiązać wszelkie problemy.
Wiele klas pomaga zidentyfikować jako podstawową pracę, a każdą klasę można wywołać w dowolnym momencie.
Jednak jeśli masz wiele klas i funkcji z ich dostępnej nazwy, łatwo jest do nich zadzwonić i zarządzać. Nazywa się to czystym kodem.
źródło