1. Introduzione

 

 

 

 

 

 

 

Uno dei principi fondamentali per una buona programmazione software è quello di codificare gli aspetti importanti riguardanti il sistema che si sta sviluppando in modo chiaro e ben localizzato. E’ un principio “generale” nel senso che può toccare ogni fase dello sviluppo.

Per un progettista è essenziale trattare i problemi di realizzazione di un sistema complesso in maniera il più possibile indipendente: decomporre ogni problema complesso in problemi più semplici ed aggregarne le soluzioni per ottenere la soluzione completa.

Questo approccio risulta essere uno strumento insostituibile anche per il programmatore il quale, attraverso le strutture del linguaggio scelto per implementare le soluzioni, si propone di realizzare l’effettiva separazione dei problemi.

 

L’Object Oriented Programming è oramai diventato il paradigma per eccellenza, avendo rimpiazzato quasi completamente quello procedurale. Uno dei più grandi vantaggi che ha introdotto è stato quello di permettere di vedere l’intero sistema come una collezione di classi, ognuna con un ben definito scopo ed una chiara responsabilità, la cui collaborazione permette di raggiungere l’obiettivo globale dell’applicazione. Strumenti come l’incapsulazione, l’ereditarietà e il polimorfismo, hanno contribuito ad un passaggio importante dall’approccio procedurale basato su procedure e funzioni, ad una migliore suddivisione basata sulle funzionalità.

Nonostante ciò, se da un lato l’object orientation promuove ed incoraggia il riuso del codice attraverso la modularità, l’esperienza sul campo ha mostrato che non sempre riesce a gestirlo così efficacemente come era stato originariamente pensato. Ci sono parti del sistema che non possono essere viste come di responsabilità di un singolo componente ma toccano o tagliano l’intera struttura o parte di essa. Ci sono cioè alcune funzionalità difficili se non impossibili da esprimere in modo ben localizzato usando i costrutti di modularizzazione tradizionali come le classi (di cui gli oggetti come istanze), i componenti, o le procedure. Gestione della memoria, meccanismi di sicurezza, condivisione di risorse, logging,  gestione degli errori e delle eccezioni, sono solo alcune delle caratteristiche che risentono di questo problema, ma sono anche funzionalità alla base dello sviluppo attuale del software la cui complessità cresce quanto le dimensioni.

 

La Separation of Concern, (separazione delle competenze nella nostra lingua anche se la traduzione letterale non permette di coglierne il pieno significato) rappresenta un importante principio oltre che la naturale astrazione di quanto detto. Decomporre un sistema in parti, ognuna delle quali si occupi ed incapsuli una particolare area di interesse, chiamata concern.

Il concetto di modularizzare il codice rappresenta proprio la volontà di mantenere ben distinte e separate le unità funzionali attribuendone precise competenze. Quando però una proprietà del sistema non riesce ad essere espressa attraverso un’effettiva separazione di competenza ma coinvolge più di un modulo funzionale, si genera un problema che chiameremo di sovrapposizione delle competenze.

 

L’Aspect Oriented Programming potrebbe essere il prossimo passo della costante evoluzione del paradigma Object Oriented, o forse potrebbe evolversi verso un nuovo paradigma completamente indipendente. Comunque sia, rappresenta un recente ed interessante filone di ricerca che cerca di identificare i costrutti linguistici appropriati per esprimere tali problematiche. L’AOP introduce un nuovo elemento di analisi e di programmazione chiamato aspetto. L’obiettivo è di fornire metodi e tecniche per decomporre i problemi in un certo numero di componenti funzionali e in un certo numero di aspetti che coinvolgono tali componenti, e quindi di comporre componenti e aspetti per ottenere l’implementazione del sistema.

 

 

L’impatto della modularità sulla qualità del software

 

L’obiettivo ultimo delle teorie e dei metodi che sottostanno a qualsiasi campo dell’ingegneria è quello di aiutare gli ingegneri nella produzione di prodotti di qualità. Normalmente questo è ottenuto fornendo una distinzione attenta tra prodotto e processo. Il prodotto è cosa è visibile ai clienti mentre il processo è il come questo obiettivo può essere realizzato. Il cosa e il come, tuttavia, sono i due lati della stessa medaglia. E’ con il processo, infatti, che gli ingegneri possono influenzare la qualità nei loro prodotti, ridurre il time to market, e controllare il costo di produzione. Queste considerazioni generali sono valide per ogni campo dell’ingegneria non meno in quella del software. La natura stessa del software, infatti, rende la qualità del prodotto difficile da ottenere e da valutare, a meno che non si adotti un processo adeguato.

 

