Pokušati ću opisati još jedan u nizu patterna za arhitekturu aplikacije, bilo web, desktop ili Silverlight, koji upotrebljavam već neko vrijeme. Pattern se pokazao jako korisnim, i to sa stajališta organizacije veće količine koda, enkapsulacije logike u zasebne, odvojive cjeline, time omogućivši i lakši unit testing. Svrha ovog patterna je i pojednostavljenje koda i uklanjanje bespotrebne apstrakcije upotrebom puno slojeva (hint: dali baš u svakoj aplikaciji treba biti layer sa Repository klasama, Service klasama sa (praznom) poslovnom logikom, i sl?).
U osnovi, radi se o starom i poznatom CQS patternu (Command Query Segragation), koji nalaže da se odvoji logika za upite (npr: dohvati sve proizvode iz kategorije), koja vraća podatke, od logike koja sadrži određeno procesiranje, i ne vraća podatke (npr: dodaj proizvod u košaricu za kupnju). Dodatnu popularnost ovaj pattern je dobio kroz upotrebu u CQRS arhitekturi, koji query/command priču diže na jedan viši, enterprise nivo, da bi se dotakli problemi (horizontalnog) skaliranja i race conditiona (više procesa se natječu za prvenstvo pristupa određenom resursu), dodajući odvojene izvore podataka za pisanje i čitanje, te asinkroni sistem zapisivanja i sinkronizacije. Dodatne informacije o CQRSu se mogu pronaći na blogu od Udi Dahana.
Primjer upotrebe ovog patterna možemo prikazati na jednostavnom primjeru web shopa, kroz upotrebu u dva scenarija:
| 1 | Dohvat prozvoda zadane kategorije |
| 2 | Dodavanje odabranog proizvoda u košaricu za kupnju |
Naravno, ovdje se radi o web aplikaciji, koja može i ne mora biti MVC.
Prvi scenarij možemo opisati generičkim interfaceom, koji potom upotrebljavamo za sve upite:
Interface IQuery<TParam, TResult>
{
TResult Execute(TParam args)
}
ovime smo dobili enkapsulaciju logike upita u zasebnu klasu, koja nasljeđuje IQuery interface.
Drugi scenarij ne zahtjeva nikakvu povratnu vrijednost, osim eventualno bacanja Exceptiona, i opisuje se interfaceom:
Interface IHandle<T> where T:class
{
void Handle(T command);
}
vrlo slična stvar kao i IQuery, samo bez povratne vrijednosti.
Cijelu upotrebu ovih interfaceova uvelike olakšava neki IoC/DI framework, jer on omogućava automatsko traženje konkretne implementacije određenog interfacea. Prije sam uvijek morao raditi vlastiti kod za pretraživanje klasa u assemblju, što je znalo imati skrivene bugove, a onda sam nabasao na korisnu funkciju unutar StructureMap frameworka, koji inače koristim za IoC/DI, i inicijalizacija izgleda ovako:
scan.ConnectImplementationsToTypesClosing(typeof(IHandle<>));
scan.ConnectImplementationsToTypesClosing(typeof(IQuery<,>));
upotreba unutar akcije/kontrolea u MVC aplikaciji izgleda ovako:
public HomeController(IQuery<CategoryInfo,ProductViewModel> productFromCategory,
ICommandBus commandBus)
{
_productFromCategory = productFromCategory;
_commandBus = commandBus;
}
public ActionResult CategoryProducts(int categoryId)
{
var products = _productFromCategory.Execute(new CategoryInfo(categoryId));
return View(products);
}
public ActionResult Add(int ProductID)
{
try {
_commandBus.Send(new AddProductToBasketCommand(ProductID));
}
catch(Exception e)
{
// handle exception ...
}
return RedirectToAction("Index");
}
Ovdje se može vidjeti upotreba CommandBus klase, odnosno ICommandBus interface-a, koji se samo “factory” za pozivanje IHandle<T> objekta, a nekako bolje opisuje namjeru i svrhu. Nije na odmet spomenuti da dozvoljava jednostavniji rast aplikacije, dodavanje novih funkcionalnosti se svodi na kreiranje novih IHandle klasa, upotreba iste logike je jednostavnija. Kod timskog rada, programer BLL sloja može u dogovoru sa Front End (FE) programerom definirati parametre za izvršavanje određenog procesa, poslovne logike, upita, i omogućiti FE programeru da samo poziva objekte prema interfaceu, ne brinući se gdje je to i kako implementirano!
Sample aplikaciju, sa primjerom ovih interface-ova, konfiguracije StructureMap-a i MVCa, možete skinuti ovdje:
https://bitbucket.org/hhrvoje/cqssample