Krok 4 | Kult warstw – 5 kroków do zagłady
Mamy już kod, który jest dobrze udokumentowany komentarzami. Funkcjonalności zamknęliśmy w dedykowanych funkcjach. Unikamy duplikacji kodu korzystając ze wspaniałej rzeczy jaką jest dziedziczenie. Nasze aplikacje są naprawdę poważne. Ale do tej pory były co najwyżej duże. Przyszła więc pora na to co prawdziwi programiści lubią najbardziej czyli na porządne, korporacyjne systemy.
Wpis jest częścią cyklu „5 kroków do zagłady”. W tym momencie dostępne wpisy to:
- Wstęp
- Krok 1 – długie metody
- Krok 2 – Komentarze po całości
- Krok 3 – Wielopokoleniowy kod
- Krok 4 – Kult warstw
Seria dostępna jest też w formie wideo na moim kanale w serwisie Youtube.
Poważna sprawa
Takie korporacyjne systemy to już nie jest zabawa. Tutaj wkraczamy w świat formalnych procedur, kontroli nad przepływem czy podziale odpowiedzialności. W jakimś śmiesznym domowym systemiku to możesz sobie zrobić klasę do wyciągania danych z bazy i używać jej w kontrolerze bezpośrednio. Ale nie tutaj! Nawet nie pokazuj nam takich bzdur! Tutaj wszystko musi być zrobione porządnie. Nie można tak po prostu pchać sobie danych na stronę. Rozdzielamy warstwę bazy od warstwy widoku. Zapamiętaj to! Walidacja to też nie są żarty i ma być porządnie zrobiona, mieć swoje miejsce. No i nie zapomnij o warstwie sprawdzania uprawnień, bo przecież dane, na których pracujemy są warte miliony! To wszystko musi być uporządkowane, mieć strukturę i zasady. Dlatego wszystko dzielimy na warstwy. Tak jak przystało na wysokiej klasy biznesowe oprogramowanie.
Dostęp do danych jest niezwykle istotny dlatego dbamy tutaj o spójną i elastyczną strukturę. Zawiera się ona w kilku plikach:
public class Data { public long Id {get;set;} public string Name {get;set;} public DateTime Date {get;set;} public string Description {get;set;} public string Username {get;set;} }
public enum AccessLevel { Edit, Readonly }
public interface IRepository { }
public interface IDataRepository : IRepository { IEnumerable<Data> GetAll(); }
public interface IService {}
public interface IDataService : IService { IEnumerable<DataDto> GetAll(); }
public class DataRepository : IDataRepository { private DbContext context = new DbContext(); public IEnumerable<Data> GetAll() { return context.Datas.ToList(); } }
public class DataDto { public long Id {get;set;} public string Name {get;set;} public DateTime Date {get;set;} public string Description {get;set;} public string Username {get;set;} public AccessLevel AccessLevel {get;set;} }
class DataMapper { public DataDto Map(Data data) { return new DataDto(data.Id, data.Name, data.Date, data.Description, data.Username); } }
public class DataAccessService : IDataService { private DataRepository repository = new DataRepository(); public IEnumerable<DataDto> GetAll() { var dataMapper = new DataMapper(); var data = repository.GetAll().Select(x => dataMapper(x)); return data; } }
public interface IProcessor<TData, TArgs> { IEnumerable<TData> Process(IEnumerable<TData> data, TArgs args); TData Process(TData data, TArgs args); }
public class DataProcessor : IProcessor<DataDto, DataListQuery> { IEnumerable<DataDto> Process(IEnumerable<DataDto> data, DataListQuery args) { return data.Select(x => Process(x)); } DataDto Process(DataDto data, DataListQuery args) { if (data.Username == args.CurrentUser) { data.AccessLevel = AccessLevel.Edit; } else { data.AccessLevel = AccessLevel.Readonly; } return data; } }
public class DataViewModel { public IEnumerable<DataItemViewModel> DataList { get; set; } }
public class DataItemViewModel { public long Id {get;set;} public string Name {get;set;} public DateTime Date {get;set;} public string Description {get;set;} public AccessLevel AccessLevel {get;set;} }
class DataDtoToViewModelMapper { public DataItemViewModel Map(DataDto data) { return new DataItemViewModel { Id = data.Id, Name = data.Name, Date = data.Date, Description = data.Description, AccessLevel = data.AccessLevel }; } }
interface IQuery {}
interface IQueryHandler<TQuery, TResult> where TQuery : IQuery { TResult Handle(TQuery query); }
class DataListQuery { public string CurrentUser { get; } private DataListQuery(string currentUser) { CurrentUser = currentUser; } public static DataListQuery(string currentUser) { if (string.IsNullOrEmpty(currentUser)) throw new ArgumentException(); return new DataListQuery(currentUser); } }
class DataListQueryHandler : IQueryHandler<DataListQuery, DataViewModel> { private DataAccessService service = new DataAccessService(); private DataProcessor processor = new DataProcessor(); private DataDtoToViewModelMapper dtoMapper = new DataDtoToViewModelMapper(); public DataViewModel Handle(DataListQuery query) { if (query == null) throw new ArgumentException(); var data = service.GetAll(); var processedData = processor.Process(data); var dataItems = processedData.Select(x => dtoMapper.Map(x)); var viewModel = new DataViewModel { DataList = dataItems }; return viewModel; } }
class DataController : Controller { private DataListQueryHandler dataListQueryHandler = new DataListQueryHandler(); public ActionResult Index() { dataListQueryHandler.Handle(); } }
Z takim kodem możemy wszystko! I tak to ma wyglądać, bo jak ktoś zrobi po szczeniacku jakieś uproszczenie to zaraz będzie bajzel w kodzie…
Problem
Być może faktycznie z tym kodem można zrobić praktycznie wszystko. Ale wiecie czego nie można zrobić? Wydajnie pracować.
Wiesz co robi ten kod powyżej, który mimo pewnych uproszczeń ma prawie 200 linii? Wyciąga dane z bazy, tym, które należą do aktualnego użytkownika dać możliwość edycji i zmapować na view model. Tylko tyle i aż tyle.
Wysoki próg wejścia
Zanim napisałem za co odpowiedzialny jest ten kod to szczerze odpowiedz sobie ile czasu potrzebowałeś na odkrycie tego? Jestem pewien, że nie było to kilka sekund. Być może nawet jak od razu domyśliłeś się pobierania danych to ilość klas powodowała, że szukałeś jakiejś dodatkowej ukrytej logiki.
W tym wypadku miałeś jednak podane wszystko na tacy bo wstawiłem tylko pliki odpowiedzialne za tą jedną funkcję. Jakby to jednak wyglądało w sytuacji kiedy taka struktura byłaby osadzona w dużym projekcie? Wg mnie byłby to koszmar dla każdej osoby, która musiałaby coś tutaj znaleźć.
A największy koszmar jest w takim przypadku w momencie kiedy chcemy wdrożyć kogoś nowego do projektu. Wytłumaczenie takiej osobie, że pobieranie danych z bazy to 12 klas i 7 interfejsów raczej powoduje, że w pewnym momencie stwierdzasz „dobra, zaraz Ci to rozrysuje”. Jeśli w takiej programistycznej lasagne dodatkowo poszczególne warstwy mogą być używane albo nie w zależności od kontekstu (np. tutaj mamy jeden procesor danych ale może ich być więcej lub nie być żadnego, albo dochodzi czasami kolejna warstwa, np. wysyłająca jakieś dane w inne miejsce) to samo omówienie co z czym się łączy wymaga poświęcenia w najlepszym przypadku kilku dni.
Za to próba rozwikłania zagadki zależności w takim kodzie w przypadku chęci wykonania refaktoringu to coś co złamie niejednego śmiałka.
Piekło implementacji
Masz taki kod. Do tego przyszedł jakiś mądry i powiedział, że trzeba używać kontenera dependency injection. I Twoje kolejne zadanie polega na wyciągnięciu wartości z bazy. Na ile dni wyceniasz takie wyzwanie? Ja bym poniżej jednego dnia nie schodził, prędzej podchodziło by to pod 2-3 dni. POBRANIE WARTOŚCI Z BAZY. Zakładając oczywiście, że jest nacisk na spójność.
Bo trzeba dodać interfejsy. Bo do interfejsów trzeba dodać implementacje. Trzeba to zarejestrować w kontenerze DI. Potem trzeba napisać mappery. I jeszcze trzeba dodać wszędzie przekazywanie przez konstruktory. Na końcu trzeba napisać do tego testy. Chociaż, czy na pewno?
W niechęci do testów
Produkując tyle kodu, który właściwie w większości przepycha dane dalej mamy od razu mnóstwo klas i metod do przetestowania. Przynajmniej kilka testów na każdy taki element. I to nawet przy najprostszym, najbardziej prymitywnym przypadku kiedy jedyne co robimy to np. pobieramy wartość tak/nie mówiącą czy użytkownik z podanym loginem istnieje w bazie. Nie mówiąc nawet o sytuacji kiedy musimy skorzystać ze wszystkich dobrodziejstw obecnych w systemie i zrobić kilka mapperów i procesorów danych. Kilka takich zadań i będziesz się budzić w nocy zlany potem krzycząc „niee! nie testujmy tego!”. I nie będziesz jedyny.
Testowanie czegoś takiego to nie dość, że praca bardzo czasochłonna to w dodatku bardzo mechaniczna. Każdy testy jest dokładnie taki sam. Piszesz je jak robot. Więc po co je właściwie pisać?
I w ten oto sposób zespołowo podejmuje się decyzję „testy są niepotrzebne”.
Przyczyna
Kult warstw to klasyczny przykład „kultu cargo” (https://pl.wikipedia.org/wiki/Kulty_cargo). Widzimy, że na konferencjach, w książkach czy na szkoleniach mówią ciągle o tym, że kod trzeba dzielić na warstwy czy moduły więc dzielimy kod na warstwy i moduły. Tylko jakoś dziwnym trafem umyka nam w tych stwierdzeniach, że chodzi o „przemyślane dzielenie kodu na warstwy i moduły”. Dodajmy do tego powtarzaną jak mantrę „spójność kodu” (o której sam też mówię czasami), która ostatecznie prowadzi do sytuacji kiedy niezależnie od złożoności problemu cały proces musi być pełny i zawierać wszystkie formalności. I mamy przepis na programistyczną lasagne. Tylko dużo mniej smaczną niż ta z makaronu.
Jeżeli narzekałeś kiedykolwiek na to, że załatwienie czegoś w korporacji wymaga wyklikania zamówienia w dwóch systemach, rozmowy z przełożonym, który pójdzie potem do swojego przełożonego i tamten zgłosi to do działu zajmowania się sprawami, który to doda sprawę do kolejki spraw, wysyłaną listownie do centrali, która odpowie oficjalnym pismem do prezesa, którego sekretarka przekaże odpowiedź do przełożonego Twojego przełożonego, który musi to odnotować w systemie, a na koniec każe Ci wypełnić raport w drugim systemie i iść po podpis do administracji mimo, że Ty tylko pytałeś czy możecie sobie kalendarz w pokoju powiesić to właśnie to samo zrobiłeś w swoim kodzie dbając o „spójność” i „podział odpowiedzialności”.
Druga przyczyna to jest przecenianie naszego projektu i podnoszenie go do rangi wielkiego systemu. Mimo, że tak naprawdę po prostu zapisujemy dane z formularza i dajemy możliwość ich usunięcia jak się ktoś pomyli. Ale w biznesowych prezentacjach widzieliśmy słowa „kluczowy” albo „potencjalnie do zastosowania w innych działach”! Tylko, że jako programiści jakoś od razu wtedy zakładamy, że „kluczowy” znaczy ” potencjalnie duży” albo „mocno rozbudowywany w przyszłości”. A często jeden formularz czy tabela wyciągana z bazy jest właśnie tym kluczowym elementem, którego używa biznes. W naszym przypadku opakowanym w 10 warstw serwisów „w razie czego”. Nie zadajemy sobie trudu dowiedzenia się czy są plany na poważną rozbudowę i warto się do tego przygotować. Przygotowujemy się do niej niezależnie od wszystkiego.
Rozwiązanie
W tym wypadku rozwiązanie trudno mi podać. Bo jest to coś co trzeba przepracować jako cały zespół.
Na pewno w tym wypadku pierwsze co trzeba zrobić to schować dumę do kieszeni i zastanowić się czy Wasz system na pewno jest taki duży. A jeżeli w tym momencie nie jest to przez podjęciem drastycznych kroków polegających na obudowaniu całości w gigantyczną wieżę serwisów, interfejsów, mapperów i bulbulatorów dopytajcie kogoś z biznesu czy mają plany na duże funkcjonalności w nadchodzącej przyszłości.
Jeżeli jednak megalomania nie jest tutaj problemem ale po prostu staracie się trzymać tych wszystkich porad o podziale kodu to mam poradę. Ale uprzedzam, że dotyczy ona raczej startujących projektów i takich, które nie mają wielkiego ciśnienia co do terminów. Mianowicie przyjmijcie strategię, że np. przez pierwszy sprint lub dwa piszecie kod najprościej jak się da. Prawie że z logiką i kontekstem bazy danych w kontrolerach. Kiedy sprint się skończy to spróbujcie napisać testy do tego kodu. W trakcie tej próby zróbcie refaktoring, który ją umożliwi. Teraz zajrzyjcie wspólnie w kod i zobaczcie które fragmenty Wam się powtarzają, są używane w tym samym kontekście albo mają tylko fragmenty, które się zmieniają w różnych miejscach. Tylko z nich wyciągnijcie interfejsy albo je same wyciągnijcie do osobnych klas.
Potem przy okazji każdego zadania powtarzajcie proces analizy tego ile kodu zaczęło wymagać możliwości podmiany albo stało się potrzebne w wielu miejscach i wtedy w tym miejscu róbcie refaktoring. Wtedy Wasze warstwy wyłonią się same. W dodatku będą tylko tam gdzie jest taka potrzeba bo jeśli jakiś proces jest tak prosty, że nie załapał się na refaktoring bo nie korzystał z niczego co jest gdzieś indziej to uniknęliście sytuacji kiedy komplikuje się nawet najprostsze czynności tylko po to żeby były „spójne” z całą strukturą.
Leave a Comment