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.Now
dzię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.
Odpowiedzi:
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.Now
brzmi 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.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.
źródło
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:
(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.
źródło
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ć.
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:
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.
źródło
AppSettings
aby uzyskać adres URL lub ścieżkę pliku.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ć.
źródło
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.
Customer
iOrder
na przykład byłyby obiektami. Teraz, gdybym miał zgadywać na podstawie nazw klas, które miałeś jako punkt wyjścia, twojaAccountLogic
klasa 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ćAccount
obiektu, a następnie przekazać go doAccountLogic
klasy i pozwolić tej klasie wprowadzić zmiany wAccount
obiekcie. To jest tak zwany model anemiczny i nie reprezentuje zbyt dobrze OO. Zamiast tego twójAccount
klasa powinna mieć zachowanie, takie jakAccount.Close()
lubAccount.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. TwojaAccount
klasa może więc zależeć na przykład od anIEmailUpdate
, który może być jednym z wielu interfejsów zaimplementowanych przezAccountRepository
obiekt. Nie chcesz przekazywać całegoIAccountRepository
interfejsu doAccount
obiektu, ponieważ prawdopodobnie zrobiłby zbyt wiele, na przykład wyszukiwanie i znajdowanie innych (dowolnych) kont, do których obiekt może nieAccount
mieć dostępu, ale mimo toAccountRepository
może implementować obaIAccountRepository
iIEmailUpdate
interfejsyAccount
obiekt 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.
źródło
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:
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.
źródło
+++
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).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.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:
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).
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.
źródło