Zarządzanie i organizacja znacznie większej liczby klas po przejściu na SOLID?


49

W ciągu ostatnich kilku lat powoli przechodziliśmy na stopniowo coraz lepszy kod, kilka kroków naraz. W końcu zaczynamy przestawiać się na coś, co przynajmniej przypomina SOLID, ale jeszcze tam nie jesteśmy. Od czasu dokonania zmiany, jednym z największych zarzutów ze strony programistów jest to, że nie mogą znieść recenzowania i przeglądania dziesiątek plików, w których wcześniej każde zadanie wymagało jedynie od programisty dotknięcia 5-10 plików.

Przed przystąpieniem do zmiany nasza architektura była zorganizowana w sposób podobny do następującego (przyznane, z jednym lub dwoma rzędami wielkości więcej plików):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

Jeśli chodzi o pliki, wszystko było niesamowicie liniowe i kompaktowe. Było oczywiście dużo duplikacji kodu, ścisłego sprzężenia i bólów głowy, jednak każdy mógł to przebrnąć i to rozgryźć. Kompletni nowicjusze, ludzie, którzy nigdy wcześniej nie otwierali Visual Studio, mogli to zrozumieć w ciągu zaledwie kilku tygodni. Brak ogólnej złożoności plików sprawia, że ​​początkujący programiści i nowi pracownicy mogą stosunkowo łatwo rozpocząć pracę bez zbyt długiego czasu przyspieszania. Ale jest to w zasadzie miejsce, w którym wszelkie zalety stylu kodu wychodzą przez okno.

Z całego serca popieram każdą próbę ulepszenia naszej bazy kodu, ale bardzo często zdarza się, że reszta zespołu reaguje na tak masywne zmiany paradygmatu. Kilka największych obecnie utrzymujących się punktów to:

  • Testy jednostkowe
  • Klasa liczyć
  • Złożoność recenzji

Testy jednostkowe były niezwykle trudne do sprzedania zespołowi, ponieważ wszyscy uważają, że to strata czasu i że są w stanie przetestować swój kod znacznie szybciej niż cały element indywidualnie. Używanie testów jednostkowych jako aprobaty dla SOLID było w większości daremne i stało się w tym momencie żartem.

Liczenie klas jest prawdopodobnie największą przeszkodą do pokonania. Zadania, które kiedyś zajmowały 5–10 plików, mogą teraz zająć 70–100! Podczas gdy każdy z tych plików służy odrębnemu celowi, sama ilość plików może być przytłaczająca. Reakcja zespołu to głównie jęki i drapanie głowy. Poprzednio zadanie mogło wymagać jednego lub dwóch repozytoriów, modelu lub dwóch, warstwy logicznej i metody kontrolera.

Teraz, aby zbudować prostą aplikację do zapisywania plików, masz klasę, aby sprawdzić, czy plik już istnieje, klasę do zapisywania metadanych, klasę do wyodrębnienia, DateTime.Nowdzięki czemu możesz wprowadzać czasy do testów jednostkowych, interfejsy dla każdego pliku zawierającego logikę, pliki zawiera testy jednostkowe dla każdej klasy i jeden lub więcej plików, aby dodać wszystko do kontenera DI.

W przypadku małych i średnich aplikacji SOLID to bardzo łatwa sprzedaż. Wszyscy widzą korzyści i łatwość utrzymania. Jednak po prostu nie widzą dobrej oferty dla SOLID w aplikacjach na bardzo dużą skalę. Staram się więc znaleźć sposoby na poprawę organizacji i zarządzania, abyśmy mogli pokonać rosnące problemy.


Pomyślałem, że podam nieco mocniejszy przykład woluminu pliku na podstawie ostatnio ukończonego zadania. Dostałem zadanie zaimplementowania niektórych funkcji w jednej z naszych nowych mikrousług, aby otrzymać żądanie synchronizacji plików. Po otrzymaniu żądania usługa wykonuje serię wyszukiwań i kontroli, a następnie zapisuje dokument na dysku sieciowym, a także 2 osobne tabele bazy danych.

Aby zapisać dokument na dysku sieciowym, potrzebowałem kilku konkretnych klas:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

To łącznie 15 klas (z wyłączeniem POCO i rusztowań), aby wykonać dość proste zapisanie. Liczba ta znacznie wzrosła, kiedy musiałem utworzyć POCO do reprezentowania bytów w kilku systemach, zbudowałem kilka repozytoriów, aby komunikować się z systemami stron trzecich, które są niekompatybilne z naszymi innymi ORM, i zbudowałem metody logiczne do obsługi zawiłości niektórych operacji.


52
„Zadania, które kiedyś zajmowały 5–10 plików, mogą teraz zająć 70–100!” Jak do diabła? To nie jest w żaden sposób normalne. Jakie zmiany wprowadzasz, które wymagają zmiany tylu plików?
Euforia

43
Fakt, że musisz zmienić więcej plików na zadanie (znacznie więcej!) Oznacza, że ​​źle robisz SOLID. Chodzi o to, aby uporządkować kod (w miarę upływu czasu) w sposób odzwierciedlający zaobserwowane wzorce zmian, co upraszcza zmiany. Każda zasada w SOLID zawiera uzasadnienie (kiedy i dlaczego powinna być stosowana); wygląda na to, że znaleźliście się w takiej sytuacji, stosując je na ślepo. To samo dotyczy testów jednostkowych (TDD); jeśli robisz to bez dobrej znajomości projektowania / architektury, wykopiesz się w dziurę.
Filip Milovanović

