Home > C#, MVC > Skup się na cechach aplikacji, a nie na jej warstwach

Skup się na cechach aplikacji, a nie na jej warstwach

dobromirJakiś czas temu przedstawiałem na Trójmiejskiej Zawodowej Grupie .NET temat związany z tworzeniem architektury aplikacji. Pokazałem najczęstsze problemy występujące w obecnych aplikacjach, jak i sugestię rozwiązań. Temat powtórzyłem na prezentacji z okazji Drzwi Otwartych w PGS Software, a niebawem przedstawię go (w zmienionej formie) również podczas Software Talks we Wrocławiu. Właśnie jakie są to problemy, jakie ewentualnie usprawnienia możemy tam dodać?

Zastanówmy się jak wyglądają takie projekty tworzone praktycznie od zera (głównie w .NET). Na początku, skupmy się na średniej wielkości projekcie, gdzie przeznaczamy serwer dla aplikacji web i mamy bazę danych. Nie używamy zbytnio rozwiązania rozproszonego na chmurze, na chwilę obecną, żadnych farm, czy wielkiej skalowalności, zostawiamy to na później – chciało się by rzec, piszemy standardowy projekt dla klienta – sklep z marchewką, no może z bardziej wyrafinowanym produktem – bananem. Czyli prosty projekt, jednak z biegiem czasu jest on coraz bardziej rozwijany.

Jakie są problemy

Obecnie aplikacje (nie mówię o wszystkich) tworzone są w pewien sposób:

  • Klasa na plik
  • Projekty na warstwę (UI, BL, DAL, Shared, Lib, Hell, Wtf…)
  • Warstwy w projekcie – czyli foldery, podfoldery, gdzie wrzucamy coś związanego z infrastrukturą danej warstwy (interface: IRepository, IUnitOfWork, IService), implementację (class: Repository, UnitOfWork), modele i view modele, encje (Models, ViewModels) etc., nie mówię już o standardowym Views/Controllers
  • Abstrakcje i interfejsy… jak wspomniane wcześniej IRepository, IService, no bo przecież DI, unit testy i zapobieganie DRY

Jak wygląda rozwój takiej aplikacji? Spoko – mamy pełno folderów, podfolderów, plików tyle ile klas – projekt jest bardzo estetycznie zrobiony, jest czytelny. Włączany jest tryb zero DRY i pojawiają się klasy bazowe skupiające logikę, lub wyciągające wspólny model danych. Ma wszystko swoje ręce i nogi. BL nie może zawierać referencji do UI (w przypadku webowym mówimy tutaj o MVC). Pojawia się jednak w pewnym momencie problem. Nawarstwienie tych projektów, folderów, plików powoduje, że coraz ciężej poruszamy się po projekcie. Często da się też zauważyć, że na siłę jest coś rozwarstwione tylko po to, aby projekty się nie połączyły, czy też aby oddzielić interfejs od implementacji oraz aby coś miało końcówkę ViewModel tylko po to, aby wskazać, że to ViewModel. Wykorzystany jest jeszcze Automapper z logiką w środku, który mapuje model z jednej warstwy na model z innej warstwy. Dodanie czy usunięcie jednego pola w widoku okazuje się problemem, bo informacje na temat tego pola są wykorzystywane jeszcze w paru innych miejscach. Projekt jest tworzony z myślą o modułowości – czyli tworzymy framework pluginów – a klient chciał tylko prosty monolityczny sklep z marchewką.

Takie rozwarstwienie i abstrakcyjność nie wnoszą nic konkretnego, w aplikacjach MVC może to wyglądać tak: Kontroler, Service (interfejs), szukamy implementacji (nawet gdy mamy resharpera), idziemy do metody, wykorzystujemy repozytorium (kolejny interfejs), i jego implementację w paru miejscach jest jeszcze Automapper. W rezultacie, nasz kontroler zawierający K-akcji, jest pustą klasą, która tylko wywołuje serwis zawierający P-metod, potrzebujący R-zależności (repozytoriów, serwisów). Nasza poszukiwana metoda używa tylko jednego repozytorium i co więcej S-metod tam dostępnych – testowanie takiego tworu sprowadza się do – wstrzykiwania wszystkich zależności, nawet tych których nie potrzebujemy. W późniejszej fazie rozwoju aplikacji bez refactora się nie obędzie, a repozytorium które kiedyś zwracało jakąś encję, teraz zawiera zwraca jeszcze powiązania, i powiązania powiązań. Nie wspominam już już o kilku metodach, które się tam pojawiły.

Co dokładniej projektujemy