L’ingegneria del software si è evoluta negli ultimi trent’anni proponendosi di identificare tali processi di produzione oltre che principi di sviluppo, tecniche e notazioni, con lo scopo ultimo di ottenere un prodotto di qualità, sviluppato nei tempi ed entro il budget previsti.

Se da un lato ha definito principi chiave come rigore e formalismo, separazione dei concetti, anticipazione dei cambiamenti, evolvibilità e scalabilità, il grosso sforzo è stato quello di capire come raggiungere questi obiettivi. Sono stati quindi definiti metodi, metodologie e strumenti per garantire la correttezza e l’efficacia del proprio procedere.

 

 

Figura 1 – Ingegneria del Software

 

A tutt’oggi rimangono valide le proprietà che McCall [McCall77], verso la fine degli anni 70, propose per delineare uno dei primi modelli di qualità standard, tant’è che nel 1991 ne derivò il modello ISO/IEC 9126 che venne riproposto per una seconda versione nel 2001.

 

Figura 2 - Modello di qualità di McCall

 

Il grafico mostra chiaramente la situazione, suddividendo i fattori di qualità rispetto a tre principali dimensioni : l’operatività, la revisione e la trasposizione del prodotto.

 

Caratteristiche essenziali dell’operatività del prodotto sono :

 

-         Correttezza: esprime in quale misura il programma rispetta i suoi requisiti.

-         Affidabilità : quanto bene le funzionalità offerte rispondono ai suoi requisiti.

-         Efficienza :  tempi di risposta, uso della memoria ...

-         Usabilità :  lo sforzo per imparare ad operare con il sistema.

-         Integrità : capacità di sopportare attacchi alla sicurezza del sistema.

 

La revisione del prodotto comporta :

 

-         Manutenibilità : sforzo richiesto per localizzare e risolvere errori in un programma in esercizio.

-         Flessibilità :  sforzo richiesto per modificare un programma in esercizio.

-         Verificabilità : sforzo richiesto per verificare il sistema, per assicurarsi che il sistema faccia effettivamente ciò per cui è stato costruito.

 

E per quanto riguarda la trasposizione del prodotto abbiamo :

 

-         Portabilità: sforzo richiesto per trasferire il software da una configurazione ad altre configurazioni.

-         Riusabilità: la misura della facilità con cui parti di software possono venire re-impiegate in altre applicazioni.

-         Interoperabilità: sforzo richiesto per far cooperare/dialogare il sistema con altri sistemi.

 

Potremmo identificare tutte queste come proprietà primarie al soddisfacimento della qualità. Se quelle relative all’operatività del prodotto (esterne) risultano essere quelle di cui ne trarrà giovamento l’utente e in base le quali valuterà la qualità del prodotto, quelle relative alla revisione e alla trasposizione (interne) assumono un ruolo fondamentale, in termini progettuali, economici e manageriali, per gli sviluppatori.

Nel 2001 si stima ancora che i costi di mantenimento sono superiori al 60% dell’intero costo di produzione di cui il 20% di correzioni, il 30% di adattamenti ed il 50% di perfezionamenti. Non per nulla la produzione di software è spesso considerata come somma tra sviluppo e mantenimento.

 

La progettazione assume quindi un ruolo di importanza strategica nello sviluppo di sistemi, fornendo da un lato una visione ad alto livello delle componenti statiche e dinamiche fondamentali, dall’altro rappresentando il tratto d'unione tra un vasto elenco di requisiti, frutto dell'analisi, ed un mare di particolari, di minuzie, frutto del design dettagliato.

Gli studiosi della progettazione del software oggi enfatizzano la nozione di architettura software, perché tale nozione permette di organizzare le conoscenze progettuali, comprendere le proprietà strutturali e favorire il riuso dei componenti.

 

Il motto fondamentale dei progettisti di software è :

 

progettare per il cambiamento [Parnas72]

 