60
Wyraźnie przyjąłeś SOLID jako religię, a nie pragmatyczne narzędzie pomagające w wykonywaniu pracy. Jeśli coś w SOLID sprawia, że ​​więcej pracy lub utrudnia to, nie rób tego.
whatsisname

25
@Euphoric: Problem może wystąpić na dwa sposoby. Podejrzewam, że reagujesz na możliwość, że 70-100 lekcji to przesada. Ale nie jest niemożliwe, że jest to po prostu ogromny projekt, który został wciśnięty w 5-10 plików (wcześniej pracowałam w plikach 20KLOC ...), a 70-100 to właściwie odpowiednia ilość plików.
Flater

18
Istnieje zaburzenie myślenia, które nazywam „chorobą szczęścia obiektowego”, a mianowicie przekonanie, że techniki OO są celem samym w sobie, a nie tylko jedną z wielu możliwych technik obniżania kosztów pracy w dużej bazie kodu. Masz szczególnie zaawansowaną formę, „SOLIDNĄ chorobę szczęścia”. SOLID nie jest celem. Celem jest obniżenie kosztów utrzymania bazy kodu. Oceń swoje propozycje w tym kontekście, a nie czy jest to doktryner SOLID. (To, że twoje propozycje prawdopodobnie nie są doktrynalne SOLID, jest również dobrym punktem do rozważenia.)
Eric Lippert,

Odpowiedzi:


104

Teraz, aby zbudować prostą aplikację do zapisywania plików, masz klasę, aby sprawdzić, czy plik już istnieje, klasę do zapisania metadanych, klasę do wyodrębnienia DateTime.Teraz możesz wprowadzać czasy dla testów jednostkowych, interfejsy dla każdego pliku zawierającego logika, pliki zawierające testy jednostkowe dla każdej klasy oraz jeden lub więcej plików, aby dodać wszystko do kontenera DI.

Myślę, że źle zrozumiałeś ideę jednej odpowiedzialności. Jedyną odpowiedzialnością klasy może być „zapisz plik”. Aby to zrobić, może następnie podzielić tę odpowiedzialność na metodę sprawdzającą, czy plik istnieje, metodę zapisującą metadane itp. Każda z tych metod ma następnie jedną odpowiedzialność, która jest częścią ogólnej odpowiedzialności klasy.

Klasa do abstrakcji DateTime.Nowbrzmi dobrze. Ale potrzebujesz tylko jednego z nich i można by go połączyć z innymi funkcjami środowiska w jedną klasę, odpowiedzialną za wyodrębnianie cech środowiska. Znowu jedna odpowiedzialność z wieloma pododdziałami.

Nie potrzebujesz „interfejsów dla każdego pliku zawierającego logikę”, potrzebujesz interfejsów dla klas, które mają skutki uboczne, np. Tych klas, które odczytują / zapisują pliki lub bazy danych; i nawet wtedy są one potrzebne tylko dla publicznych części tej funkcjonalności. Na przykład w AccountRepo, możesz nie potrzebować żadnych interfejsów, możesz potrzebować tylko interfejsu do faktycznego dostępu do bazy danych, który jest wstrzykiwany do tego repozytorium.

Testy jednostkowe były niezwykle trudne do sprzedania zespołowi, ponieważ wszyscy uważają, że to strata czasu i że są w stanie przetestować swój kod znacznie szybciej niż cały element indywidualnie. Używanie testów jednostkowych jako aprobaty dla SOLID było w większości daremne i stało się w tym momencie żartem.

Sugeruje to, że również źle zrozumiałeś testy jednostkowe. „Jednostka” testu jednostkowego nie jest jednostką kodu. Co to w ogóle jest jednostka kodu? Klasa? Metoda? Zmienna? Instrukcja pojedynczej maszyny? Nie, „jednostka” odnosi się do jednostki izolacji, tj. Kodu, który można wykonać w oderwaniu od innych części kodu. Prostym testem, czy test automatyczny jest testem jednostkowym, jest sprawdzenie, czy można go uruchomić równolegle ze wszystkimi innymi testami jednostkowymi bez wpływu na jego wynik. Jest kilka podstawowych zasad dotyczących testów jednostkowych, ale to jest twój kluczowy miernik.

Jeśli więc części kodu można rzeczywiście przetestować jako całość bez wpływu na inne części, zrób to.

Zawsze bądź pragmatyczny i pamiętaj, że wszystko jest kompromisem. Im bardziej przestrzegasz DRY, tym silniejszy kod musi się stać. Im częściej wprowadzasz abstrakcje, tym łatwiej jest testować kod, ale trudniej jest go zrozumieć. Unikaj ideologii i znajdź równowagę między ideałem a prostotą. Na tym polega optymalizacja wydajności i konserwacji.


27
Chciałbym dodać, że podobny ból głowy pojawia się, gdy ludzie próbują stosować się do zbyt często powtarzanej mantry „metody powinny robić tylko jedną rzecz” i kończą się tonami metod jednowierszowych tylko dlatego, że można je technicznie przekształcić w metodę .
Logarr

8
Re „Zawsze bądź pragmatyczny i pamiętaj, że wszystko jest kompromisem” : uczniowie wuja Boba nie są z tego znani (bez względu na pierwotne zamiary).
Peter Mortensen

