TSM - AOP folosind .NET stack

Radu Vunvulea - Solution Architect

În cele ce urmează vom discuta despre AOP și despre cum putem implementa propria noastră stivă (stack) AOP utilizând caracteristicile .NET Core. Acronimul vine de la Aspect Oriented Programming și este o altă paradigmă de programare cu scopul principal de a crește modularitatea unei aplicații. AOP încearcă să atingă acest țel permițând separarea aspectelor/relațiilor secante (cross-cutting concerns).

Fiecare parte a aplicației este împărțită în unități distincte bazate pe funcționalitate (aspecte/relații). Bineînțeles, chiar dacă nu utilizăm această paradigmă, vom încerca să avem o separare clară a funcționalităților. Dar în AOP toate funcționalitățile sunt separate, chiar și cele pe care în OOP le acceptăm ca fiind secante.

Un exemplu bun în acest caz este audit (controlul) și logging (jurnalizare). În mod normal, dacă utilizăm OOP pentru a dezvolta o aplicație care necesită logging și audit, vom avea într-o formă sau alta diferite apelări ale mecanismului de logging din codul nostru. În OOP, acest lucru poate fi acceptat, deoarece aceasta este singura modalitate de a scrie logaritmi, de a prelucra și așa mai departe.

Când folosim AOP, implementarea sistemului de logging sau audit va trebui plasată într-un modul separat. În plus, vom avea nevoie de o modalitate de a scrie informația de logging fără a scrie cod în alte module care fac apelarea în sine a sistemului de logging.

Există diferite opțiuni care pot fi utilizate în AOP pentru a soluționa această problemă. Depinde de tipul de tehnologie pe care îl utilizați, de tipul de stack (stivă) pe care doriți să-l folosiți și așa mai departe. Am putea utiliza atribute (adnotații) ale metodelor și claselor care ar activa logging și audit. O altă abordare este configurarea unor înlocuitori care vor fi folosiți de către sistem drept implementarea reală a unei funcționalități. Acest înlocuitor va fi capabil să scrie logs și să facă apelarea implementării de bază.

Interceptarea

Aproape toate cadrele disponibile pentru AOP sunt în jurul interceptării. Folosind interceptarea, dezvoltatorii pot specifica ce metode trebuie interceptate și ce ar trebui să se întâmple în acest caz. Prin interceptare, noi putem chiar să adăugăm noi câmpuri, proprietăți , metode sau chiar să modificăm implementarea curentă.

Cum este implementat?

De obicei, există două moduri oferite de diferitele cadre care oferă suport AOP în timpul de execuție și în timpul de compilare.

Când cadrul oferă suport AOP în timpul execuției, înseamnă că va crea în timp real diverși reprezentanți care vor redirecționa apelările noastre. Aceasta ne va da o mare flexibilitate, făcându-ne capabili să modificăm în timpul execuției comportamentul aplicației noastre.

O altă abordare este în timpul de compilare. Acest tip de cadre sunt de obicei integrate cu IDE și cu mediul de dezvoltare. În timpul de compilare, ele vor aduce modificări codului nostru și vor introduce apelările diferitelor functionalități. În final, va rezulta același cod ca și când am apela codul prin metoda noastră, dar codul pe care trebuie să îl întreținem este mai simplu, curat și cu toate aspectele clar separate.

Costuri

Când cadrul AOP folosește reprezentanți în timp real, performanța aplicației noastre poate să scadă. Acest lucru se întâmplă deoarece există cineva la mijloc care interceptează apelările noastre și face anumite acțiuni. La acest nivel de obicei se folosește reflecția, iar noi știm cu toții că reflecțiile costă mult din perspectiva procesorului.

A avea un cod care se modifică în timpul de compilare, înseamnă că putem avea anumite probleme când este nevoie să corectăm codul și să găsim o problemă. Aceasta se întâmplă deoarece în timpul de execuție nu rămâi doar cu codul tău, ci sfârșești prin a avea codul tău original din relația de bază, plus codul din a doua relație de care ai avut nevoie în relația de bază și cu codul cadru AOP care face redirecționarea.

După cum putem vedea în diagrama de mai sus, numai primul și ultimul pas fac parte din ciclul normal. Restul sunt adăugați de cadrul AOP.

Componentele de bază ale AOP

Join Points (Puncte de întâlnire)