alla luce delle considerazioni precedenti e del fatto che, in molti casi, vengono progettate famiglie di applicazioni, non applicazioni singole.

Idealmente il progettista sceglie un’architettura software, la modella, la valuta rispetto alla specifica dei requisiti e dopo alcune iterazioni produce una specifica dettagliata di progetto che i programmatori possono implementare.

 

Progettare per il cambiamento ha inoltre una duplice scopo. Da un lato questa prevenzione è finalizzata ai cambiamenti che avvengono durante la manutenzione, dopo che il prodotto è stato consegnato. Esempi possono essere i cambiamenti dei requisiti dell’utente, cambiamenti agli algoritmi (routine più efficienti), cambiamenti alla rappresentazione dei dati (si veda il problema dell’anno 2000), cambiamenti all’infrastruttura (nuovi sistemi operativi, protocolli di rete, DBMS), introduzione di nuove periferiche o modifiche del contesto sociale (cambiamenti del sistema di tassazione o del sistema monetario).

Dall’altro lato è necessario ricordare che la visione della costruzione del software paragonata ad altre discipline ingegneristiche da cui trarne idee ed approcci ai problemi, (in letteratura è spesso identificata come “impollinazione”) è un’idea sbagliata.

Ne è testimonianza l’abbandono, a livello di analisi, del modello a cascata (waterfall) per un modello più flessibile come quello a spirale fino ad arrivare ai cosiddetti metodi agili come l’Extreme Programming; nello sviluppo di software, a differenza della costruzione di un progetto architetturale come un ponte, non si sa esattamente, al momento della partenza, che cosa si vuole ottenere. Un progetto che specifichi completamente un sistema software è perciò un'utopia e di questa “instabilità” è necessario tenerne in considerazione.

 

Uno degli aspetti dei moderni cicli di produzione che più influenzano la strategia di progettazione è la necessità di costruire famiglie di applicazioni, ovvero diversi prodotti che vengono visti come una singola applicazione che viene riutilizzata, specializzata e modificata opportunamente in contesti diversi, dando luogo a versioni differenti. Si applica un ciclo di vita speciale, esteso, in cui la fase di progetto viene effettuata su un dominio e non per un’applicazione.

 

Le preoccupazioni principali durante la fase di progetto sono :

 

-         il disegno di un’architettura software adeguata al problema

-         la decomposizione dell’architettura in sottostrutture

-         il dettaglio di interfacce e implementazione dei moduli

-         il riuso di schemi di progetto, di codice, di applicazioni

 

Anche se viene stabilita l’architettura di base, rimane problematica la sua decomposizione.

In tutti questi anni gli sforzi si sono concentrati sulla cosiddetta decomposizione modulare da cui ne scaturisce il comune concetto di modularità.

Al fine di stabilire cosa si intende per architettura modulare e metodo di progetto modulare, è necessario introdurre dei criteri e dotarsi di opportuni principi:

 

-         Scomponibilità: un metodo di progetto deve favorire la decomposizione del problema in sottoproblemi risolubili separatamente.

-         Componibilità : l’architettura deve favorire il riuso di elementi software già esistenti e in grado di configurarsi in base alle funzionalità richieste.

-         Comprensibilità : le funzioni di un modulo devono essere comprensibili indipendentemente dal resto del sistema.

-         Continuità : il metodo deve minimizzare le variazioni necessarie per far evolvere il sistema e soddisfare eventuali nuove specifiche.

-         Protezione : ogni modulo deve limitare la propagazione di errori a run-time verso gli altri moduli.

 

Non è difficile vedere come la modularità, per sua natura, impatta profondamente sulle proprietà che McCall indicò per caratterizzare un software di qualità [McCall77] e come quindi possa essere considerata come proprietà architetturale principale.

Criteri come comprensibilità e scomponibilità favoriscono la manutenzione, la flessibilità e la verificabilità del sistema, come la componibilità e la continuità possono considerarsi alla base di un buon sistema riusabile e portabile.

 

Paradigmi funzionali e paradigmi object-oriented

 

Una volta stabiliti gli obiettivi da raggiungere è necessario sviluppare metodologie in grado di soddisfare i principi  e le  proprietà introdotte.