13
Podsumowując pierwszą część, zwykle masz stażystkę na kawę, a nie pełny zestaw plug-in-perkolatora, flip-switch, sprawdź, czy cukier wymaga uzupełnienia, otwarta lodówka, wyjmij mleko, weź - łyżki, kubki, kubki do kawy, dodatek cukru, mleko do dodania, kubki do mieszania i stażyści do filiżanek. ; P
Justin Time 2 Przywróć Monikę

12
Główną przyczyną problemu PO wydaje się niezrozumienie różnicy między funkcjami, które powinny wykonywać jedno zadanie, a klasami, które powinny mieć jedną odpowiedzialność.
alephzero

6
„Zasady mają na celu prowadzenie mądrych ludzi i posłuszeństwo głupców”. - Douglas Bader
Calanus

29

Zadania, które kiedyś zajmowały 5–10 plików, mogą teraz zająć 70–100!

Jest to przeciwieństwo zasady pojedynczej odpowiedzialności (SRP). Aby dojść do tego punktu, musisz podzielić swoją funkcjonalność w bardzo drobiazgowy sposób, ale nie o to chodzi w SRP - ignorowanie kluczowej idei spójności .

Według SRP oprogramowanie powinno być podzielone na moduły zgodnie z możliwymi przyczynami zmiany, tak aby pojedynczą zmianę projektu można było zastosować w jednym module bez konieczności modyfikacji w innym miejscu. Pojedynczy „moduł” w tym sensie może odpowiadać więcej niż jednej klasie, ale jeśli jedna zmiana wymaga dotknięcia dziesiątek plików, to naprawdę jest to wiele zmian lub źle robisz SRP.

Bob Martin, który pierwotnie sformułował SRP, napisał post na blogu kilka lat temu, aby spróbować wyjaśnić sytuację. Długo dyskutuje, jaki jest „powód zmiany” dla celów SRP. Warto przeczytać w całości, ale wśród rzeczy, które zasługują na szczególną uwagę, jest to alternatywne sformułowanie SRP:

Zbierz razem rzeczy, które zmieniają się z tych samych powodów . Oddziel rzeczy, które zmieniają się z różnych powodów.

(moje podkreślenie). SRP nie polega na dzieleniu rzeczy na najmniejsze możliwe elementy. To nie jest dobry projekt, a Twój zespół ma rację, aby się oprzeć. Utrudnia to aktualizację i utrzymanie bazy kodu. Wygląda na to, że próbujesz sprzedać swój zespół na podstawie testów jednostkowych, ale to oznaczałoby postawienie wózka przed koniem.

Podobnie, zasada segregacji interfejsów nie powinna być traktowana jako absolutna. To nie jest powód do dzielenia twojego kodu tak drobiazgowo, jak SRP, i ogólnie dość dobrze dopasowuje się do SRP. To, że interfejs zawiera pewne metody, których nie używają niektórzy klienci, nie jest powodem do jego zerwania. Znowu szukasz spójności.

Ponadto wzywam was, abyście nie brali pod uwagę zasady otwartego zamknięcia ani zasady substytucji Liskowa jako przyczyny faworyzowania hierarchii głębokiego dziedziczenia. Nie ma mocniejszego sprzężenia niż podklasa z jej nadklasami, a ścisłe połączenie jest problemem projektowym. Zamiast tego faworyzuj kompozycję zamiast dziedziczenia wszędzie tam, gdzie ma to sens. Zmniejszy to sprzężenie, a tym samym liczbę plików, których może wymagać konkretna zmiana, i ładnie dopasowuje się do inwersji zależności.


1
Chyba po prostu próbuję ustalić, gdzie jest linia. W ostatnim zadaniu musiałem wykonać dość prostą operację, ale było to w bazie kodu bez większego rusztowania lub funkcjonalności. W związku z tym wszystko, co musiałem zrobić, było bardzo proste, ale wszystko było dość wyjątkowe i nie pasowało do wspólnych zajęć. W moim przypadku musiałem zapisać dokument na dysku sieciowym i zalogować go do dwóch oddzielnych tabel bazy danych. Zasady otaczające każdy krok były dość szczegółowe. Nawet generowanie nazw plików (prosty przewodnik) miało kilka klas, aby uczynić testowanie wygodniejszym.
JD Davis,

3
Ponownie, @JDDavis, wybór wielu klas zamiast jednej wyłącznie dla celów testowych, stawia wózek przed koniem i idzie bezpośrednio w kierunku SRP, co wymaga grupowania spójnych funkcji razem. Nie mogę udzielić szczegółowych informacji, ale problem polegający na tym, że poszczególne zmiany funkcjonalne wymagają modyfikacji wielu plików, to problem, którym należy się zająć (i starać się go unikać), a nie problem, który należy próbować uzasadnić.
John Bollinger

Zgadzam się, dodaję to. Cytując Wikipedię, „Martin definiuje odpowiedzialność jako powód zmiany i stwierdza, że ​​klasa lub moduł powinien mieć jeden i tylko jeden powód do zmiany (tj. Przepisania)”. i „ostatnio stwierdził„ Ta zasada dotyczy ludzi ”.„ W rzeczywistości uważam, że oznacza to, że „odpowiedzialność” w SRP dotyczy interesariuszy, a nie funkcjonalności. Klasa powinna być odpowiedzialna za zmiany wymagane tylko przez jednego interesariusza (osobę wymagającą zmiany programu), tak aby zmieniać jak najmniej rzeczy w odpowiedzi na różne zainteresowane strony żądające zmiany.
Corrodias

