Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

AOP e PostSharp: scenari d’uso

Posted by Ricibald on December 7th, 2014

Normalmente un software risolve problemi specifici di un certo layer e problemi trasversali che vanno a impattare tutti i layer e tipicamente costituiscono soluzioni difficilmente rimpiazzabili. Se ad esempio utilizziamo una tecnica di logging o di gestione delle eccezioni questa sarà spalmata su tutto il progetto e un’eventuale cambiamento di gestione diventa molto complesso, lungo e impattante.

L’aspect-oriented programming (AOP) si pone l’obbiettivo di modularizzare le problematiche trasversali abilitandone astrazione, riuso e sostituibilità.

L’AOP può essere implementato in diversi modi:

PostSharp è gratuito nella versione Express. Consente di implementare qualunque aspetto “elementare” in modo completamente free, ma si possono sfruttare pattern pronti all’uso pagando un costo aggiuntivo.

In questo articolo cercherò di descrivere i pattern che PostSharp implementa: lo scopo è quello di scoprire quali sono i pattern in cui ha senso utilizzare AOP, identificarli nel proprio progetto e modularizzarli in qualche modo (anche tramite le altre due tecniche prima descritte).

Diagnostica e monitoraggio

Tramite attributo Log viene tracciato start/end del metodo, relativi parametri (nome+tipo+valore) e ritorno scrivendo su un generico LoggingBackend (System.Diagnostics.Trace, System.Console, Log4Net, NLog, Enterprise Library, custom).

Se si vuole tracciare anche le eventuali eccezioni allora si deve aggiungere al metodo l’attributo LogException. In particolare una gestione del genere consente di realizzare una gestione dell’eccezione globale in modo da poter centralizzare una policy di gestione degli errori (es. rethrow in un’eccezione più astratta).

Performance e Caching

È possibile pensare di incrementare automaticamente performance counter allo start/end di un metodo per poter monitorare le performance dell’applicazione in specifici punti.

Inoltre si potrebbe implementare un meccanismo automatico di caching che:

  • allo start calcola la chiave di cache basata su nome metodo e relativi parametri e verifica se tale cache esiste:
    • in caso positivo restituisce la chiave di cache bypassando la reale esecuzione del metodo
    • in caso negativo esegue realmente il metodo e salva il ritorno in cache

Lo scope della cache potrebbe essere globale, per-session, per-request, transient.

Validazione e Contratti

Tipicamente ogni metodo dovrebbe verificare che i parametri passati siano coerenti con quanto atteso (es. non nulli, positivi, …). La verifica manuale di ogni campo ha il problema di duplicare il codice e di legare l’implementazione al concetto. In realtà ogni metodo dovrebbe essere progettato secondo il principio Design by Contract (DbC) descrivendo il contratto richiesto. Tale contratto si tradurrà in: documentazione, validazione a tempo di compilazione e, ovviamente, implementazione.

Sebbene .NET 4.0 implementi i Code Contracts, PostSharp ha comunque la sua soluzione (più immediata) mediante attributi nel namespace Contracts da applicare ai parametri dei metodi come Required, Range, RegularExpression, GreaterThan.

Notifica delle modifiche (trigger)

Tramite attributo NotifyPropertyChanged è possibile implementare senza scrivere codice ripetitivo l’interfaccia INotifyPropertyChanged sollevando automaticamente l’evento PropertyChanged per ogni variazione delle proprietà.

Rilascio delle risorse: commit/dispose/rollback

Mediante la decorazione di PostSharp è possibile realizzare un aspect che:

  • allo start crea una risorsa da rilasciare (WebService, Socket, UnitOfWork, TransactionScope, IDisposable)
  • in caso di successo esegua (se serve) la Commit (altrimenti la Rollback)
  • all’end esegue automaticamente la relativa Dispose

Metadati e aggregati

È possibile descrivere la composizione logica di una struttura decorando un aggregato logico tramite attributo (advice) Aggregatable che descrive i field come Child (e le relative collezioni AdvisableCollection e AdvisableDictionary), Parent o Reference (cioè non coinvolto nella relazione padre/figlio).

Questa struttura con i relativi metadati è ispezionabile tramite pattern Visitor mediante metodo esposto da PostSharp VisitChildren. Tramite l’analisi della struttura è possibile implementare determinati pattern (anche custom) che propagano o meno il proprio comportamento in tutto l’aggregato.