Un punct de întâlnire este reprezentat de punctul din cod unde este necesar să punem în aplicare operațiile propuse de noi. Acesta este un punct din cod unde poți face o apelare a unei alte funcționalități într-un mod foarte simplu. În general, cadrele care susțin AOP utilizează metode, clase și proprietăți care reprezintă principalele lor puncte de întâlnire.

De exemplu, înainte și/sau în timpul apelării unei metode, poți executa codul tău obișnuit. Același lucru se poate întâmpla pentru clase și proprietăți. În general, vei descoperi că conceptele AOP sunt foarte simple, dar destul de dificil de implementat păstrând un cod curat și simplu.

Pointcuts

Definesc o modalitate de a specifica un punct de întâlnire în sistemul tău. Aceasta oferă dezvoltatorului posibilitatea de a specifica și identifica un punct de întâlnire în sistem unde dorim să facem o anume apelare. Acest lucru poate fi realizat în diferite feluri, de la atribute (adnotații) la configurații diverse (în fișiere, în cod și multe altele).

Sfatul

Sfatul se referă la codul pe care vrem să îl executăm când ajungem la un punct de întâlnire. În principiu, acesta este reprezentat de codul care este apelat în jurul unui punct de întâlnire.

De exemplu, înainte ca o metodă să fie apelată, dorim să scriem niște informații pe traseu.

Aspect

Aspectul este format din două elemente diferite - pointcut și sfatul. Combinarea acestor două elemente formează un aspect. În general, când vrem să utilizăm AOP, avem o locație unde dorim să executăm codul și codul obișnuit care vrem să fie executat.

.NET Stacks

Există moduri diferite de a implementa AOP în .NET. Pe piață, veți găsi numeroase cadre care oferă acest suport, o parte dintre ele sunt gratuite.

Unity

Unity oferă suport pentru AOP într-o anumită parte a aplicației. În general, Unity oferă suport pentru scenariile cele mai comune, cum ar fi tratarea excepțiilor, logging, protecție sau acces la date.

PostSharp

Acesta este unul dintre cele mai cunoscute cadre în lumea .NET pentru AOP. Nu este un cadru gratuit, dar este plin de caracteristici utile și este 100% integrat cu mediul de dezvoltare.

Aspect.NET

Acesta este un instrument gratuit care poate fi găsit pe Codeplex. El oferă funcționalitățile de bază necesare pentru a dezvolta o aplicație utilizând paradigma AOP.

Enterprise Library

Această bibliotecă oferă de asemenea suport pentru AOP, furnizând diferite capabilități precum autorizarea, tratarea excepțiilor, validare, logging și calculator pentru performanță. Este destul de asemănătoare cu funcționalitățile oferite de Unity.

AspectSharp

Aceasta este o altă stivă similară cu Aspect.NET. În ambele cazuri, trebuie să știți că aveți acces numai la funcționalitățile de bază ale paradigmei AOP.

Castle Project - Dynamic Proxy

Asemănător cu AspectSharp. Aceasta este o bibliotecă susținută de către comunitate și veți putea găsi mult suport și informații utile pe diferite forumuri și bloguri.

Din perspectiva mea, instrumentul care oferă toate caracteristicile de care ai nevoie atunci când vrei să utilizezi AOP este PostSharp. Dar, în funcție de nevoile proprii, ar trebui să încercați să identificați instrumentul care satisface nevoile voastre.

Ce oferă .NET Code

Vestea bună este că noi putem utiliza AOP fără niciun fel de instrument. Poate fi mai complicat, dar ceea ce poți realiza utilizând .NET API este destul de interesant. În următoarea parte a articolului vom vedea cum putem folosi clasa RealProxy pentru a intercepta apelările metodei și pentru a injecta comportament obișnuit. Clasa RealProxy poate fi găsită în stiva .NET Core.

Cea mai importantă metodă a RealProxy este "Invoke"(Invocarea). Această metodă este apelată de fiecare dată când este apelată o metodă din clasa ta specifică. De aici, poți accesa numele metodei, parametrii și poți apela metoda ta reală sau una falsă.

Este important de știut că aceasta va funcționa numai când folosiți și interfețele.

În următorul exemplu, vom vedea cum putem implementa un mecanism de profilare obișnuit, utilizând clasa RealProxy.

Primul pas este să creăm un atribut obișnuit, care acceptă un mesaj obișnuit care va fi scris atunci când scriem durata pe traseu.

public class DurationProfillingAttribute : Attribute
{
   public DurationProfillingAttribute(string message)
   {
       Message = message;
   }

   public DurationProfillingAttribute()
   {
       Message = string.Empty;
   }