12

Zadania, które kiedyś zajmowały 5–10 plików, mogą teraz zająć 70–100!

To jest kłamstwo. Zadania nigdy nie trwały tylko 5-10 plików.

Nie rozwiązujesz żadnych zadań z mniej niż 10 plikami. Dlaczego? Ponieważ używasz C #. C # to język wysokiego poziomu. Używasz ponad 10 plików tylko po to, by stworzyć hello world.

Och, na pewno ich nie zauważysz, ponieważ ich nie napisałeś. Więc nie patrzysz na nie. Ufasz im.

Problemem nie jest liczba plików. To, że teraz dzieje się tak wiele, że nie ufasz.

Zastanów się, jak sprawić, by te testy działały do ​​tego stopnia, że ​​po ich przejściu ufasz tym plikom tak, jak ufasz plikom w .NET. Takie postępowanie jest celem testów jednostkowych. Nikt nie dba o liczbę plików. Dbają o liczbę rzeczy, którym nie mogą ufać.

W przypadku małych i średnich aplikacji SOLID to bardzo łatwa sprzedaż. Wszyscy widzą korzyści i łatwość utrzymania. Jednak po prostu nie widzą dobrej oferty dla SOLID w aplikacjach na bardzo dużą skalę.

Zmiana jest trudna w przypadku aplikacji na bardzo dużą skalę, bez względu na to, co robisz. Najlepsza mądrość do zastosowania tutaj nie pochodzi od wuja Boba. Pochodzi od Michaela Feathersa w jego książce Working Effective with Legacy Code.

Nie zaczynaj przepisywania festu. Stary kod reprezentuje trudną wiedzę. Wyrzucenie go, ponieważ ma problemy i nie jest wyrażone w nowym i ulepszonym paradygmacie X, po prostu prosi o nowy zestaw problemów i nie ma ciężko zdobytej wiedzy.

Zamiast tego znajdź sposoby, aby przetestować swój stary, nieczytelny kod (starszy kod w Feathers mówi). W tej metaforze kod jest jak koszula. Duże części są łączone w naturalnych szwach, które można cofnąć, aby oddzielić kod w sposób, w jaki usuwasz szwy. Zrób to, abyś mógł dołączyć testowe „rękawy”, które pozwolą ci wyodrębnić resztę kodu. Teraz, kiedy tworzysz rękawy testowe, masz do nich zaufanie, ponieważ robiłeś to z roboczą koszulą. (ow, ta metafora zaczyna boleć).

Pomysł ten wynika z założenia, że ​​podobnie jak w większości sklepów, tylko aktualne wymagania dotyczą działającego kodu. Pozwala to zablokować to w testach, które pozwalają wprowadzać zmiany do sprawdzonego działającego kodu bez utraty wszystkich jego sprawdzonych statusów roboczych. Teraz, dzięki pierwszej fali testów, możesz zacząć wprowadzać zmiany, które sprawią, że „starsze” (niesprawdzalne) kody będą testowane. Możesz być odważny, ponieważ testy szwów wspierają cię, mówiąc, że zawsze tak było, a nowe testy pokazują, że Twój kod faktycznie robi to, co myślisz.

Co to ma wspólnego z:

Zarządzanie i organizacja znacznie większej liczby klas po przejściu na SOLID?

Abstrakcja.

Możesz sprawić, że nienawidzę dowolnej bazy kodu ze złymi abstrakcjami. Zła abstrakcja to coś, co każe mi zajrzeć do środka. Nie zaskakuj mnie, kiedy zajrzę do środka. Bądź prawie taki, jak się spodziewałem.

Daj mi dobre imię, czytelne testy (przykłady), które pokazują, jak korzystać z interfejsu, i zorganizuj go, aby móc znaleźć rzeczy i nie będę się przejmować, jeśli użyjemy 10, 100 lub 1000 plików.

Pomagasz mi znaleźć rzeczy o dobrych opisowych nazwach. Umieść rzeczy o dobrych imionach w rzeczach o dobrych imionach.

Jeśli to wszystko zrobisz dobrze, wyodrębnisz pliki do miejsca, w którym zakończenie zadania zależy tylko od 3 do 5 innych plików. 70-100 plików nadal tam jest. Ale chowają się za 3 do 5. To działa tylko wtedy, gdy ufasz 3 do 5, że zrobisz to dobrze.

Więc tak naprawdę potrzebujesz słownictwa, aby wymyślić dobre nazwy dla tych wszystkich rzeczy i testów, którym ludzie ufają, aby przestali brnąć przez wszystko. Bez tego doprowadziłbyś mnie do szaleństwa.

@Delioth ma rację co do rosnących bólów. Kiedy przyzwyczaisz się do naczyń znajdujących się w szafce nad zmywarką, trzeba trochę przyzwyczaić się do bycia nad barem śniadaniowym. Utrudnia niektóre rzeczy. Ułatwia niektóre rzeczy. Ale powoduje koszmary wszelkiego rodzaju, jeśli ludzie nie zgadzają się, dokąd idą naczynia. W dużej bazie kodu problemem jest to, że możesz przenosić tylko niektóre naczynia naraz. Więc teraz masz naczynia w dwóch miejscach. To jest mylące. Trudno uwierzyć, że naczynia są tam, gdzie powinny. Jeśli chcesz to ominąć, jedyne, co musisz zrobić, to przesuwać naczynia.