PostSharp basandosi sul concetto di Aggregatable implementa i seguenti pattern:

Rilascio “cascade” delle risorse

In PostSharp l’attributo Disposable consente di implementare IDisposable propagando (cascade) tale chiamata a tutti gli oggetti Child coinvolti

Undo/Redo

Tramite l’attributo Recordable i metodi invocati in un’istanza vengono registrati in un corrispondente Recorder che, tramite il salvataggio di vari restore point (pattern Memento) consente undo/redo dei restore point registrati (pattern Command), eseguendo eventualmente prima di undo/redo una corrispondente callback.

Per registrare le modifiche degli oggetti figli dipendenti l’attributo Recordable si affida sulle informazioni sull’aggregato logico provenienti da Child/Parent.

Di default tutti i metodi vengono registrati come potenziali cambiamenti che concorrono a undo/redo, ma la lista dei metodi è personalizzabile e componibile in “scope” personalizzati. Inoltre il Recorder di default può essere rimpiazzato con uno custom (ad es. su db).

Threading Model

Normalmente in un codice si implementa la gestione del thread senza ricondurre la problematica a uno specifico Threading Model. Infatti ricondurre un problema di concorrenza a una problematica ricorrente consente di parlare un vocabolario comune e, grazie all’AOP, di disinteressarsi dell’implementazione (normalmente ripetitiva e fragile) la quale, in quanto rimpiazzabile, può essere espressa in modo dichiarativa, “concettuale”.

Di seguito i threading model possibili:

  • Immutable: garantisce che la classe sia immodificabile dopo il costruttore, abilitando il multithread senza rischi (thread-safe). In caso di modifiche dopo il costruttore solleva una ObjectReadOnlyException
  • Freezable: garantisce che la classe sia immodificabile dopo aver invocato Freeze(). È simile a Immutable, ma meno aggressivo. Propaga il freeze nei corrispondenti Child/Parent definiti tramite aggregati
  • ThreadUnsafe garantisce che i metodi saranno eseguiti al massimo da un thread, altrimenti eccezione
  • ThreadAffine garantisce che i metodi saranno eseguiti dallo stesso thread che ha creato l’istanza (stessa affinity)
  • Synchronized garantisce che i metodi saranno eseguiti al massimo da un thread, altrimenti l’altro attende
  • ReaderWriterSynchronized: a seconda del livello di accesso dichiarato nei metodi (nelle property sono già automaticamente impostate) garantisce le seguenti:
    • Reader: legge concorrentemente fino all’arrivo concorrente di un Writer che blocca tutti i Reader
    • Writer: scrive in modo esclusivo attendendo gli ultimi Reader e bloccando i successivi che verranno
    • UpgradeableReader: legge concorrentemente (come Reader) ma se trova un punto nel suo metodo che implica un Writer acquisisce tale lock. Consente quindi di aumentare la concorrenza dei Reader e tipicamente viene usato nelle operazioni long-running dove si cerca di accorpare i cambiamenti di stato in un’unica istruzione finale
  • Actor: tecnica per evitare completamente le problematiche di concorrenza Reader/Writer andando a definire un altro modello di comunicazione tra le classi. Invece di invocare metodi standard su oggetti e sincronizzare l’accesso, si accodano messaggi che sono poi scodati nell’ordine di accodamento in modalità asincrona (async/await) e tramite thread-pool ottimizzando le performance (CPU). Questo garantisce che se eseguo una GetXXX ma prima è avvenuta una SetXXX la sequenza delle invocazioni verrà garantita e quindi la classe sarà race-free

Tutte le verifiche automatiche da PostSharp possono essere evitate per specifici metodi (opt-out) se definito l’attributo ExplicitlySynchronized o possono essere aggiunte esplicitamente nei metodi privati (opt-in) tramite EntryPoint.

PostSharp è inoltre in grado di eseguire la Runtime Verification per verificare che il codice sia coerente con quanto descritto negli attributi: se abbiamo definito l’attributo Reader in un metodo che altera lo stato allora PostSharp ci avviserà dell’incoerenza (disabilitato in Release).

Dispatch nel giusto thread

Spesso si ha necessità di eseguire determinati metodi nel thread corretto.
Quando si esegue un metodo in una applicazione windows si dovrebbe farlo solo in un BackgroundWorker e scrivere nella GUI solo rispondendo all’evento RunWorkerCompleted per garantire di farlo nel thread UI.