   public string Message { get; set; }
}

Apoi avem nevoie de o clasă generală care extinde RealProxy și calculează durata apelării. În metoda Invocare va trebui să utilizăm un cronometru care va calcula cât durează o apelare. La acest nivel, putem verifica dacă o metodă specifică este decorată cu atributul nostru.

public class DurationProfilingDynamicProxy : RealProxy
{
    private readonly T _decorated;

    public DurationProfilingDynamicProxy(T decorated)
        : base(typeof(T))
    {
        _decorated = decorated;
    }

    public override IMessage Invoke(IMessage msg)
    {
    IMethodCallMessage methodCall = 
		(IMethodCallMessage)msg;
    
MethodInfo methodInfo = methodCall.MethodBase as 
		MethodInfo;

DurationProfillingAttribute profillingAttribute = 
(DurationProfillingAttribute)methodInfo.
GetCustomAttributes(typeof(
DurationProfillingAttribute)).FirstOrDefault();

// Method don"t needs to be measured. 
if (profillingAttribute == null)
{
    return NormalInvoke(methodInfo, methodCall);
}

return ProfiledInvoke(methodInfo, methodCall,
		 profillingAttribute.Message);
}

private IMessage ProfiledInvoke(MethodInfo methodInfo, IMethodCallMessage methodCall, string profiledMessage)
{
   Stopwatch stopWatch = null;
   try
   {
    stopWatch = Stopwatch.StartNew();
    var result = InvokeMethod(methodInfo, methodCall);
  	          stopWatch.Stop();

    WriteMessage(profiledMessage, 
	methodInfo.DeclaringType.FullName, 
	methodInfo.Name, stopWatch.Elapsed);

     return new ReturnMessage(result, null, 0,
                methodCall.LogicalCallContext,
		methodCall);


     }
     catch (Exception e)
     {
       if (stopWatch != null
            && stopWatch.IsRunning)
         {
            stopWatch.Stop();
          }
          return new ReturnMessage(e, methodCall);
      }
    }

  private IMessage NormalInvoke(MethodInfo methodInfo, 
		IMethodCallMessage methodCall)
   {
   try
   {
    var result = InvokeMethod(methodInfo, methodCall);

    return new ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
          return new ReturnMessage(e, methodCall);
      }
    }

   private object InvokeMethod(MethodInfo methodInfo, 
	  IMethodCallMessage methodCall)
    {
    object result = methodInfo.Invoke(_decorated, 
		methodCall.InArgs);
       
    return result;
    }


   private void WriteMessage(string message, string 
	className, string methodName, 
	TimeSpan elapsedTime)
    {
    Trace.WriteLine(string.Format("
	Duration Profiling: "{0}" for "{1}.{2}" 
	Duration:"{3}"", message, 		
	className,methodName, elapsedTime));
    }

Am putea avea o altă abordare aici, calculând durata pentru toate metodele din clasă. Puteți găsi mai jos clasele utilizate pentru a testa implementarea. Folosind metoda "GetTransparentProxy" putem obține o referință la interfața noastră.

class Program
{
 static void Main(string[] args)
  {
   DurationProfilingDynamicProxy 
   fooDurationProfiling = new
     DurationProfilingDynamicProxy(new Foo());
    
    IFoo foo = (IFoo)fooDurationProfiling.
	GetTransparentProxy();

        foo.GetCurrentTime();
        foo.Concat("A", "B");
        foo.LongRunning();
        foo.NoProfiling();
    }
}

public interface IFoo
{
[DurationProfilling("Some text")]
DateTime GetCurrentTime();

[DurationProfilling]
string Concat(string a, string b);

[DurationProfilling("After 2 seconds")]
void LongRunning();

string NoProfiling();
}

public class Foo : IFoo
{
public DateTime GetCurrentTime()
{
    return DateTime.UtcNow;
}

public string Concat(string a, string b)
{
    return a + b;
}

public void LongRunning()
{
    Thread.Sleep(TimeSpan.FromSeconds(2));
}

public string NoProfiling()
{
    return "NoProfiling";
}

}

Concluzie

În concluzie, putem spune că AOP ne poate face viața mai ușoară. Aceasta nu înseamnă că de acum o vom folosi în toate proiectele. AOP trebuie utilizată numai acolo unde are sens și de obicei poate fi foarte utilă când lucrăm la un proiect care este foarte complicat, cu multe module și funcționalități.

În următorul articol vom descoperi cum putem utiliza Unity pentru a implementa AOP.