Problem polega na tym, że naprawdę chciałbyś wiedzieć, czy jedzenie potraw przy barze śniadaniowym jest tego warte, zanim przejdziesz przez te wszystkie bzdury. Cóż, za to wszystko, co mogę polecić, to biwakować.

Podczas testowania nowego paradygmatu po raz pierwszy ostatnim miejscem, w którym powinieneś go zastosować, jest duża baza kodu. Dotyczy to każdego członka zespołu. Nikt nie powinien wierzyć, że SOLID działa, że ​​działa OOP lub że działa programowanie funkcjonalne. Każdy członek zespołu powinien mieć szansę na zabawę z nowym pomysłem, czymkolwiek jest, w projekcie zabawki. Pozwala im przynajmniej zobaczyć, jak to działa. Pozwala im zobaczyć, co nie robi dobrze. Pozwala im to nauczyć się robić to, zanim zrobią wielki bałagan.

Zapewnienie ludziom bezpiecznego miejsca do zabawy pomoże im w przyjęciu nowych pomysłów i da im pewność, że potrawy naprawdę mogą działać w ich nowym domu.


3
Warto wspomnieć, że część bólu zadawanego przez pytanie prawdopodobnie również powoduje narastający ból - podczas gdy tak, być może będą musieli utworzyć 15 plików dla tej jednej rzeczy ... teraz już nigdy nie będą musieli pisać GUIDProvider ani BasePathProvider , lub ExtensionProvider itp. To ta sama przeszkoda, którą napotykasz, rozpoczynając nowy projekt greenfield - wiązki funkcji pomocniczych, które są w większości trywialne, głupie do napisania, a jednak wciąż muszą zostać napisane. Jest do bani, aby je zbudować, ale kiedy już tam będą, nie powinieneś o nich myśleć ... nigdy.
Delioth,

@Delioth Jestem niesamowicie skłonny wierzyć, że tak jest. Poprzednio, jeśli potrzebowaliśmy jakiegoś podzbioru funkcjonalności (powiedzmy, że po prostu chcieliśmy adresu URL mieszczącego się w AppSettings), po prostu mieliśmy jedną ogromną klasę, która została przekazana i wykorzystana. Dzięki nowemu podejściu nie ma powodu, aby omijać całość, AppSettingsaby uzyskać adres URL lub ścieżkę pliku.
JD Davis,

1
Nie zaczynaj przepisywania festu. Stary kod reprezentuje trudną wiedzę. Wyrzucenie go, ponieważ ma problemy i nie jest wyrażone w nowym i ulepszonym paradygmacie X, wymaga tylko nowego zestawu problemów i nie ma trudnej wiedzy. To. Absolutnie.
Flot2011

10

Wygląda na to, że Twój kod nie jest bardzo dobrze oddzielony i / lub Twoje rozmiary zadań są zbyt duże.

Zmiany kodu powinny wynosić 5–10 plików, chyba że przeprowadzasz refaktoryzację w trybie kodowania lub na dużą skalę. Jeśli jedna zmiana dotyczy wielu plików, prawdopodobnie oznacza to, że zmiany są kaskadowe. Niektóre ulepszone abstrakcje (więcej pojedynczej odpowiedzialności, segregacja interfejsu, inwersja zależności) powinny pomóc. Jest również możliwe, że może poszedł za pojedynczy odpowiedzialność i przydałoby się nieco więcej pragmatyzmu - krótsze i cieńsze hierarchie typu. To powinno także ułatwić zrozumienie kodu, ponieważ nie trzeba rozumieć dziesiątek plików, aby wiedzieć, co robi kod.

Może to również oznaczać, że twoja praca jest zbyt duża. Zamiast „hej, dodaj tę funkcję” (która wymaga zmian interfejsu użytkownika i zmian interfejsu API oraz zmian dostępu do danych oraz zmian bezpieczeństwa i zmian testowych i ...) rozbić go na bardziej przydatne części. To staje się łatwiejsze do przejrzenia i łatwiejsze do zrozumienia, ponieważ wymaga ustanowienia przyzwoitych umów między bitami.

I oczywiście testy jednostkowe pomagają w tym wszystkim. Zmuszają cię do stworzenia przyzwoitych interfejsów. Zmuszają cię do uczynienia kodu wystarczająco elastycznym, aby wstrzyknąć bity potrzebne do testowania (jeśli będzie to trudne do przetestowania, będzie trudne do ponownego użycia). I odsuwają ludzi od nadmiernie inżynierskich rzeczy, ponieważ im więcej projektujesz, tym więcej musisz testować.


2
Pliki od 5-10 do 70-100 to nieco więcej niż hipotetyczny. Moim ostatnim zadaniem było stworzenie pewnej funkcjonalności w jednej z naszych nowych mikrousług. Nowa usługa miała otrzymać żądanie i zapisać dokument. W tym celu potrzebowałem klas do reprezentowania jednostek użytkownika w 2 osobnych bazach danych i repozytoriach dla każdej z nich. Repozytoria reprezentują inne tabele, do których musiałem pisać. Dedykowane klasy do obsługi sprawdzania danych plików i generowania nazw. Lista jest długa. Nie wspominając o tym, że każda klasa, która zawierała logikę, była reprezentowana przez interfejs, aby można ją było wykonać na testy jednostkowe.
JD Davis,