Fino all’inizio agli anni 80, la progettazione di un sistema software complesso era eseguita seguendo due principi base. Quello del “divide et impera”, in cui il sistema veniva decomposto in componenti funzionali più semplici ed indipendenti, e quello della “centralizzazione” dove lo stato globale veniva comunque gestito in un’area condivisa, anche in caso di programmazione non sequenziale.

 

Le strategie di progetto erano sostanzialmente orientate alla definizione dei processi di elaborazione (progettazione funzionale) e l’enfasi era posta sulle funzioni caratteristiche del sistema, viste come trasformatrici di dati in ingresso in dati in uscita.

 

Il paradigma era quello strutturato, guidato da diagrammi Entità-Relazione per modellare i dati del sistema e da diagrammi Data-Flow per modellare il comportamento o il funzionamento del sistema. La decomposizione modulare era vista come decomposizione funzionale. Al modulo corrispondeva una certa funzionalità.

Il maggior svantaggio fu la scarsa consistenza tra dati e comportamento all’interno dell’intero sistema, nonché una difficile corrispondenza di concetti tra il mondo reale e implementazione.

 

A partire degli anni 80, l’introduzione del concetto di abstact data type , in cui dati e comportamento vengono strettamente accoppiati, contribuì in modo significativo a gettare le basi per le strategie data-oriented, in cui l’enfasi viene messa sulla definizione delle strutture dati, sulle operazioni che le manipolano e sulle loro proprietà.

I principi divennero: “orientamento agli oggetti”, dove il sistema viene decomposto in moduli riusabili ed autonomi, e “decentralizzazione” attraverso cui lo stato globale viene distribuito anche nel caso sequenziale.

 

Il paradigma object oriented la fece da padrone e si impose come base per lo sviluppo di una varietà di linguaggi di programmazione, database systems e approcci di modellizzazione.

Il successo della modellizzazione object-oriented derivò sostanzialmente dal fatto che fu il primo vero traguardo al soddisfacimento delle proprietà primarie di McCall, oltre che un’ottima concretizzazione dei principi di modularità.

L’OO risultò quindi il paradigma più seguito, nonostante un’evoluzione ostacolata agli inizi degli anni 90 dal fatto che più di cinquanta approcci determinarono la cosiddetta “method war”, dove ognuno cercava di imporsi come il metodo corretto.

 

Alle tecniche strutturali, basate sull’idea di costruire un sistema concentrandosi sulle funzioni, le tecniche object-oriented proposero un rovesciamento di questo rapporto costruendo il sistema a partire dalla classificazione degli oggetti da manipolare.

L’idea quindi è di non chiedersi cosa fa il sistema ma a cosa serve, di quali oggetti è composto e come si opera su di essi. Questo obiettivo permette di modellare gli aspetti della realtà nel modo più diretto e astratto possibile, mediante le nozioni di oggetto, classi e relazioni.  Uno degli strumenti più interessanti  che vennero introdotti fu quello delle schede CRC (Class, Responsibility, and Collaboration) [Beck89] in risposta al bisogno di documentare le relazioni di collaborazione e responsabilità tra oggetti in fase di design.

 

Attraverso questi elementi e con l’introduzione di concetti come ereditarietà e polimorfismo è possibile esprimere in modo chiaro e naturale i principi di una buona architettura modulare. La scomponibilità e la componibilità trovano espressione dal fatto che il sistema viene costruito in termini di moduli, sulla base del principio che ogni classe è un modulo. La natura stessa della classe, attraverso costrutti di incapsulazione che possano delinearne parti private e pubbliche, fornisce uno strumento per riflettere la volontà di proteggere le parti delicate del software permettendo solo un accesso controllato e protetto a dati e funzioni. Legando gli aspetti comportamentali a quelli strutturali e modellando i concetti attraverso l’ereditarietà di ottiene un aumento della comprensibilità del sistema, la cui astrazione è garantita attraverso il polimorfismo che permette un riuso di concetti tra tipi diversi di oggetti.

 

Figura 3 - Modello funzionale  e Modello dell’oggetto

 

 

Oltre la decomposizione modulare

 

Come accennato in precedenza, l’attività fondamentale di un buon designer/architetto del software è la decomposizione del sistema al fine di soddisfare tutte le proprietà necessarie al raggiungimento di un software di qualità.

 

