UPDATE: Knihovnu pro testování závislostí si nyní můžete nainstalovat jako NuGet balíček. Více v následujícím postu.
U každé větší softwarové aplikace hrozí, že kód začne časem degradovat. Jedním z typů degradace jsou závislosti, které porušují původní architektonický návrh. Modelový příklad: v ASP.NET MVC aplikaci platí, že model nesmí referencovat žádný controller. Jak ale takové pravidlo vynutit, když jsou modely i controllery v jednom Visual Studio Projectu?
Poznámka na úvod: Pro jednoduchost nebudeme uvažovat o tom, zda je dobrý nápad mít modely, controllery i view v jednom projektu. Také nebudeme rozebírat, proč nesmí model referencovat controller, budeme to považovat za pravidlo, které vyplývá z architektonického návrhu aplikace.
Jednou z možností, jak lze v .NET vynutit směr závislostí, je rozdělení aplikace na více knihoven, kterým odpovídají Visual Studio Projecty. Mohli bychom tak mít např. projekty WebSite, který by obsahoval jen ASP.NET MVC Views, a dále projekt Controllers a projekt Models. Závislosti mezi projekty jsou jednosměrné, což v konečném důsledku vynutí pravidlo o povoleném směru závislostí. To ale není vždy úplně šťastné, protože nám knihovny začnou bujet jak cukety po dešti, což se negativně projeví na rychlosti kompilace. Lepší je rozdělovat aplikaci do projektů jen tehdy, když si to vynutí fyzický deployment.
Další možností je koupit NDepend, naučit se v něm vyznat a integrovat ho do buildu. Pro velké aplikace to určitě není špatná volba, zejména pro spoustu dalších fíčur které NDepend nabízí. Existuje ale i jednodušší varianta – analyzovat závislosti v unit testu.
Vrátíme se nyní k modelové situaci: máme ASP.NET MVC projekt, který má typickou strukturu:
Naše architektonické pravidlo říká, že model nesmí referencovat controller. Potřebovali bychom proto test, který by vypadal nějak takhle:
[Test] public void ModelsDoNotReferenceControllers() { AssertDependencies .InAssemblyContaining<User>() .All(type => type.Namespace.EndsWith(".Models")) .MustNotReference(reference => reference.IsDerivedFrom<Controller>()); }
Jakým způsobem se identifikují modely a kontrolery je celkem jedno, zde jsou ukázány dva možné přístupy – přes namespace nebo přes base třídu.
Analýza závislostí v .NET není zas tak easy. Pomocí reflexe se jednoduše podíváme na závislosti mezi knihovnami, ale my potřebujeme jít hloub, na úroveň tříd. To už bychom se ale museli vrtat přímo v instrukcích MSIL, což je dost komplikované. Naštěstí existuje šikovná knihovna Mono.Cecil, která dokáže DLL knihovnu načíst a pohybovat se v jejích vnitřnostech pomocí rozumného API. Stáhnout si ji můžete také jako Nuget balíček. Mimochodem, pomocí Mono.Cecil můžete s knihovnou dokonce i manipulovat a změněný kód uložit (kde jsem to už viděl?). Nám ale stačí jen přečíst závislosti.
S Mono.Cecil můžeme jednoduše iterovat tělem metody a zkoumat jednotlivé instrukce. Přiznám se že se v instrukcích MSIL moc neorientuju, ale metodou test-omyl jsem nakonec došel k tomuto kódu, který, zdá se, funguje (alespoň na mém počítači):
foreach (var instruction in instructions) { var mr = instruction.Operand as MemberReference; if (mr != null && mr.DeclaringType != null) yield return mr.DeclaringType; var tr = instruction.Operand as TypeReference; if (tr != null) yield return tr; }
Dostat se na typy proměnných, parametrů a návratových hodnot metod, fields a properties je velmi jednoduché, najdete je v API pomocí intuice.
Na rozdíl od reflexe Mono.Cecil nepotřebuje nahrát analyzovanou .DLL knihovnu do paměti jako spustitelný kód. Pracuje s knihovnami jako s daty, a tak nepoužívá s klasické typy jak je znáte z .NETu (System.Type), ale vlastní TypeReference a TypeDefinition. Z toho bohužel plyne pár drobných věcí, které vás mohou nemile zaskočit – například pro názvy vnořených tříd používá jako oddělovač lomítko místo plusu.
Pokud vás zajímá kompletní implementace testu uvedeného výše, včetně jednoduchého fluent rozhraní, stáhněte si tento .zip. Mějte ale na paměti, že je to neotestovaný sample pro demostrační účely.
Žádné komentáře:
Okomentovat