1
Jeśli chodzi o nasze starsze podstawy kodu, wszystkie są ściśle ze sobą powiązane i niezwykle monolityczne. Dzięki podejściu SOLID jedyne sprzężenie między klasami miało miejsce w przypadku POCO, wszystko inne jest przekazywane przez DI i interfejsy.
JD Davis

3
@JDDavis - czekaj, dlaczego jedna mikrousługa działa bezpośrednio z wieloma bazami danych?
Telastyn

1
To był kompromis z naszym menedżerem deweloperów. Ogromnie woli oprogramowanie monolityczne i proceduralne. W związku z tym nasze mikrousługi są znacznie bardziej makro, niż powinny. W miarę jak nasza infrastruktura się poprawia, powoli rzeczy staną się własnymi mikrousługami. Na razie nieco podążamy za dusiącym podejściem do przeniesienia niektórych funkcji do mikrousług. Ponieważ wiele usług potrzebuje dostępu do określonego zasobu, przenosimy je również do ich własnych mikrousług.
JD Davis

4

Chciałbym wyjaśnić niektóre rzeczy już tu wspomniane, ale bardziej z perspektywy miejsca, w którym wyznaczane są granice obiektów. Jeśli podążasz za czymś podobnym do projektowania opartego na domenie, Twoje obiekty prawdopodobnie będą reprezentować aspekty Twojej firmy. Customeri Orderna przykład byłyby obiektami. Teraz, gdybym miał zgadywać na podstawie nazw klas, które miałeś jako punkt wyjścia, twoja AccountLogicklasa miałaby kod, który działałby dla dowolnego konta. Jednak w OO każda klasa ma mieć kontekst i tożsamość. Nie powinieneś dostać Accountobiektu, a następnie przekazać go do AccountLogicklasy i pozwolić tej klasie wprowadzić zmiany w Accountobiekcie. To jest tak zwany model anemiczny i nie reprezentuje zbyt dobrze OO. Zamiast tego twójAccountklasa powinna mieć zachowanie, takie jak Account.Close()lub Account.UpdateEmail(), a te zachowania wpłynęłyby tylko na tę instancję konta.

W JAKI sposób obsłużyć te zachowania, można (iw wielu przypadkach należy) odciążyć do zależności reprezentowanych przez abstrakcje (tj. Interfejsy). Account.UpdateEmail, na przykład może chcieć zaktualizować bazę danych lub plik albo wysłać wiadomość do magistrali usług itp. I to może się zmienić w przyszłości. Twoja Accountklasa może więc zależeć na przykład od an IEmailUpdate, który może być jednym z wielu interfejsów zaimplementowanych przez AccountRepositoryobiekt. Nie chcesz przekazywać całego IAccountRepositoryinterfejsu do Accountobiektu, ponieważ prawdopodobnie zrobiłby zbyt wiele, na przykład wyszukiwanie i znajdowanie innych (dowolnych) kont, do których obiekt może nie Accountmieć dostępu, ale mimo to AccountRepositorymoże implementować oba IAccountRepositoryi IEmailUpdateinterfejsyAccountobiekt miałby dostęp tylko do małych części, których potrzebuje. Pomaga to zachować zasadę segregacji interfejsu .

Realistycznie, jak wspomnieli inni ludzie, jeśli masz do czynienia z eksplozją klas, istnieje prawdopodobieństwo, że używasz zasady SOLIDNEJ (a więc i OO) w niewłaściwy sposób. SOLID powinien pomóc ci uprościć kod, a nie komplikować go. Ale potrzeba czasu, aby naprawdę zrozumieć, co oznaczają takie elementy, jak SRP. Ważniejsze jest jednak to, że sposób działania SOLID będzie bardzo zależny od twojej domeny i ograniczonych kontekstów (inny termin DDD). Nie ma srebrnej kuli ani jednego uniwersalnego rozwiązania.

Jeszcze jedna rzecz, którą chciałbym podkreślić dla osób, z którymi pracuję: ponownie obiekt OOP powinien mieć zachowanie i jest w rzeczywistości definiowany przez jego zachowanie, a nie jego dane. Jeśli twój obiekt ma tylko właściwości i pola, nadal zachowuje się, choć prawdopodobnie nie jest to zachowanie, które zamierzałeś. Publicznie zapisywalna / ustawialna właściwość bez żadnej innej logiki sugeruje, że zachowanie jej zawierającej klasy jest takie, że każdy gdziekolwiek z dowolnego powodu i w dowolnym czasie może modyfikować wartość tej właściwości bez jakiejkolwiek niezbędnej logiki biznesowej lub sprawdzania poprawności pomiędzy. Nie jest to zwykle zachowanie, które ludzie zamierzają, ale jeśli masz model anemiczny, jest to zachowanie, które Twoje klasy ogłaszają każdemu, kto ich używa.


2

To łącznie 15 klas (z wyłączeniem POCO i rusztowań), aby wykonać dość proste zapisanie.

