Miroslav Holec

Software & Cloud Architect

miroslavholec.cz / blog / webova-architektura-orientovana-na-sluzby

Webová architektura orientovaná na služby

Miroslav Holec

Miroslav Holec

Publikován 19. prosince 2014 , aktualizace: 29. března 2016

Tento článek je starší 18 měsíců a je proto možné, že popisuje postupy nebo technologie, které v uplynulé době mohly doznat výraznějších změn. Názory a myšlenky v tomto článku již nemusí vyjadřovat současné stanovisko autora nebo autorů. Článek byl napsán 19. prosince 2014.

Dnes popíšu celkem jednoduchou architekturu pro vývoj webových aplikací, která je založena na kombinaci několika návrhových vzorů a myšlenek. Zároveň tato architektura abstrahuje od použití některých jiných uznávaných vzorů, které zbytečně zvyšují komplexitu kódu a vytváří diskutabilní abstraktní vrstvu, kterou často vývojářský tým ani nepotřebuje.

Můj návrh je sestavení minimalistického řešení, které je schopné fungovat (jak nečekané v XP). Je to řešení dobře testovatelné a umožňuje velmi specifické optimalizace na libovolných místech aplikace. Jedná se o řešení, které v základu nevyžaduje žádné úsilí, ve své podstatě je velmi podobné kódu vývojářů - začátečníků ale s tím rozdílem, že se zaměřuje na úzká místa, která zefektivňuje dodržováním principu SoC. Architektura využívá především Request-Response pattern a vrstvu Services vystavených jako Facade.

Architektura řešení

Princip komunikace

  1. Uživatel například vyplní formulář a odešle jej na prezentační vrstvě. Ta je reprzentována HTML stránkou sestavenou pomocí Razor View.
  2. Požadavek dorazí jako HTTP Request *do určité metody controlleru a provede se *data binding na příslušný InputModel. Ten je naplněn.
  3. Controller má svůj první úkol, provést serverovou validaci InputModelu a v případě neúspěchu provést rutiny.
  4. Pokud vše dopadne dobře, data z InputModelu jsou poslána vybrané službě na business vrstvě. Dostáváme se tedy na vrstvu níže, kde už mimochodem existují i db objekty.
  5. Služba přijme požadavek ve formě InputModelu a nyní má za úkol provést patřičné úkoly. Buď komunikuje s nějakým API, nebo s databází. Nepracuje s repozitáři ale přímo s DbContextem.
  6. Pokud má služba vrátit data, obvykle je posbírá odkud je potřeba a naplní jimy nějaký ViewModel (nebo ho vytvoří), nebo udělá kolekci ViewModelů, atd.
  7. Služba vrátí odpověď controlleru. Buď je to jen objekt typu Response nebo je to generický Response, kde T je nějaký ViewModel nebo celá kolekce dat.
  8. Controller si odpověď vezme a podívá se na výsledek. Podle něj se rozhodne kam uživatele pošle, jaké mu zobrazí View a případně do vybraného View předá data z odpovědi.
  9. Uživateli je vykresleno View

Patterny

Nejprve si shrneme výhody z hlediska návrhových vzorů, které jsou použity a co nám poskytují:

I/O ViewModel

Stále více se mi osvědčuje separovat vstupní a výstupní ViewModel. Dává to smysl, když se zaměříme na rozdíly:

InputModel

  • je vstupem uživatele
  • obvykle jeden plain object
  • obsahuje validace (data annotations)
  • může jich být více na jedné razor page
  • kvůli validaci často realizuje IValidatable
  • unit testy řeší co uživatel zadal a zda je to přijatelné (validní)

ViewModel

  • je výstupem pro uživatele (resp. pro view)
  • často se jedná o složitější struktury
  • neobsahuje validace
  • pouze jediný ViewModel pro razor page
  • unit testy řeší variace vstupních dat a zda je ViewModel správně interpretuje

Z popisu je patrné, že u komplikovaného ViewModelu by v případě společného použití i jako InputModelu mohly způsobovat problémy například validace. Také hodnoty NULL by mohly být interpretovány různě. Často se určitě setkáte se situací, že si pošlete přes ViewModel data na stránku ale zpátky je už uživatel nevrací (nikde je nevyplňuje). Pak se to obchází obskurně přes ViewBag apod.

Separace je čisté řešení problému, které navíc hezky odděluje i logiku při psaní unit testů. U InputModelu se testují vstupy na základě zadání uživatele, u ViewModelu zase výstupy na základě dat z datového zdroje.

Request Response pattern

Další pattern, který pomáhá udržet v aplikaci pořádek je Request-Response (občas také Transfer nebo Request/Reply). Myšlenkou je vytvoření komunikačního protokolu mezi controllerem a servisními třídami, které vytváří fasádu. Díky dobře nastavené komunikaci přestane mít vývojář potřebu psát jakoukoliv logiku do controlleru ale přesune vše do služeb. Ty controlleru pomocí zpráv sdělí jak požadavek dopadl a controller se podle toho může rozhodnout, jak se zachová.

