Cos'è ElectroYou | Login Iscriviti

ElectroYou - la comunità dei professionisti del mondo elettrico

8
voti

Programmazione orientata agli aspetti: un esempio in C#

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.

Funzioni di logging in Apache Tomcat (da aspectj.org)

Funzioni di logging in Apache Tomcat (da aspectj.org)

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.

form1.PNG

form1.PNG

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:

all-in-one-code.PNG

all-in-one-code.PNG

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:

threaded_classic.PNG

threaded_classic.PNG

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:

threaded_aop.PNG

threaded_aop.PNG

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.

10

Commenti e note

Inserisci un commento

di ,

Articolo molto interessante, ti invidio la capacità di spiegare così chiaramente.

Rispondi

di ,

volendo potete aprire un thread sul forum e io aggiungo il link a questo commento.

Rispondi

di ,

Ti ringrazio per la disponibilità, concordo che questo non sia il posto per uno scambio come questo. Siccome il problema è che IO non sto ancora vedendo il vantaggio concreto della questione e non sulla bontà del paradigma, resto in attesa e aspetterò di leggere con interesse eventuali ulteriori articoli di approfondimento futuri. Grazie e buone vacanze.

Rispondi

di ,

Rispondo brevemente (per due motivi: qui nei commenti è poco comodo e essendo ufficialmente in vacanza, con l'ambiente di sviluppo rimasto in ufficio, non posso fare chissà che esempio). Ma se vuoi approfondire ti invito ad aprire un thread nel forum. E a gennaio posso anche mettere altri esempi. - Le classi astratte non sono una soluzione. Perlomeno, non lo sono in C#, che non supporta la multiple inheritance. Quindi se ti serve per motivi funzionali di derivare una classe, non potrai derivare anche la classe per -per esempio- il logging. Si può risolvere con un'interfaccia, ma l'interfaccia non contiene implementazioni, quindi non aiuterebbe. Inoltre credo che il problema non sia come realizzare la funzione, ma come integrarla. Un esempio ultra-minimo di logging. Se tu vuoi monitorare una variabile importante, con PostSharp, alla sua dichiarazione usi il seguente costrutto: "[LogSetValue] static int Banana". Senza scrivere null'altro (!) ad ogni modifica della variabile Banana, indipendentemente da dove succede e perché, nel logging framework che si è scelto apparirà un'entry. Cosa c'è dentro si può scegliere nella configurazione dell'aspect. Si va da una semplice notifica al contenuto della variabile prima e dopo il cambiamento così come la funzione che ha eseguito l'accesso. Immaginati l'overhead nel farlo manualmente.

Rispondi

di ,

La mia esperienza si basa su architetture MVC, ma non Microsoft dove le cose sono un po' mescolate tra di loro. Dove i 3 layer sono ben separati per capirci dove riesci a riscriverne uno solo con tecnologie differenti senza grossi problemi. Ad es. per implementare una grafica differente. WEB piuttosto che APP, ecc. Faccio un po' di fatica a collocare l'AOP in questo contesto. Si collocherebbe a metà tra il Model e il Controller. Su Microsoft mi trovo a disagio perchè è sempre il View la parte centrale. Dovrebbe fattorizzare le cose in modo funzionale, ma non proprio. Citando la definizione da Wikipedia: "La programmazione orientata agli aspetti è un paradigma di programmazione basato sulla creazione di entità software - denominate aspetti - che sovrintendono alle interazioni fra oggetti finalizzate ad eseguire un compito comune. Il vantaggio rispetto alla tradizionale Programmazione orientata agli oggetti consiste nel non dover implementare separatamente in ciascuna classe il codice necessario ad eseguire questo compito comune." Spesso si può fare con l'ereditarietà e le classi astratte, faccio fatica a vedere come si concretizza l'AOP in uno strumento separato. L'esempio purtroppo non mi aiuta e nemmeno il preambolo. Parlare del logging del Tomcat mi mette ancora di più fuori strada. Questo si risolve con una libreria che si occupa di logging ma poi ogni classe si occuperà di loggare quello che serve nel suo contesto di applicazione, per le cose comuni ci saranno superclassi che fattorizzano cose consuete a più oggetti e via dicendo. Faccio fatica a collocare uno strumento ulteriore. Tornando all'esempio del logging, non mi riesco proprio ad immaginare come questo possa essere tenuto fuori dalla mia implementazione: "Usando un'estensione AOP per il compiler possiamo lasciare il codice riguardante i cross-cutting concerns al di fuori della nostra implementazione." Sono incuriosito ma allo stesso tempo spiazzato.

Rispondi

di ,

Addendum: restando in C#, le autoproperties che credo chiunque usi, sono se vogliamo una specie di codice che viene introdotto dal compiler e nessuno si preoccupa piú di tanto. Certo, si tratta di una cosa molto semplice, dove non c'è molto che possa andare storto. Ma anche in ambiti piú complessi, se l'aspect non viene scritto da Pinco Pallino, ma da un professionista qualificato (come lo sono, stando alla mia esperienza, gli sviluppatori di SharpCrafters), allora il caso è equivalente.

Rispondi

di ,

Le preoccupazioni sono legittime, ma non mi fascerei la testa prima di essermela rotta. Nella sezione in questione ho forse omesso di scrivere che, certo, si genera un overhead ma diminuisce la quantità di codice, quindi la somma dei due potrebbe essere grossomodo costante. Inoltre da parecchi anni vale la regola che il compiler ottimizza quasi sempre meglio della persona (con forse poche eccezioni di persone alle quali non sono degno di sciogliere i legacci dei sandali). Addirittura la riduzione del cosiddetto boilerplate-code (nonostanze il nick sono innocente ;-)) è uno dei pilastri del clean-code e del refactoring. Riguardo alla sparare con un cannone su un moscerino, hai ragione, senza dubbio. Ma è voluto: per dare un'introduzione semplice non posso importare un progetto super-complesso, altrimenti ci si perde. L'esempio è volutamente semplicissimo.

Rispondi

di ,

Ho letto un po' con superficialità quindi il mio commento è da prendere con le pinze, ma la mia prima impressione è che si usi un cannone per sparare a un moscerino. Inoltre mi ha colpito la sezione "... e la performance ..." Il contenuto è emblematico e risponde ad un quesito esistenziale: Perchè ogni nuova versione di Windows riesce sempre a vanificare tutti i miglioramenti di performance dei nuovi processori. Forse perchè ad ogni raddoppio delle prestazioni dell'hardware si aggiunge uno strato di inefficienza nel software? Forse un po' di applicazione di sana ingegneria del software risolve gli stessi problemi in modo più efficiente, a volte basta solo fermarsi un po' di più all'inizio per progettare meglio il software a livello teorico prima di iniziare a scriverlo. Sono pronto a rivedere la mia posizione applicando il contenuto dell'articolo su un esempio un po' più calzante o meglio su un esempio dove se ne può cogliere meglio il vantaggio. Seguirò con interesse eventuali sviluppi perchè mi piace apprendere cose nuove anche se al primo impatto mi lasciano un po' perplesso.

Rispondi

di ,

bella spiegazione, interessante, ma credo solo per chi è gia pratico di programmazione a basso livello, programmare partendo dai thread mi pare azzardato. saluti.

Rispondi

di ,

L’aperitivo era davvero gustoso. Non ne sapevo nulla. Grazie

Rispondi

Inserisci un commento

Per inserire commenti è necessario iscriversi ad ElectroYou. Se sei già iscritto, effettua il login.