To szalone ... ale te zajęcia brzmią jak coś, co sam bym napisał. Spójrzmy na nie. Zignorujmy na razie interfejsy i testy.

  • BasePathProvider- IMHO potrzebuje każdego nietrywialnego projektu pracującego z plikami. Zakładam, że jest już coś takiego i można z niej korzystać w takiej postaci, w jakiej jest.
  • UniqueFilenameProvider - Jasne, już to masz, prawda?
  • NewGuidProvider - Ten sam przypadek, chyba że chcesz tylko użyć GUID.
  • FileExtensionCombiner - Ta sama sprawa.
  • PatientFileWriter - Myślę, że to główna klasa dla bieżącego zadania.

Dla mnie wygląda to dobrze: musisz napisać jedną nową klasę, która potrzebuje czterech klas pomocników. Wszystkie cztery klasy pomocnicze brzmią dość wielokrotnie, więc założę się, że są już gdzieś w twojej bazie kodu. W przeciwnym razie jest to albo pech (czy naprawdę jesteś osobą w zespole, aby pisać pliki i używać identyfikatorów GUID?) Lub inny problem.


Jeśli chodzi o klasy testowe, na pewno po utworzeniu lub aktualizacji nowej klasy należy ją przetestować. Zatem napisanie pięciu klas oznacza także napisanie pięciu klas testowych. Ale to nie komplikuje projektu:

  • Nigdy nie będziesz używać klas testowych w innym miejscu, ponieważ zostaną one wykonane automatycznie i to wszystko.
  • Chcesz jeszcze raz na nie spojrzeć, chyba że zaktualizujesz testowane klasy lub użyjesz ich jako dokumentacji (idealnie, testy pokazują wyraźnie, w jaki sposób klasa powinna być używana).

Jeśli chodzi o interfejsy, są one potrzebne tylko wtedy, gdy środowisko DI lub środowisko testowe nie może poradzić sobie z klasami. Możesz postrzegać je jako opłatę za niedoskonałe narzędzia. Lub możesz postrzegać je jako przydatną abstrakcję, pozwalającą zapomnieć, że są bardziej skomplikowane rzeczy - czytanie źródła interfejsu zajmuje znacznie mniej czasu niż czytanie źródła jego implementacji.


Jestem wdzięczny za ten punkt widzenia. W tym konkretnym przypadku pisałem funkcjonalność w całkiem nowej mikrousługie. Niestety, nawet w naszej głównej bazie kodu, chociaż mamy niektóre z powyższych w użyciu, żadna z nich nie jest tak naprawdę w sposób zdalny do wielokrotnego użytku. Wszystko, co musi być wielokrotnego użytku, skończyło się w jakiejś statycznej klasie lub jest po prostu skopiowane i wklejone wokół kodu. Wydaje mi się, że wciąż posunąłem się trochę za daleko, ale zgadzam się, że nie wszystko trzeba całkowicie przeanalizować i oddzielić.
JD Davis

@JDDavis Próbowałem napisać coś innego niż inne odpowiedzi (z którymi w większości się zgadzam). Za każdym razem, gdy coś kopiujesz i wklejasz, uniemożliwiasz ponowne użycie, ponieważ zamiast uogólniać coś, tworzysz kolejny fragment kodu, którego nie można ponownie użyć, co zmusi cię do skopiowania i wklejenia jednego dnia. IMHO to drugi największy grzech, zaraz po ślepym przestrzeganiu zasad. Musisz znaleźć swój ulubiony punkt, w którym przestrzeganie zasad zwiększa produktywność (szczególnie przy przyszłych zmianach), a czasami ich łamanie pomaga w przypadkach, gdy wysiłek nie byłby nieodpowiedni. Wszystko jest względne.
maaartinus

@JDDavis A wszystko zależy od jakości twoich narzędzi. Przykład: są ludzie, którzy twierdzą, że DI jest przedsiębiorczy i skomplikowany, a ja twierdzę, że w większości jest bezpłatny . +++Jeśli chodzi o łamanie zasad: są cztery klasy, których potrzebuję w miejscach, w których mogłem je wstrzyknąć tylko po poważnym refaktoryzacji, co czyni kod bardziej brzydkim (przynajmniej dla moich oczu), więc postanowiłem zrobić je w singletony (lepszy programista może znaleźć lepszy sposób, ale cieszę się z tego; liczba singletonów nie zmienia się od wieków).
maaartinus

Ta odpowiedź wyraża w przybliżeniu to, o czym myślałem, kiedy OP dodał przykład do pytania. @ JDDavis Dodajmy, że możesz zapisać niektóre kody / klasy typu „plateplate”, używając narzędzi funkcjonalnych do prostych przypadków. Na przykład dostawca GUI - zamiast wtargnąć do nowego interfejsu nową klasę do tego, dlaczego po prostu nie wykorzystać Func<Guid>do tego i wprowadzić anonimowej metody jak ()=>Guid.NewGuid()do konstruktora? I nie ma potrzeby testowania tej funkcji .NET Framework, jest to coś, co Microsoft zrobił dla Ciebie. W sumie pozwoli ci to zaoszczędzić 4 klasy.
Doc Brown

... i powinieneś sprawdzić, czy inne przedstawione przypadki można uprościć w ten sam sposób (prawdopodobnie nie wszystkie).
Doc Brown

2

W zależności od abstrakcji tworzenie klas jednoosobowych i pisanie testów jednostkowych nie są naukami ścisłymi. Uczenie się, przechodzenie za daleko, a następnie znajdowanie normy, która ma sens, jest całkowicie normalne. Wygląda na to, że wahadło za bardzo się obróciło, a może nawet utknęło.

