TSM - Proiectarea sistemelor .NET complexe cu Managed Extensibility Framework

Horațiu Florea - .NET software developer @ BisSoft


Fără să exagerăm, se poate spune că ne aflăm la începutul unei noi evoluţii a IT-ului. Pentru majoritatea companiilor, îndeplinirea cerinţelor funcţionale şi livrarea unui produs performant în timp util au în mod evident cea mai mare prioritate. De aceea, mare parte din sistemele software complexe exercită nevoia de a putea fi extinse cu noi funcţionalităţi, respectiv cu noi module, într-o manieră simplă şi eficientă. Pentru că proiectarea unui sistem software extensibil nu este uşor de realizat, au fost dezvoltate o serie de platforme care facilitează proiectarea acestor sisteme software. Una dintre cele mai noi astfel de platforme este Managed Extensibility Framework (MEF). Platforma MEF permite dezvoltarea de aplicaţii .Net extensibile.

MEF se fundamentează pe trei concepte fundamentale care conferă valoare şi utilitate acestei platforme: extensibilitate, detecţie și metadata.

Extensibilitate

Un sistem extensibil permite adăugarea şi/sau înlocuirea unor funcţionalităţi existente, fără a aduce modificări acelui sistem. Extensibilitatea se realizează prin compunere: interconectarea mai multor părţi (extensii, plug-in-uri) cu scopul de a forma un întreg. Compunerea asigură crearea unui sistem slab cuplat, coeziv, întrucât se evită utilizarea dependinţelor rigide între componente şi respectă două dintre principiile SOLID- SOLID reprezintă un acronim ce face referire la cinci principii de bază ale programării orientate pe obiecte, principii ce pun bazele unui cod bine scris, uşor de întreținut şi optimizat pentru modificabilitate: Open/Close (sistemul facilitează extinderea funcționalităților (open), fără a aduce modificări acelui sistem (close)și Dependința Inversă (modulele high-level sau ,în cazul nostru, extensiile, care nu depind de modulele low-level, aplicaţia gazdă, ci depind de abstracţii, concepte MEF pe care le vom trata mai jos.

Probabil, cel mai important aspect de menţionat aici este faptul că extensibilitatea se realizează la execuţie (de aceea, ea se mai numeşte şi extensibilitate dinamică). Extensiile sunt integrate şi injectate în sistem la rularea aplicaţiei, şi nu este nevoie de re-compilare, pentru ca extensiile să poată fi detectate şi utilizate că părţi componente ale acelei aplicaţii.

Deşi există numeroase alte soluţii care tratează această arie a extensibilităţii, dintre care putem aminti de Managed Addin Framework (MAF) sau de framework-uri de IoC (StructureMap, Unity), Microsoft a constatat că aceste soluţii nu sunt viabile pentru extensibilitatea cu componente terţe, deoarece astfel de soluţii sunt mult prea sofisticate şi necesită o infrastructură construită din fază incipientă. MEF oferă out-of-the-box o arhitectură simplistă folosind un model de programare declarativ bazat pe utilizarea atributelor .NET obişnuite, şi permite aplicaţiei să expună atât serviciile disponibile, cât şi nevoia de a consuma aceste servicii (extensii). De fapt, acesta reprezintă principiul fundamental ce stă la baza platformei MEF: furnizarea de servicii şi trebuinţa de a utiliza serviciile.

MEF e componentă a platformei .NET 4.0 şi tot ce ţine de acest framework e conţinut în librăria System.ComponentModel.Composition. Această librărie găzduieşte atributele ce vor fi folosite în realizarea compunerii şi câteva clase destinate tratării diferitelor excepţii.

Detecţie

Acest concept face referire la modul în care detectăm plug-in-urile ce vor fi încărcate în sistemul nostru extensibil, decuplat. Premisa de bază este următoarea: la crearea unei clase, dorim că acea clasă să realizeze ceva specific, un serviciu. Să ne gândim acum la o serie de astfel de clase aflate într-un ansamblu separat, care oferă o serie de servicii specifice. La un moment dat, vom avea nevoie să utilizăm acele servicii în aplicaţia principală, însă nu dorim să ţinem o dependință directă cu aceste servicii, întrucât acest lucru poate să îngreuneze testarea şi modificarea ulterioară a aplicaţiei. Vom folosi o interfaţă pentru aceste servicii, şi vom face referință în aplicaţia gazdă numai această la interfaţă. Totuşi, mai este nevoie de ceva pentru a conecta interfaţa de implementare: detecţia, reprezentând mecanismul de detectare şi furnizare a implementărilor astfel încât celelalte componente ale sistemului să le poată utiliza, la nevoie.

Metadata

Reprezintă tehnologia MEF prin care se pot furniza aspecte, capabilităţi şi informaţii adiţionale despre componente. Prin intermediul metadata, aplicaţia poate să filtreze şi să examineze componentele, fără a cunoaşte detalii despre implementarea acestora. Metadata oferă o abordare contextuală de a determina ce componente sunt potrivite pentru mediul curent, filtrându-le în mod corespunzător şi apoi, încărcându-le.

Principii şi terminologii

Din cele prezentate mai sus, reiese că din punct de vedere al platformei MEF, un sistem software este compus din aşa numitele părţi componente [(părţi ce pot fi compuse), composable parts [eng]]. O astfel de parte oferă servicii altor părţi şi la rândul ei consumă servicii oferite de alte părţi. Faptul că aceste părţi sunt dezvoltate odată cu sistemul software sau ulterior că şi componente terţe, nu reprezintă nici o diferenţă. Principalele concepte care trebuie înţelese pentru a putea folosi platforma MEF sunt:

Figura 1: Exemplu de arhitectură bazată pe MEF

În Figura 1 este prezentat un exemplu de arhitectură care folosește MEF pentru a interconecta părțile componente ale sistemului software. Astfel, sistemul software este compus din patru componente (composable parts), două componente exportă servicii (composable part 1 și composable part 2) care sunt importate de alte două componente (composable part 3 și composable part 4). În acest exemplu, platforma MEF are rolul de a face disponibile serviciile exportate de componentele composable part 1 și composable part 2 către componentele composable part 3 și composable part 4. Astfel componentele care exportă servicii, implementează interfețele contract, iar componentele care importă serviciile vor obține referințe la servicii prin intermediul platformei MEF și vor putea apela funcționalitatea publică a acelor servicii definită prin intermediul interfețelor contract. Rolul platformei MEF este acela de a face transparent legătura între componente asigurând o decuplare totală a componentelor sistemului. Astfel platforma se ocupă de crearea instanțelor, iar singurul element de legătură între componentele sistemului software este reprezentat de interfețele contract care reprezintă o abstractizare a serviciilor. Cu alte cuvinte, dacă se dorește o implementare diferită a unui serviciu, nu trebuie decât să se realizeze o nouă implementare a interfeței care definește contractul acelui serviciu și să se exporte acea implementare prin intermediul platformei MEF.

Catalog

Modelul MEF bazat pe compunere implică utilizarea unui mecanism de detecţie automată a părţilor, la execuţie. Acest mecanism se realizează prin folosirea unui obiect ce poată numele de Catalog. MEF oferă out-of-the-box patru tipuri de cataloage, clasificare indusă de modalitatea prin care dorim să detectăm părţile:

Container

Container-ul este responsabil pentru gestionarea compunerii, prin crearea dependinţelor dintre părţi, asociind exporturile cu importurile în mod corespunzător. Pentru a realiza compunerea, container-ul utilizează cataloage, unde sunt încărcate toate părţile disponibile ce au fost detectate în sistem. Cel mai comun tip de container este CompositionContainer, iar compunerea propriu-zisă se efectuează la apelul metodei ComposeParts; de obicei, această acţiune are loc la începutul aplicaţiei (ex: în cardul procesului de bootstraping).

Figura 2: Arhitectura MEF la nivel general

Figura 2 ilustrează o diagramă de ansamblu a arhitecturii MEF. Un catalog este responsabil pentru detectarea componentelor, prin asocierea părţilor exportate (funizoare de servicii) cu părţile importate (utilizatoare de servicii). Container-ul (o instanţă de CompositionContainer) realizează compunerea efectivă a părţilor, prin intermediul catalogului ce conţine părţile detectate.

Demo: Aplicația Calculator

Pentru a demonstra modul de funcţionare a platformei MEF, vom prezenţa o mică aplicaţie ce simulează un calculator simplu care poate să efectueze patru operații aritmetice de bază: adunare, scădere, înmulţire şi împărţire, expuse că servicii (extensii) ce vor fi importate cu ajutorul compunerii MEF.

Stuctura soluției

Figura 3: Structura soluţiei pentru aplicaţia MEF Calculator

Soluţia cuprinde trei proiecte, ilustrate în Figura 3:

Pentru a evidenţia utilizarea diferitelor tipuri de cataloage , am inclus în directorul Extensions (din aplicaţia gazdă CalculatorDemoMef) referinţă către o componentă terţă (Division.dll) care va trata operaţia de împărţire.

Poate cel mai important aspect de menţionat e că aplicaţia gazdă CalculatorDemoMef nu necesită referinţă către ansamblul CalculatorExtension, ci va referi doar librăria CalculatorContract. Cu alte cuvine, aplicaţia consumatoare de servicii (extensii) nu ţine dependință directă spre aplicaţia furnizoare de servicii; detecţia şi încărcarea părţilor se realizează prin intermediul unui contract (ansamblu separat). Reiese un sistem modular, slab cuplat şi coeziv, uşor de menţinut şi de testat.

Părţile extensibile (Export, Import, Contract)

[Export(typeof(IOperation))]
public class Add : IOperation
{
    public int Calculate(int num1, int num2)
    {
        return num1 + num2;
    }
}

Cele patru clase (operații aritmetice de bază) sunt decorate cu atributul Export, pentru a indica faptul că aceste clase vor fi exportate în modelul de compunere, şi vor respecta contractul de tip de dată IOperation. Mai sus, este prezentată extensia aferentă operaţiei de adunare. Similar, se creează şi extensiile pentru operaţiile de scădere, înmulțire și împărțire.

[ImportMany]
public IEnumerable<IOperation> CalculatorPlugins { get; set; }

În aplicaţia gazdă (în exemplul nostru, în clasa Program.cs), vom decora obiectul CalculatorPlugins cu atributul ImportMany, pentru a indica faptul că mai multe extensii se vor asocia acestui obiect. Asocierea se realizează prin atribuirea aceluiaşi contract IOperation ca tip de dată al obiectului CalculatorPlugins. Deoarece avem de a face cu cardinalitate "0 la n" (ImportMany), obiectul CalculatorPlugins va fi o colecţie de IOperation.

Realizarea compunerii (Catalog şi Container)

private const string AssemblyPath = @"C:\Projects\CalculatorDemoMef\CalculatorExtension\bin\Debug\CalculatorExtension.dll";

private const string DirectoryPath = @"C:\Projects\CalculatorDemoMef\CalculatorDemoMef\Extensions";

private void AssembleCalculatorComponents()
{
   //An aggregate catalog that combines
   // multiple catalogs

   var catalog = new AggregateCatalog();

   //Add all the parts found in the assembly
   //located at this path

  catalog.Catalogs.Add(new AssemblyCatalog(Assembly. 
     LoadFrom(AssemblyPath)));

   //Add all the parts found in the assemblies
   //contained in the directory located at this path

  catalog.Catalogs.Add(new  
     DirectoryCatalog(DirectoryPath, "*.dll"));

    //Create the CompositionContainer with the parts 
  //in the catalog

 var container = new CompositionContainer(catalog);

    //Fill the imports of this object
    try
    {
        container.ComposeParts(this);
    }
    catch (CompositionException compositionException)
    {
        Console.WriteLine(compositionException.
           ToString());
    }
}

[ImportMany]
public IEnumerable<IOperation> CalculatorPlugins { get; set; }

Părţile extensibile sunt detectate din două locaţii diferite. Vom utiliza un catalog de tip AssemblyCatalog pentru detecţia extensiilor localizate în ansamblul CalculatorExtension (extensii associate operaţiilor de adunare, scădere şi înmulţire), şi un catalog de tip DirectoryCatalog pentru detecţia extensiilor localizate în directorul Extensions (extensia furnizată de o componentă terţă şi referită printr-un fişier dll, asociată operaţiei de împărţire). Deoarece avem de-a face cu două tipuri diferite de cataloage, vom utiliza un AggregateCatalog, unde vom adăuga cele două cataloage. Acest AggregateCatalog va fi folosit de către container (CompositionContainer) pentru realizarea compunerii.

Figura 4: Părţile încărcate în catalog

Figura 4 prezintă maniera în care părţile sunt încărcate în AggregateCatalog. Se obervă că primele trei extensii (Add, Subtract, Multiply) provin din ansamblul CalcuatorExtension, iar cea de a patra extensie (Divide) provine din ansambulul Division.

Metadata

Marele avantaj oferit de Metadata este acela că permite exporturilor (plug-in-urilor) să poată fi detectate şi folosite (ex. pentru filtrare) înainte de crearea propriu-zisă a părţii, evitând astfel încărcarea plug-in-urilor în memorie, în caz că nu avem nevoie de ele.

[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
public class Add : IOperation
{
    public int Calculate(int num1, int num2)
    {
        return num1 + num2;
    }

Prin intermediul atributului ExportMetadata şi a unei perechi cheie-valoare, putem să descriem semnificaţia fiecărui plug-in exportat. MEF permite decorarea unui plugin cu mai multe atribute ExportMetadata, în cazul în care dorim să asociem mai multe informaţii.

Pentru a putea folosi metadata-ul furnizat de exporturi, trebuie să folosim tipul generic Lazy<T>, ce oferă suport pentru instanțierea "leneşă" (instanțierea se amână până când instanţa respectivă e într-adevăr necesară). De fapt, vom utiliza o variantă de Lazy<T>, şi anume Lazy<T,Metadata> prin intermediul căruia vom furniza o referinţă "leneşă" indirectă către un plug-in şi către metadata-ul asociat. Acest tip are trei proprietăţi, care prezintă interes:

[ImportMany]
public IEnumerable<Lazy<IOperation, IDictionary<string, object>>> CalculatorPlugins { get; set; }

Cu ajutorul metadata, putem să inspectăm şi să filtrăm plug-in-urile.

var somePlugins = CalculatorPlugins.Where(p => p.Metadata["Symbol"].Equals('+') || p.Metadata["Symbol"].Equals('*'));

Dezavantajul atributulului ExportMetadata este acela că el implică o abordare de tip slab (weakly type [eng]), întrucât nu avem nici o garanţie că cheia de metadata solicitată există (nu se poate verifica validitatea ei la compilare). Pentru a rezolva această problemă, putem utiliza metadata de tip puternic (stongly type [eng]). Această abordare necesită două piese importante: o interfaţă pentru definirea proprietăţiilor de metadata şi un atribut Custom.

Concluzii

MEF oferă out-of-the-box o platformă eficientă pentru extinderea aplicaţiilor, prin detectarea dinamică a componentelor la execuţie. Componentele terţe sunt integrate cu uşurinţă în aplicaţie, fără nevoia de a cunoaşte detalii de implementare despre acestea, iar lipsa dependinţelor directe dintre aplicaţie şi părţile extensibile creează un sistem modular slab cuplat şi coeziv ce facilitează mentenanţa, testarea şi reutilizarea.

Bibliografie

  1. https://mef.codeplex.com/
  2. http://www.codeproject.com/Articles/188054/An-Introduction-to-Managed-Extensibility-Framework
  3. https://www.safaribooksonline.com/library/view/fundamentals-of-the/9780132929400/
  4. https://app.pluralsight.com/player?course=mef&author=dustin-davis&name=mef-m2-parts&clip=0&mode=live
  5. https://visualstudiomagazine.com/articles/2013/02/12/mef-convention-model.aspx