Tutto questo codice può essere evitato decorando i metodi con gli attributi Background (viene eseguito in background) e Dispatched (viene eseguito nel thread UI).

Deadlock detection

Un deadlock avviene quando thread multipli rimangono in attesa reciprocamente del processamento su una risorsa condivisa (attraverso un ciclo che può anche essere ampio e difficilmente individuabile). Tramite AOP si possono intercettare e monitorare (WatchDog) le occorrenze dei metodi .NET che sincronizzano i thread, come Mutex.WaitOne, Mutex.WaitAll, Mutex.Release, Mutex.SignalAndWait, Monitor.Enter, Monitor.Exit, Monitor.TryEnter, Thread.Join e altri. Quando l’attesa supera una soglia (es. 200ms) allora viene rilevato un deadlock e viene sollevata una eccezione in tutti i thread coinvolti nel ciclo tramite il grafo di dipendenze tra thread determinato.

PostSharp consente una gestione automatica applicando a livello di assembly l’attributo DeadlockDetectionPolicy.

Sicurezza

In .NET l’attributo PrincipalPermission consente alla CLR di verificare che l’utente corrente abbia il ruolo richiesto per invocare il metodo decorato. Quando però i check di sicurezza diventono più a grana fine non è possibile esprimere tutto con il concetto di ruolo: è necessario un if!

Per coprire queste casistiche si potrebbe richiedere alle classi che richiedono check di sicurezza di implementare obbligatoriamente un’interfaccia con il metodo IsUserInRole(role) che può eseguire check custom ove richiesto. Potremmo poi definire tramite PostSharp un attributo SecuredOperation(role) per richiamare tale metodo nelle classi che lo dichiarano (altrimenti comportamento standard tramite .NET).

A questa tecnica può essere abbinato anche il corrispondente auditing andando a scrivere in un Backend specializzato per l’auditing (event log).

Policy architetturali, convenzioni e patch di framework

AOP può essere usato per sollevare eccezioni a tempo di compilazione, per rinforzare l’utilizzo di determinati pattern architetturali o convenzioni e verificarli in modo automatico (da Visual Studio o mediante build server). Ad esempio potremmo stabilire la regola che un certo layer non è mai invocabile dai layer sottostanti.

Inoltre può essere sfruttato per “fixare” lacune del framework. Ad esempio in .NET per rendere serializzabile un oggetto esiste l‘attributo Serializable che però non è un’interfaccia e non può essere utilizzata per rinforzare determinati vincoli (es. un metodo clone che accetta solo oggetti serializable). Sarebbe utile il meccanismo usato in Java in cui esiste l’interfaccia “marker” Serializable. Tramite AOP e PostSharp potremmo dichiarare una nostra interfaccia “marker” ISerializable e fare in modo che le classi che implementano tale interfaccia abbiano automaticamente dichiarato l’attributo Serializable (per iniettare attributi in PostSharp si usa CustomAttributeIntroductionAspect).

Lo stesso meccanismo può essere sfruttato per rinforzare policy tramite convenzioni:

  • tutti i tipi non nullable sono automaticamente Required
  • tutti i tipi che terminano con CreditCard sono automaticamente soggetti all’attributo RegularExpression
  • tutti i tipi da un certo namespace in poi (es. company.application.domain.*) sono automaticamente Serializable
  • tutti i metodi del layer Application creano e committano automaticamente un relativo UnitOfWork
  • tutte le eccezioni catturate nel layer Appication vengono automaticamente convertite in un’eccezione di più alto livello (exception policy)
  • tutti le classi che terminano con ViewModel implementano automaticamente l’interfaccia INotifyPropertyChanged

Persistenza

Altri casi interessanti:

  • field virtualization: la semantica di un campo (read/write) potrebbe differire dallo store in cui viene memorizzato (memory, db, …). Tramite AOP possiamo disaccoppiare i due aspetti
  • lazy loading: a volte si dichiarano alcune proprietà come Lazy poiché costose da inizializzare. Come per il field virtualization, l’AOP consente di disaccoppiare il concetto di lazy loading dalla sua reale implementazione
  • change tracking: l’attributo Recordable consente di determinare le istanze dirty e propagare in blocco i cambiamenti verso lo store (lo stesso NHibernate internamente fa uso di AOP).

Leave a Reply

You must be logged in to post a comment.