Oto, gdzie podejrzewam, że to znika z szyn:

Testy jednostkowe były niezwykle trudne do sprzedania zespołowi, ponieważ wszyscy uważają, że to strata czasu i że są w stanie przetestować swój kod znacznie szybciej niż cały element indywidualnie. Używanie testów jednostkowych jako aprobaty dla SOLID było w większości daremne i stało się w tym momencie żartem.

Jedną z korzyści wynikających z większości zasad SOLID (z pewnością nie jedyną korzyścią) jest to, że ułatwia pisanie testów jednostkowych dla naszego kodu. Jeśli klasa zależy od abstrakcji, możemy kpić z niej. Abstrakcje posegregowane łatwiej wyśmiewać. Jeśli klasa robi jedną rzecz, prawdopodobnie będzie miała mniejszą złożoność, co oznacza, że ​​łatwiej jest poznać i przetestować wszystkie możliwe ścieżki.

Jeśli Twój zespół nie pisze testów jednostkowych, dzieją się dwie powiązane rzeczy:

Po pierwsze, wykonują oni wiele dodatkowej pracy, aby stworzyć wszystkie te interfejsy i klasy, nie zdając sobie sprawy z pełnych korzyści. Zajmuje trochę czasu i praktyki, aby zobaczyć, jak pisanie testów jednostkowych ułatwia nam życie. Są powody, dla których ludzie, którzy uczą się pisać testy jednostkowe, trzymają się tego, ale trzeba trwać wystarczająco długo, aby je odkryć. Jeśli twój zespół nie próbuje tego, poczuje się, jakby reszta dodatkowej pracy, którą wykonują, jest bezużyteczna.

Na przykład, co dzieje się, gdy trzeba dokonać refaktoryzacji? Jeśli mają sto małych klas, ale nie ma testów, które mogłyby im powiedzieć, czy ich zmiany zadziałają, te dodatkowe klasy i interfejsy będą wydawały się obciążeniem, a nie poprawą.

Po drugie, pisanie testów jednostkowych może pomóc ci zrozumieć, ile abstrakcji naprawdę potrzebuje twój kod. Tak jak powiedziałem, to nie jest nauka. Zaczynamy źle, skręcamy wszędzie i wracamy do zdrowia. Testy jednostkowe mają szczególny sposób na uzupełnienie SOLID. Skąd wiesz, kiedy musisz dodać abstrakcję lub coś rozdzielić? Innymi słowy, skąd wiesz, kiedy jesteś „wystarczająco SOLIDNY”? Często odpowiedź brzmi, gdy nie można czegoś przetestować.

Być może twój kod byłby testowalny bez tworzenia tylu drobnych abstrakcji i klas. Ale jeśli nie piszesz testów, jak możesz to stwierdzić? Jak daleko posuniemy się Możemy mieć obsesję na punkcie niszczenia coraz mniejszych rzeczy. To królicza nora. Umiejętność pisania testów dla naszego kodu pomaga nam zobaczyć, kiedy osiągnęliśmy nasz cel, dzięki czemu możemy przestać mieć obsesję, przejść dalej i dobrze się bawić, pisząc więcej kodu.

Testy jednostkowe nie są srebrną kulą, która rozwiązuje wszystko, ale są naprawdę niesamowitą kulą, która poprawia życie programistów. Nie jesteśmy doskonali, podobnie jak nasze testy. Ale testy dają nam pewność. Oczekujemy, że nasz kod będzie poprawny i jesteśmy zaskoczeni, gdy jest on nieprawidłowy, a nie na odwrót. Nie jesteśmy doskonali i nasze testy też nie są. Ale kiedy testujemy nasz kod, mamy pewność. Podczas wdrażania naszego kodu rzadziej gryziemy paznokcie i zastanawiamy się, co tym razem się zepsuje i czy to będzie nasza wina.

Co więcej, kiedy już się zorientujemy, pisanie testów jednostkowych przyspiesza, a nie spowalnia tworzenie kodu. Spędzamy mniej czasu przeglądając stary kod lub debugując, aby znaleźć problemy, które są jak igły w stogu siana.

Błędy spadają, robimy więcej i zastępujemy lęk pewnością siebie. To nie jest moda ani olej z węża. To jest prawdziwe. Wielu programistów to potwierdzi. Jeśli twój zespół tego nie doświadczył, musi przejść przez tę krzywą uczenia się i pokonać garb. Daj mu szansę, zdając sobie sprawę, że nie uzyskają natychmiastowych rezultatów. Ale kiedy to się stanie, będą zadowoleni, że tak zrobili i nigdy nie będą oglądać się za siebie. (Albo staną się odizolowanymi pariasami i piszą wściekłe posty na blogach o tym, jak testy jednostkowe i większość zgromadzonej wiedzy programistycznej to strata czasu).

Od czasu dokonania zmiany, jednym z największych zarzutów ze strony programistów jest to, że nie mogą znieść recenzowania i przeglądania dziesiątek plików, w których wcześniej każde zadanie wymagało jedynie od programisty dotknięcia 5-10 plików.

Recenzja jest o wiele łatwiejsza, gdy wszystkie testy jednostkowe zakończą się pomyślnie, a duża część tej recenzji polega jedynie na upewnieniu się, że testy są znaczące.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.