Il principio alla base delle scelte di decomposizione è quello della Separation of Concern (Separazione delle Competenze) [Dijkst76] che a tutt’oggi viene universalmente riconosciuto come uno dei principi fondamentali dell’ingegneria del software: decomporre un sistema in parti, ognuna delle quali si occupi ed incapsuli una particolare area di interesse, chiamata concern. Questo principio riconosce che non possiamo trattare più problemi alla volta ma, al contrario, dobbiamo trattare con ognuno di essi, separatamente. Afferma, tra l’altro, che gli aspetti importanti dovrebbero essere rappresentati nel programma intenzionalmente (in modo esplicito, dichiarativo, con poca o senza aggiunta di “rumore”) e dovrebbero essere ben localizzati. Questo facilita la comprensibilità, l’adattabilità, la riusabilità e molte altre buone qualità di un programma, perché intenzionalità e localizzazione ci permettono di verificare in modo più semplice come un programma implementa i nostri requisiti.

 

Alla luce di ciò, si può astrarre la problematica di decomposizione seguendo due principali approcci dove uno non esclude necessariamente l’altro:

 

 

Una rappresentazione della differenza tra aspetti e unità modulari è mostrata in Figura4. Il disegno alla sinistra mostra come le unità modulari vengano incapsulate  in modo chiaro e organizzate in gerarchie. La figura sulla destra mostra che gli aspetti tagliano (cross-cuts)  un certo numero di unità modulari.

 

Figura 4 - Decomposizione modulare e decomposizione ad aspetti.

 

La seguente Figura5 mostra un’altra rappresentazione di un aspetto: il Modello A è un aspetto del Modello B perché si riferisce a diverse locazioni di B. Per esempio, B potrebbe essere una classe che implementa un certo tipo di struttura di dati astratti e A potrebbe essere una specifica di sincronizzazione riferita ai metodi di tale struttura.

 

Figura 5 – Un altro esempio di aspetto

 

La decomposizione modulare ci permette di ottenere perfezionamenti in modo semplice aggiungendo strutture che non attraversano i contorni delle unità modulari già stabilite. Ogni volta che dobbiamo aggiungere una struttura che oltrepassa questi contorni, stiamo applicando una decomposizione ad aspetti.

La decomposizione ad aspetti e quella modulare possono essere combinate, poiché una rappresenta il complemento dell’altra. Entrambe corrispondono alla strategia umana e naturale di modellizzazione : dipaniamo i problemi investigandoli sotto diverse prospettive e li organizziamo gerarchicamente.

 

La decomposizione in aspetti rappresenta le fondamenta della Programmazione Orientata agli Aspetti che verrà presentata in seguito. Come già accennato, tale decomposizione è comunque presente, in modo più o meno evidente, nello sviluppo di software, ad esempio nei diversi aspetti usati nei metodi di analisi e di design, ma la ricerca nel campo dell’AOP fornisce, alla decomposizione ad aspetti, alcune nuove prospettive. L’AOP incoraggia l’introduzione di nuovi aspetti piuttosto che l’uso di un ridotto set di aspetti generici (come nel caso dei metodi esistenti di OOA/D); enfatizza la necessità di aspetti specializzati oltre che una loro combinazione per differenti categorie di domini. L’AOP dichiara inoltre aperta la sfida di introdurre la decomposizione ad aspetti non solo nei modelli di design ma a livello di implementazione del sistema. In particolare c’è la necessità di nuovi meccanismi di composizione e nuovi costrutti di linguaggio, tutto questo, ovviamente, allo scopo di ottenere miglioramenti di qualità come la riduzione di codice “aggrovigliato”, riduzione di ridondanza, migliore localizzazione del codice, migliore comprensione, manutenibilità, e riusabilità.

 

Verso la completa Separazione delle Competenze

 

La separazione delle competenze segue il fondamentale principio di nascondere la complessità attraverso l’astrazione. Astraendo le competenze e separandole, gestirne i relativi comportamenti diventa sostanzialmente meno complesso.