Controller responsibility

Dostáváme se k zodpovědnosti controlleru. Má totiž v podstatě jen 3 velké úkoly:

  1. Zvalidovat InputModel
  2. Předávat data servisním službám (často InputModely) a "vyzvedávat" si je (obvykle Response<T>)
  3. Na základě odpovědí servisních služeb a svého úsudku zobrazovat uživateli stránky nyplněnými ViewModely

Ukázka toku v controlleru

Controller nezná žádné databázové třídy a neví nic o vzdálených službách apod. Komunikuje jen se servisními službami, které jsou o level níže. To, jak získají služby data už je jejich problém.

Services

O službách už jsem se několikrát zmínil a nastínil jsem jejich význam. Jsou základním kamenem celé aplikace, protože obsahují business logiku. Nekomunikují s žádnými repozitáři (ale mohly by). V případě rozsáhlých aplikací (a těch opravdu moc není, byť si každý myslí, že ta jeho aplikace je) bych v tom viděl přínos, nicméně ve většině případech jsou repozitáře zbytečné.

Repository antipattern

Na generických repozitářích mi vadí to, že vytváří abstraktní vrstvu, která vystavuje tytéž metody, které umí i Entity Framework. Obvykle pak inject repository pro zavolání metody AddNew() není o nic zajímavější než zavolání db.Whatever.Add(). Z celkem velkých projektů mám už odkoukáno, že 95% metod repozitářů je voláno 1x a zbývajících 5% je těch, které způsobují performance issues, protože jsou volány vícekrát z různých míst a každé volání by vyžadovalo například jinou projekci nebo spojení. Vývojáři pak podle potřeby joinují další a další tabulky a způsobují na různých místech výkonnostní problémy. Řešení v podobě rozpadu na samostatné metody by vedlo k tomu, že co metoda repozitáře, to jedno použití. V praxi se tak tímto způsobem vyhýbám:

  1. dokonalé zbytečnosti kdy z controlleru se volá someService.DoSomething() a uvnitř je jen someRepository.DoTheSame().
  2. dokonalému zmatky, kdy z controlleru se jednou volá service, jednou zase repository a nikdo neví kde co vlastně je

Ukázka implementace metody v Services

Na obrázku výše je vidět metoda, která dost nápadně připomíná repozitářovou GetBySubdomain(string subdomain). Pracuje se s contextem a protože je jasné, že data jsou pouze pro čtení, je použit AsNoTracking() pro zvýšení performance. V případě repozitáře by bylo něco takového nemyslitelné, protože by nikdo nebyl schopen získaná data později aktualizovat.

Můžete teď namítnout, že používat using s DbContextem není šťastné řešení. Ve skutečnosti se takový blok v rámci jednoho HTTP Requestu zase tak často nevolá. Nevidím příliš velký rozdíl mezi sestavováním repozitářů, injectováním závislostí nebo několika DbContexty. Samozřejmě ani v rámci této architektury nikomu nic nebrání v tom, vyrobit si UnitOfWork a DbContext si v něm uchovávat a celý UoW injectovat do Services. Mně to přijde zbytečné a vzhledem ke sdílení DbContextu hrozí jeho rozbití na jednom místě a nepoužitelnost po zbytek HTTP Requestu všude jinde.

Samosebou repozitáře jsou na delší debatu a v případě projektů s potenciálem migrace na jiné DB servery jejich smysluplnost výrazně roste.

Závěr

Ukázal jsem celkem jednoduché řešení, jak psát rychle a celkem i efektivně webové aplikace. Svépomocí jsem tak napsal kompletní projekt Spicephoto a nyní stejným způsobem pracuji na jednom dalším projektu, který pokrývám z velké části i unit testy. Unit testy píšu aktuálně dost odlišné a ukazuje se, že v mých aplikacích mi dost výrazně pomáhají předejít problémům. Výhodu UT vidím hlavně v otočení myšlení. Tím, že uvažuji nad testem vlastně uvažuji nad různými teoretickými scénáři, které mohou vyvolat chybu. Při standardním vývoji obvykle vývojář pracuje jen s pozitivním průběhem a nestandardní scénáře příliš neřeší. Nakonec návrh v tomto článku je snadno rozšiřitelný o všechny dnes běžně používané patterny, ať už je váš pohled na ně jakýkoliv.

Potřebujete pomoci?

Líbil se Vám článek? Máte dotaz nebo chcete v této oblasti s něčím pomoci? Neváhejte se na mě obrátit.

mirek@miroslavholec.cz

  • Řešení vývojářských problémů
  • Konzultace
  • Firemní školení a workshopy