Należy zadać sobie pytanie – co projektujemy? Warstwowość aplikacji i jej struktuę, czy też funkcjonalności? Jeżeli się zastanowimy nad większością aplikacji, to mamy do czynienia z pobraniem danych, edycją, tworzeniem, usuwaniem, dociąganiem zależności (czyli inne pobieranie) – patrząc na wszystko – nic innego tylko CRUD. Czasami znajdzie się też jakaś skomplikowana logika biznesowa, która to ma coś wyliczyć (np. stawki VAT, czy promocje), jednak jest to tak samo – pobranie danych, przemielenie i zapisanie. Patrząc jednak na to z pewnej perspektywy – przechodzimy przez wszystkie warstwy z zapytaniem lub poleceniem do bazy i wracamy z wynikiem.

Jak wtedy projektować

Nie chciałbym tutaj nikomu mówić jak ma projektować – ważne, aby robić to z głową – jest wiele mądrych osób, które opisują to w swoich książkach, a nie na jakichś blogach. Najważniejsze, to bez nadmiernej liczby interfejsów, bez nadmiernej liczby metod publicznych na klasę i skupić się na funkcjonalnościach. Przykładowo, gdy wykorzystujemy Entity Frameworka do prostego projektu, to:

  • Czy musimy używać wzorca repozytorium (takiego generycznego, o którym kiedyś pisałem) ? Może, coś z poziomu Query/Command, albo i bezpośrednio odpytywać się EF?
  • Czy musimy używać wzorca Service tylko po to, aby był wywoływany przez kontroler MVC czy WebApi
  • Czy jest potrzeba wykorzystywania wszystkich metod HTTP używając przeglądarek? Może skupić się jedynie na GET/POST?
  • Czy klasa modelu musi być na siłę, reużywalna pomiędzy Actionami spośród różnych kontrolerów? Jak wygląda domena?
  • Czy musimy pisać specjalne ViewModele i Modele dla warstwy widokowej, aby ‘zmapować’ dane z Modeli i ViewModeli z warstwy biznesowej?
  • Co daje nam klasa na plik? Czy nie możemy grupować modeli i klas zależnych od polecenia w jednym pliku w czytelnym formacie (dla ‘code stylenazi’ – ba interfejsu i implementacji razem) ?

Sugestia

Od jakiegoś czasu stosuję w projekcie wzorzec Command, Query, Event (i nie wykorzystuję gotowego frameworka, ani takiego ‘prawdziwego’ CQRSa). Od długiego czasu w projektach gdzie używane są serwisy (web services), wykorzystuję wzorzec RRSL (Request-Response Service Layer – na przykładzie Agatha), który w skrócie wygląda jak wzorzec Command, nie używam WebApi wykorzystującego standardowy routing typu /api/Controller/Action/Id – i całkiem dobrze się to sprawdza. Pozbyłem się zbędnych interfejsów, testuję to co potrzebuję, rozbijam na małe, niezależne funkcjonalności (i nadal testowalne), jestem w stanie w prosty sposób dodać autoryzację do danej funkcjonalności, a nawet zapamiętać dane i użyć ich później bez uruchamiania zbędnej logiki (czyli cache). Sprowadzając się do tego, że sama architektura spełnia zasady SOLIDa (stosując je do architektury aplikacji):

  • Single Responsibility – funkcjonalność typu Command/Query – pobierz stronę produktów, pobierz detale produktu, zmień liczbę produktów w koszyku – każde polecenie czy zapytanie dotyczy konkretnej rzeczy – i jest właśnie w ten sposób implementowane – mam stronę /Products/Details/5, tworzę polecenie o nazwie GetProductDetails, które przyjmuje id produktu.
  • Open/Closed – dodanie/modyfikowanie funkcjonalności powinno być łatwe, i nie wpływać na resztę aplikacji. Jeżeli mam dodać stronę /Products/ShortInfo/5, to tworzę kolejne polecenie GetProductShortInfo, nie dotykając poprzednich metod. Ważne jednak może być wydzielenie jakiegoś wspólnego kodu, jeżeli jest potrzeba, ale nie należy się bać DRY. Mogę dzięki temu spokojnie modyfikować później ProductDetails nie martwiąc się zbytnio poleceniem ProductShortInfo
  • Liskov Substitution – w bardzo ograniczonym stopniu wykorzystuję dziedziczenie, nie tworzę rozbudowanej hierarchii klas, spłaszczam hierarchię jak się da. Dzięki temu wiem czego się spodziewać
  • Interface Segregation – wiem co wywołuję  – mam szynę poleceń, szynę zapytań, kontekst do bazy danych, etc.
  • Dependency Injection – wstrzykuję to co potrzebuję do danej funkcjonalności, a nie to co np. potrzebuje kontroler.

Szczegóły i funkcjonalności prawdopodobnie przedstawię w osobnych postach, starając się bardziej wyjaśnić o co w tym wszystkim chodzi. Mogę już jednak wskazać, że takie podzielenie funkcjonalności jest skalowalne, nic nie stoi na przeszkodzie je rozdzielić, da się prosto dodać autoryzację, ‘cache’ czy walidację.

Tags: , , ,
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: