Kardynalne błędy
Nie, to nie jest recenzja trzeciego dodatku do Munchkina. To wyjaśnienie, czemu ten blog jest smutny, jak i całe programistyczne życie. Ponadto, ten post w pewnym stopniu wyjaśnia, czemu PizzaMVC od strony technologicznej i architektonicznej, wygląda jak wygląda - bo po prostu jest zupełnym przeciwieństwiem patologicznego podejścia, które opisałem poniżej.
Smutna rzeczywistość
Podczas swojego życia widziałem wiele aplikacji napisanych w oparciu o ASP.NET MVC. Niezależnie od tego, czy spotkałem się z ich kodem w pracy, czy na czyimś blogu albo githubie; czy była to mała, czy duża firma, wszystkie wyglądały bardzo podobnie:
- Logika biznesowa, aplikacyjna i infrastrukturalna były upchane głównie w kontrolerach.
- Skutkiem czego kontrolery zawierały tysiące linii kodu.
- Dla każdego kontrolera były tworzone widoki dla poszczególnych akcji.
- Widoki akcji danego rodzaju były niemalże identyczne dla poszczególnych kontrolerów:
- “index” zazwyczaj zawierał konfigurację jakiegoś grida, czyli ręczną definicję tego, które właściwości modelu widoku mają być kolumnami, jaką maja mieć szerokość, sposób sortowania, itp;
- “create” lub “edit”, to zawsze była lista pól edycji dla właściwości danego obiektu, z ręczną konfiguracją typu pola oraz oczywiście podpięciem walidacji.
- Viewmodelami były te same obiekty, które były mapowane przez ORM na tabele bazy danych (zazwyczaj z niewiadomych powodów nazywane “encjami”).
- W związku z tym, te biedne klasy często ociekały atrybutami - bo jedna właściwość miała zarówno atrybuty definiujące mapowanie przez ORM, jak i formatowanie czy walidację po stronie widoku.
- Dostęp do danych realizowany był przez repozytoria. Każda “encja” miała swoje “repozytorium”.
- Model składał się wyłącznie z “encji” i “repozytoriów”. (Zresztą, nic dziwnego, wszakże to, co powinno w nim być, czyli logika biznesowa, zostało już zaimplementowane w kontrolerach. Widać tu silne trzymanie się zasady “DRY”.)
- “Repozytoria” wstrzykiwane były bezpośrednio do kontrolerów.
- W ekstremalnych przypadkach nie było “repozytoriów”, lecz kontroler operował na gołym
DbContext
, a każda postowa akcja kontrolera zawierałatry...catch...
zdb.SubmitChanges()
wewnątrz.
Co w tym złego?
Właściwie wszystko. W takim podejściu nic nie jest zgodne z wzorcami projektowymi, dobrymi praktykami ani pragmatyczne:
- Kontrolery z definicji mają być cienkie i nie zawierać logiki. Ich odpowiedzialnością jest pobranie żądania użytkownika, zweryfikowanie go i przekazanie do Modelu. To Model zajmuje się logiką biznesową, a gdy ta tego wymaga coś pobiera albo zapisuje do bazy (albo innego źródła danych).
- Użycie tej samej klasy jako viewmodelu i obiektu mapowanego przez ORMa to kolejne naruszenie SRP. Ale nie tylko - to też początek dziwnych problemów z ręcznym dołączaniem i odłączaniem obiektów z kontekstu ORMa, selectów n+1, oraz pobierania ogromnych ilości danych bez potrzeby. Zazwyczaj jest tak, że w bazie trzyma się więcej danych niż jest wyświetlana na interfejsie użytkownika czy możliwa do jego edycji. Po co pobierać kilkadzisiąt kolumn z tabeli, skoro w gridzie chcemy wyświetlić tylko kilka? A weźmy do tego fakt, że nieraz na widoku chcemy mieć projekcję np. trzech tabel (Klient, Faktura, Towar). Jeden taki widok potrafi pobrać pół bazy danych.
- Repozytorium to wzorzec projektowy będący elementem DDD, który jest źródłem obiektów biznesowych dla serwisów dostarczającą specifycznych metod kontekstowych. Repozytorium to nie jest nakładka na ORM. Oczywiście, jeśli używa się upośledzonego ORMa w rodzaju EF, to taką nakładkę trzeba napisać - w przeciwnym razie nie będzie możliwe pisanie testów jednostkowych, ale to nie powód, żeby tak ją nazywać.
Ale mniejsza z tym - MVC, repozytoria i nawet EF zasługują na oddzielne wpisy. Abstrahując już od wyżej wymienionych błędów architektonicznych i nie trzymania się dobrych wzorców - to podejście jest okropne, bo narzuca nieefektywne programowanie metodą copy & paste. Widoki, kontrolery, “repozytoria” - wszystko jest robione na jedno kopyto, dodawanie nowego przypadku użycia do aplikacji wymaga skopiowania kilku katalogów, plików i pozmieniania paru nazw. Jest to nudne, odtwórcze, czasochłonne i błędogenne, do tego powstaje masa kodu, która nie ma znaczenia biznesowego, ale istnieje i trzeba ją utrzymywać, zmieniać i debugować…
Naprawdę, zadziwia mnie fakt, że komuś się chce tracić czas na tak bezsensowne działanie. Ale ludzie tak robią - i co gorsza robią to powszechnie…
I po co to wszystko?
80% aplikacji to CRUD. Nie oszukujmy się - to jest banalne, powtarzalne i w ogóle niewarte pisania kodu. Język ma swoje mechanizmy: refleksję, metaprogramowanie, generyki po to, abyśmy mogli automatyzować powtarzalne zadania. Począwszy od widoków, po kod DAO - wszystko może być tworzone dynamicznie. Można automatycznie mapować modele rekordów na view modele na podstawie zgodnych nazw właściwości i ich typów, a dzięki dobremu ORMowi i odpowiedniej konfiguracji - wybierać z bazy tylko te kolumny, które występują w widoku zamiast całych rekordów. Dzięki kontenerowi IoC z obsługą interceptorów można mieć jedno miejsce, w którym commitowany jest unit of work w całej aplikacji. Generyczność (i rozszerzalny ORM) zapewnia też łatwość implementacji takich funkcji jak soft delete, audyt czy optimistic concurrency.
Dzięki współdzieleniu widoków w katalogu Views/Shared
można mieć po jedynym widoku: Create
, Edit
, Details
i Index
w całej aplikacji. Index
może przyjmować metamodel grida, który powstaje na podstawie viewmodelu, i może być jeden dla wszystkich typów danych, jakie w gridach chcemy wyświetlać. Widoki edycyjne dla pojedynczych obiektów mogą zostać automatycznie wygenerowane na podstawie listy właściwości i ich typów.
Do realizacji CRUDa w aplikacji wystarczy jeden kontroler, jeden serwis aplikacyjny i trochę automatyzacji.
Po co kopiować kod, zamiast skupić się na programowaniu?