Sfortunatamente, gli aspetti rilevanti in un’applicazione sono solitamente “sovrapposti” e in mutua dipendenza tra loro; quindi, se tentiamo di rappresentare tutti questi aspetti esplicitamente e localmente, introdurremo inevitabilmente molta ridondanza. E’ un problema da non sottovalutare poiché dobbiamo assicurarci che tutte le rappresentazioni ridondanti rimangano consistenti. Il modello globale del sistema rischia di diventare molto complesso vista la necessità di controllare tutte le differenti rappresentazioni. D’altra parte, se decidiamo di mantenere una rappresentazione con bassa ridondanza, alcuni aspetti risulteranno ben localizzati ma altri non lo saranno. E’ un’idea simile alla rappresentazione di un segnale nel dominio del tempo e in quello delle frequenze. Nel dominio del tempo riusciamo a vedere l’ampiezza del segnale in ogni istante di tempo ma non riusciamo a vederne le componenti in frequenza; nel dominio delle frequenza, d’altra parte, riusciamo a vedere le componenti in frequenza ma non l’ampiezza dell’intero segnale in un dato istante. Se decidiamo di tenere entrambe le rappresentazioni ne vedremo entrambe le proprietà ma introdurremo ridondanza.

Idealmente vorremmo implementare e memorizzare la soluzione di un problema attraverso una rappresentazione efficiente, ossia una con minima ridondanza, ed avere qualche meccanismo di supporto che permetta di estrarre dal modello ogni prospettiva di cui abbiamo bisogno. Stesse informazioni sotto diversi aspetti.

 

Progettare un sistema basandosi su tale principio è un compito che a tutt’oggi è più arduo di quello che si possa pensare e, con le tecnologie attuali, risulta essere una soluzione impraticabile. E’ necessario quindi orientarci su soluzioni più pratiche: lo scopo delle odierne tecniche di modellizzazione è di sviluppare un modello che rispecchi i requisiti (funzionali e di qualità, come performance, throughput, availability, failure safety, ecc.) e, allo stesso tempo, bilanci i seguenti obiettivi :

 

-         avere gli aspetti di maggior importanza il più possibile localizzati.

-         diminuire la complessità di composizione di tali aspetti (ci si riferisce a questo problema come view rendering o complexity of compiler) poiché la decomposizione e la conseguente localizzazione di certi aspetti può considerevolmente aumentare la complessità delle trasformazioni necessarie alla loro ricomposizione.

-         minima ridondanza

-         abilità di soddisfare con lo stesso grado cambiamenti previsti e imprevisti.

 

Vedremo con maggior dettaglio ognuno di questi obiettivi quando discuteremo delle tecniche e dei meccanismi che sono stati proposti nel campo della programmazione orientata agli aspetti.

 

Prima di concludere è necessario ribadire la necessità di introdurre meccanismi per poter seguire l’approccio della separazione delle competenze in tutto lo sviluppo del sistema. A tale scopo è possibile distinguere due differenti livelli di separazione.

In un livello concettuale bisogna fornire una chiara definizione delle competenze,  e identificarle in modo tale che ognuna di esse possa essere distinta concettualmente dalle altre. E’ necessario inoltre  assicurarsi che i concetti siano individualmente “primitivi” nel senso che non risultino composizione di altri concetti.

In un livello d’implementazione, la separazione delle competenze deve provvedere a un’adeguata organizzazione che isoli tali competenze. L’obiettivo di questo livello è di separare i blocchi di codice che implementano le differenti competenze, provvedendo a rendere minimo l’accoppiamento tra di essi.

 

Figura 6 - Livello concettuale e livello d'implementazione

 

I concern identificati nel livello concettuale sono usualmente mappati nel livello d’implementazione attraverso i costrutti messi a disposizione dai linguaggi di programmazione. I concern non primari nel livello concettuale hanno però l’effetto indesiderato di essere rappresentati in costrutti monolitici (di linguaggio) che cercano di gestire i differenti concern allo stesso tempo.  Nonostante l’astrazione concettuale venga riconosciuta in molte metodologie di design, pochi linguaggi di programmazione ne permettono un’effettiva implementazione. L’organizzazione del codice risulta monolitica e intrecciata come rappresentato nei Linguaggi A o B in Figura6.

Solo attraverso un’effettiva applicazione della separazione delle competenze ad entrambi i livelli se ne possono ottenere i reali benefici.

 

 

 

Copyright (C)2002 Fabrizio Rovelli. La copia letterale e la distribuzione di questa pagina nella sua integrità sono permesse con qualsiasi mezzo, a condizione che questa nota sia riprodotta.