No, non è un errore di battitura. Ho proprio scritto aspetti. La programmazione orientata agli aspetti (AOP, aspect oriented programming) è un paradigma di programmazione che completa (meglio, estende) la programmazione orientata agli oggetti, semplificando certe operazioni.
Indice |
Preambolo
- Di solito inizio a scrivere un articolo, poi lo metto in stand-by per mancanza di tempo e il tutto si trascina per mesi. Questo sarà un articolo molto breve per ovviare a questo problema. Dovesse esserci interesse posso scriverne una continuazione.
- Per capire di cosa stiamo parlando è necessaria un'infarinatura di base di programmazione orientata agli oggetti e conoscenze di C#.
- A causa di un problema con la formattazione degli articolo del quale non sono ancora venuto a capo (https://www.electroyou.it/forum/viewtopic.php?f=5&t=75667), il sorgente è in forma di screeenshot e non come testo.
Toolchain
- Benché sia possibile implementare AOP da zero, usando costrutti nativi della lingua che si usa, questo approccio è molto macchinoso. Noi opteremo (come il 99% degli utilizzatori di AOP) per un'estensione del compiler. Visto che lavoro principalmente con C#, la toolchain sarà Visual Studio, con un'estensione chiamata PostSharp.
- Non ho alcuna affiliazione con SharpCrafters, tuttavia mi sento di consigliare Postsharp perché lo uso regolarmente e sono convinto della sua utilità.
- PostSharp è a pagamento, ma ce n'è una versione gratuita e fortemente limitata chiamata PostSharp Essentials che è perfettamente adatta a fare i primi passi in questo campo.
Un po' di teoria (e nomenclatura)
Gli oggetti che usiamo programmando, per esempio in C#, sono le classi, le proprietà, i metodi... Sono entità che contengono informazione, la elaborano, la mettono a disposizione. Sono le parti utili del programma, quelle che fanno qualcosa. Dobbiamo però spesso usarle per lavori meno utili, lavori necessari a permettere il buon funzionamento del resto. Questi lavori meno interessanti sono la gestione dei threads, l'exception handling, il logging, eccetera. Tutte cose che potrei omettere, ottenendo un programma funzionante, ma, a dipendenza dei casi, ingestibile, lentissimo o totalmente instabile. È quindi buona cosa occuparsene, anche se meno interessante dell'algoritmica che dà i risultati cercati.
Queste funzioni ausiliarie sono dette aspetti. Sono caratterizzati dal fatto di essere dispersi un po' in tutto il programma senza appartenere semanticamente ad una parte specifica dello stesso.
Qui abbiamo la visualizzazione delle classi usate da Apache Tomcat. Sono evidenziate le funzioni che si occupano del logging.
Da qui risulta chiaro perché gli aspetti sono classificati come cross-cutting concerns. Altri esempi classici sono la verifica dell'integrità delle variabili, funzioni di sincronizzazione, gestione della memoria, sicurezza (user management),...
Uno dei principi della programmazione orientata agli oggetti (OOP) è la segregazione delle funzionalità. In soldoni: meno una classe deve sapere dell'altra, meglio è. Questi aspetti violano palesemente questo principio. Qui ci viene in aiuto l'AOP.
Usando un'estensione AOP per il compiler possiamo lasciare il codice riguardante i cross-cutting concerns al di fuori della nostra implementazione. Quando si compila il codice necessario alla loro esecuzione viene integrato automaticamente nel nostro codice. In inglese il tool che esegue questa operazione si chiama aspect weaver (tessitore di aspetti). Il nome, secondo me, spiega bene cosa fa: il codice che svolge le funzioni utili è come l'ordito di un tessuto, i cross cutting concerns ne formano la trama, che attraversa orizzontalmente l'ordito.
Questi aspetti possono essere pre-compilati e forniti assieme al weaver o scritti ad-hoc per l'applicazione e registrati nel weaver. Ovviamente è possibile anche miscelare le due fonti.
Un esempio: thread safety
Userò una piccola e insulsa applicazione per dimostrare certi effetti. Si tratta di un'applicazione Windows Forms con l'interfaccia grafica visibile qui sotto.
Ho istanziato un tasto e due etichette di testo. Inoltre, non visibile, c'è un timer con intervallo 100 ms. Una delle due etichette viene continuamente aggiornata dal timer e indica i secondi del clock di sistema. L'altra viene controllata dal tasto: quando lo si clicca il programma memorizza il numero di secondi in quel momento, poi aspetta 5 secondi e aggiorna il valore visualizzato.
Un'applicazione all-in-one (ovvero Il Problema)
Nel primo esempio implementiamo la funzionalità richiesta senza l'uso di threads.
Il codice è estremamente semplice:
Il timer viene attivato all'inizializzazione della classe e a partire da quel momento genera 10 eventi al secondo. Questi vengono gestiti dall'event handler (timer1_Tick) che sovrascrive il testo di label2 con il numero di secondi dell'ora attuale. Quando si clicca sul bottone, viene chiamata l'altra funzione presente (button1_Click). Il numero di secondi viene memorizzato in una variabile, il thread va in sleep e dopo 5 secondi il valore memorizzato viene scritto in label1.
Forse troppo semplice: quando il thread è in sleep, il timer non produce eventi e anche se lo facesse, non verrebbero eseguiti. Il risultato è che il contatore dei secondi (label2) si blocca per 5 secondi.
Un'applicazione con due thread (ovvero La Soluzione Classica)
La soluzione è far aspettare quei 5 secondi al programma in un thread separato. Ecco il codice:
In linea 17 riconosciamo l'handler per l'evento del timer. Qui non è cambiato nulla. Invece l'handler del tasto (button1_Click) apre un nuovo thread e vi avvia la funzione MethodRunningAsThread. Dopodiché ritorna il controllo al thread principale, in modo che gli eventi del timer possano venir gestiti. Il risultato è che anche premendo il tasto il contatore dei secondi viene correttamente aggiornato.
Nella funzione che viene avviata, dopo la pennicchella di 5 secondi, viene generato un evento. È quello che ho definito in linea 15. Inoltre, in fondo al file, è definita una classe derivata da EventArgs, che mi serve a passare il numero di secondi al quale il thread secondario è stato avviato al thread principale.
La cosa interessante, particolare (e noiosa) è che ci servono due metodi e un delegate per garantire la thread-safety dell'operaziona di visualizzazione dei dati provenienti dal thread secondario. Se il thread accedesse direttamente la UI si potrebbero avere delle condizioni di race. L'evento viene gestito sempre dalla funzione con signatura (object sender, StringEventArgs sea). Questa non accede mai direttamente alla UI, ma chiama la funzione omonima con signatura (string s) che riconosce se è necessario usare un delegate e si chiama ricorsivamente tramite lo stesso.
Normalmente avrei messo il codice del thread in un file cs separato. Ciononostante si tratta di parecchio codice per un'operazione abbastanza banale. La leggibilità del codice ne risente.
Un'altra applicazione con due thread (ovvero La Soluzione Orientata agli Aspetti)
Usando un tool per l'AOP, in questo caso PostSharp, il codice diventa:
La prima cosa che si nota è la leggibilità. Dall'alto in basso:
- Iniziallizazione dell'applicazione e avvio del timer
- Gestione degli eventi del timer
- Gestione degli eventi del bottone: come prima avvio una funzione in un nuovo thread, ma l'unica cosa che faccio è chiamare un metodo.
- Definizione del metodo che viene eseguito nel thread secondario. Si noti il decorator [Background]
- Definizione del metodo che aggiorna la UI con il decorator [Dispatched]
Sono questi decorator che dicono a PostSharp di trattare in modo particolare questi metodi e aggiungere del codice per realizzare le funzioni richieste.
Addirittura qui il metodo che viene svolto nel thread secondario chiama direttamente la funzione che aggiorna la UI!
La funzionalità è la stessa, la thread-safety è garantita, ci sono 23 linee di codice in meno e la leggibilità è molto migliore!
Ma...
Ma e la performance?
Il weaving genera un overhead nel managed code e può portare ad un peggioramento della performance. Nella maggiorparte dei casi l'utente non si accorge di nulla. Questo tipo di preoccupazioni è tipica dell'elettronico, abituato a programmare per un microcontroller e a fare attenzione alla velocità di esecuzione e alla memoria. Se parliamo di applicazioni per computer, di velocità e di memoria non se ne parla, le si hanno (a parte casi molto particolari).
Ma e il control flow?
L'aspect weaver interviene in prodondità nella struttura del codice (è ovvio considerando l'esempio di cui sopra), ragion per cui il control flow del programma viene modificato. Va tenuto in mente se si presentano comportamenti inaspettati. Va da sè che non è un paradigma adatto se implementate sofware di controllo per centrali nucleari o per ambiente militare/aerospace.
Ma e il debugging?
Il debugging può diventare un inferno. Dipende tutto dal tool usato. Io conosco solo PostSharp in VisualStudio e non ho motivo di lamentarmi. Il debugger (di default, si può cambiare) ignora gli aspetti e visto che il weaving avviene a livello di Managed Language è trasparente al programmatore.
Ma e lo sfoggio di saccenza?
"Programmare deve essere complicato e il codice di difficile leggibilità, altrimenti non posso fare il sapientone". Ecco... questa è in effetti una critica che è difficile da smontare. Fortunamente, le persone che la pensano così sono spesso quelle che credono di saper programmare. E per gestire produttivamente un tool come questo bisogna saper programmare per davvero, non crederlo.
Ma e Java?
http://www.eclipse.org/aspectj/
In conclusione
L'AOP è un paradigma che ha ormai oltre 15 anni. Benché non sia molto conosciuto viene usato parecchio nell'industria. Ci sono estensioni AOP per tutti i maggiori linguaggi di programmazione.
Questo articoletto è da intendersi come aperitivo, se il concetto vi è piaciuto vi invito ad approfondire.