In questa sesta parte, è giunto il momento di parlare di architettura del sistema, cercando di capire come e perché sia necessario trattare il modello architetturale, ossia il modo in cui le varie parti del progetto possono essere messe insieme per realizzare l‘applicazione.
Introduzione
Dopo aver visto nelle puntate precedenti i temi legati al project management, alla progettazione delle entità basilari, alla definizione dello strato di persistenza, è ora di fare il punto della situazione per capire in che modo le varie parti del progetto possono essere messe insieme per realizzare l’applicazione. In pratica è giunto il momento di parlare di architettura del sistema, cercando di capire come e perche’ sia necessario trattare il modello architetturale.
Molte delle nozioni che andremo a presentare in questo articolo sono in parte già state pubblicate nei mesi passati (anche piuttosto indietro nel tempo) in serie dedicate ai pattern, o al design in genere. In questo articolo vedremo quindi di riprendere i concetti già noti, cercando di dare un senso compiuto nell’ottica del progetto che virtualmente stiamo andando a presentare.
L’importanza dell’architettura
In questa serie di articoli, fino a questo punto ci siamo preoccupati degli aspetti organizzativi (le prime puntate dedicate alla impostazione del progetto e al project management), di quelli legati alla progettazione dei vari componenti, e di quelli tecnologici in genere. Prima di procedere nello sviluppo di una applicazione di complessità medio-alta, è necessario dare spazio a un altro aspetto importante: quello architetturale, appunto.
In realtà la definizione della architettura e di tutti i dettagli connessi è sicuramente una fase che dovrebbe essere fatta prima o in parallelo al processo di analisi e pianificazione iniziale. In questo caso, per non appesantire troppo la fase di introduzione alla serie di articoli, abbiamo fatto finta di averla definita inizialmente prendendoci la libertà editoriale di vederla in un secondo momento.
Ora quel “secondo momento” è arrivato e vedremo pertanto la definizione dell’architettura: in pratica significa fare una serie di scelte (o derivare da quelle che sono le considerazioni iniziali fatte in fase di planning, ovvero durante la stesura del PID [PID]) che avranno ripercussioni non solo sugli aspetti tecnologici del sistema, ma anche su quelli quelli organizzativi legati al “come” sarà realizzato e al “cosa” comporrà il prodotto finale, al “come” si evolverà, al “come” le persone potranno essere impiegate per la sua realizzazione.
Spesso, “architettura” sembra significare esclusivamente la gestione degli aspetti tecnologici, la stratificazione dei vari layer, oppure semplicemente il modo in cui i vari componenti sono collegati fra loro per dar vita al tutto. Purtroppo sovente ci si dimentica che l’architettura di un’applicazione, in quanto modo con cui l’applicazione stessa è costruita, influenza il modo in cui deve essere organizzato il lavoro: l’architettura deve essere realizzata da persone; non tenere conto dei fattori umani può essere un grosso errore.
Questo è ancora più importante nel caso di applicazioni complesse: determinate scelte architetturali influenzano radicalmente la velocità e la qualità della costruzione di un’applicazione. Se lo sviluppo si protrae per lungo tempo, le condizioni valide alla partenza possono variare in corso d’opera e quindi il rispetto delle linee guida del piano iniziale può rivelarsi un problema con il passare del tempo: possono cambiare i requisiti iniziali o il panorama architetturale generale, possono apparire soluzioni standard come si può verificare che gli standard precedenti vengano superati.
Architettura e aspetti ambientali
La definizione dell’architettura a volte è frutto di considerazioni legate ai fattori ambientali: il gruppo di lavoro, le conoscenze tecnologiche disponibili, i prodotti già acquistati. Queste decisioni sono tra le più difficili da sostenere “a posteriori”: in genere non vi è tracciamento delle motivazioni di alcune scelte che possono essere il risultato di situazioni fortemente contingenti.
Poter difendere o giustificare tali ipotesi anche nel medio termine potrebbe essere un buon fattore di scelta: adottare una soluzione che si mostra vincente adesso, ma che non è supportata da solide e durature giustificazioni nel medio e lungo termine potrebbe essere una strada rischiosa per la carriera del progettista o per il successo del progetto nel suo complesso. Fra le varie azioni che possono essere particolarmente utili vi è una attenta e corretta valutazione del mercato (inteso come scenario ma anche come strategia evolutiva) soppesando attentamente le varie alternative.
Molto utile è cercare di darsi delle risposte a domande di questo tipo:
- Qual è il punto di partenza per la realizzazione delle applicazioni?
- Ci sono tecnologie specifiche cui conformarsi?
- Ci sono in azienda prodotti già acquistati?
- Ci sono in azienda esperienze e skills già maturati?
Per quanto concerne le conoscenze del gruppo di lavoro, appare ovvio che il know-how maturato è un fattore di accelerazione del processo di costruzione; è meno ovvio che basarsi troppo su quello che il gruppo di lavoro conosce può portare verso una applicazione che nasce già vecchia. È necessario tenere in considerazione non solo il panorama visibile partenza, ma anche quello (previsto) al punto di arrivo.
Open o proprietario?
Fra gli aspetti ambientali, una delle decisioni importanti è la scelta di adozione fra prodotti open source o proprietari. I fattori che guidano nella scelta di uno o dell’altro sono piuttosto evidenti, a partire dai costi diretti (acquisto delle licenze) e indiretti (assistenza, supporto, documentazione, problem solving).
Qui il discorso potrebbe diventare lungo: in alcuni contesti l’open source sembra essere una scelta ovvia, in altri è visto come il fumo negli occhi. Open Source significa prendersi in carico un costo di apprendimento sulla base di informazioni la cui qualità è fortemente variabile da progetto a progetto. Significa anche fare a meno di un supporto formativo “ufficiale”, anche se è comunque possibile trovare un supporto di qualche tipo.
Tuttavia, specialmente in certi contesti, dire open source significa anche non poter contare sull’apporto di un esperto esterno (o doverselo cercare); oppure, più prosaicamente, non poter scaricare la responsabilità sul fornitore, perche non c’è.
Per quanto riguarda gli scenari a lungo termine il discorso si complica, costringendoci a tenere conto anche di altri fattori quali la stabilità e la longevità delle scelte. Una soluzione gestita da un’azienda non sempre è il risultato di scelte fatte nell’ottica del cliente; oppure l’azienda può semplicemente fallire. Aziende grandi (IBM ad esempio) possono scegliere di non supportare più un determinato prodotto (come è avvenuto con OS/2) costringendo di fatto i clienti a una migrazione. Paradossalmente, in alcuni casi, soluzioni open source possono essere tenute in vita dagli utilizzatori stessi, se questa risulta l’alternativa più conveniente.
In generale, le scelte tecnologiche possono rivelarsi non più valide, con il passare del tempo. Il compito dell’architettura è anche quello di assicurarsi che il costo di rimozione di una particolare componente sia accettabile.
Architettura come modello documentale
In questo contesto ci basiamo sul semplice assunto che un’applicazione è realizzata da persone: persone che hanno esperienze, conoscenze, modi di vedere e di interpretare il modello applicativo differenti gli uni dagli altri. Quello che il committente vuole può essere interpretato in vari modi dagli analisti, progettato differentemente dai designer e infine implementato nei modi più disparati dai programmatori.
Il “rumore di fondo” può derivare quindi da visioni diverse dello stesso problema e conseguenti differenti interpretazioni della soluzione da adottare; il rumore pero’ può essere amplificato dalle sempre presenti difficoltà di comunicazione che si instaurano fra le varie persone del gruppo.
Da sempre le varie discipline dell’informatica hanno cercato di ridurre l’alea legata alla personalizzazione del processo di sviluppo, nel tentativo di far evolvere l’informatica da disciplina artigianale a processo industriale.
Dalla OOP alla programmazione componenti, dall’uso dei pattern alla MDA (Model Driven Architecture, [MDA]), un obiettivo ricorrente è stato quello di limitare l’uso dell’interpretazione personale a favore della creazione di standard, cercando di raggiungere un modo comune di vedere il mondo.
L’introduzione del concetto di architettura, come punto di riferimento per l’implementazione è un modo per imporre una serie di linee guida per il gruppo di lavoro: la definizione deve essere sufficientemente precisa per limitare l’intraprendenza personale, ma al contempo deve essere abbastanza lasca per consentire il refactoring, la modifica, le evoluzioni sulle versioni successive.
Tanto più l’architettura è precisa, stretta e deterministica (nelle varie parti ed evoluzioni che arriveranno, o nella possibilità di individuare i problemi) tanto più sarà difficile adattarla alle variazioni, al refactoring, alle modifiche evolutive. Al contempo una definizione troppo aleatoria e vaga finisce per rendere inutile il concetto di architettura.
L’architettura può disegnare il “grande piano” ma in definitiva sono le persone che lo realizzano: in buona sostanza anche se l’architettura non è documentazione, può essere usata come strumento per documentare e migliorare la comunicazione all’interno del gruppo.
Se è vero che il gruppo di lavoro non realizzerà mai l’applicazione così come è stata progettata dall’architetto, questo deve lavorare in modo da considerare il modello come l’arte della gestione delle imperfezioni: l’imperfezione è parte del gioco (anche se ovviamente non può minare il quadro complessivo).
Figura 1 – “La prima Matrix che disegnai era assolutamente perfetta, un’opera d’arte impeccabile, sublime; un trionfo eguagliato solo dal suo monumentale fallimento. L’inevitabilità del suo destino mi è ora evidente quale conseguenza dell’imperfezione intrinseca dell’essere umano. … il 99% dei soggetti testati accettò il sistema a condizione di avere una scelta, anche se la consapevolezza di tale scelta era a livello quasi inconscio. Benche’ la trovata funzionasse, era fondamentalmente difettosa dato che, di fatto, generava quella contraddittoria anomalia sistemica che, se non controllata, poteva minacciare il sistema stesso.”
Best architecture, antipattern architecture
L’architetto deve sempre tenere ben presente un altro aspetto quando realizza l’architettura del progetto: il livello di complessità tecnologica che si vuole adottare nel sistema.
Spesso si è portati a pensare che soluzioni semplici o rudimentali possano rivelarsi inadatte o insufficientei per rispondere a esigenze di un evoluto scenario enterprise: sovente la continua ricerca di soluzioni sofisticate e tecnicamente avanzate viene visto come un obiettivo onorevole e necessario.
Maggior complessità tecnologica o sofisticazione, se da un lato possono permettere di rispondere meglio alle complesse necessità di uno scenario enterprise moderno (performance, scalabilità, modularità, multicanalità, etc…), dall’altro portano ad avere prodotti che normalmente hanno un elevato costo di manutenzione e risultano difficili da far evolvere o “aggiustare”.
Inoltre, un modello sofisticato porta a un prodotto che può essere compreso solo da poche persone (in genere i più bravi ed esperti), se non adeguatamente documentato; ma scrivere una buona documentazione è un processo quanto mai costoso. Componenti non documentati diventano patrimonio esclusivo di una sola risorsa; se la risorsa “brava” deve anche spendere del tempo per preparare una documentazione più corposa del normale, probabilmente il costo finale non sarà così vantaggioso. Ci si dimentica quindi che anche le persone possono essere considerate risorse chiave nella definizione della architettura. Se avere personale molto esperto è indubbiamente un vantaggio, è vero che sviluppatori “troppo” esperti possono scrivere codice non manutenibile da altri.
L’architettura deve essere quindi il più possibile semplice, compatibilmente con i requisiti funzionali e non funzionali imposti dal progetto e dal PID di progetto, come ampiamente detto nel primo articolo della serie dedicato al project management. L’architettura deve rifarsi a modelli e casistiche frequentemente adottate in altri progetti e deve essere il più possibile in linea con le “mode” del momento (in modo che si possa trovare in rete molta documentazione), appoggiandosi a standard di dominio pubblico rispetto a soluzioni proprietarie.
L’uso dei pattern abbinato ai classici modelli architetturali è la strategia vincente che ogni progettista dovrebbe sempre adottare. Se si propone una modello a tre layer con disaccoppiamento basato sul binomio Facade-Delegate e in cui il presentation layer utilizza il pattern MVC, forse non avremo prodotto un modello che eccelle per originalità, ma probabilmente anche uno studente del primo anno di corso sarà in grado di comprenderlo (e magari di completarlo se siamo a corto di personale).
Preassemblaggio, templating, prototyping
Migliorare il flusso di informazioni all’interno del gruppo di lavoro in modo da ridurre al minimo il diffondersi di concetti errati o male interpretati è uno degli obiettivi primari di ogni capo progetto. Il guadagno si ottiene sia semplificando l’architettura che provvedendo a far crescere il gruppo delle persone ad esempio migliorandone la preparazione di base.
Ogni alternativa che si presenta nel corso dello sviluppo è una possibile fonte di errore: introdurre strumenti che “limitano” la libertà di scelta è sicuramente un buon modo per limitare l’entropia del sistema: strumenti come il wiki si stanno diffondendo all’interno dei progetti non solo come tool per la condivisione di informazioni, ma anche come repository di librerie di esempi (snippet di codice). Un prototipo ben realizzato e condiviso sotto forma di mini schemi, template, parti di architettura, porzioni di progetto “preassemblate” evita ogni dubbio su come debbano essere realizzate determinate parti.
Il modus operandi è in questo caso quello del copia e incolla: rendere pubbliche le parti di codice che verranno poi usate più e più volte, permette di limitare fortemente l'”estrosità” del singolo (facilitando il propagarsi di un unico modo di implementare le cose), portando a un’omogeneizzazione della applicazione in tutte le sue parti.
Purtroppo, però, questo approccio apre a possibili ripercussioni negative: un pezzo di codice con un errore (che è comunque facile da scoprire) o una superficiale interpretazione di un problema con conseguente implementazione troppo vaga (molto più difficile da individuare o anche comprendere), possono dar vita a errori concettuali o funzionali che si propagano a macchia d’olio per tutto il progetto. Sbagliare tutti allo stesso modo è comunque un risultato ragguardevole: a parte la facile ironia, un errore comune, pur grave, replicato in molti punti è più facile da individuare e rifattorizzare che non una una pletora di differenti cattive interpretazioni.
Il pattern: mattone fondante della architettura
La nozione di pattern proviene dall’architettura: “Un pattern rappresenta una soluzione testata e ottimizzata a un problema ricorrente in diversi contesti” (Cristopher Alexander)
Proseguendo la metafora edilizia, potremmo dire che costruire un palazzo con mattoni standard e con componenti strutturali ben noti aiuta il lavoro del carpentiere in fase di costruzione e dell’artigiano nelle successive rielaborazioni e ristrutturazioni. Per questo motivo, la realizzazione di una buona architettura non può prescindere dalla adozione di pattern come elementi fondanti.
I pattern sono in genere classificati in vario modo a seconda dello scopo che si prefiggono e la nozione stessa di pattern è trasversale rispetto alla dimensione del problema:
- GRASP (implementazione, singole classi)
- GoF (design e architettura, gruppi di classi)
- J2EE Patterns (specifici dell’architettura SUN)
Normalmente tutti hanno un set di fili conduttori comuni:
- High Coesion / Low Coupling (alta coesione con basso accoppiamento)
- Separazione tra interfaccia ed implementazione
- Incapsulamento
- KISS (Keep It Simple, Stupid)
La parte restante di questo articolo (e parte del prossimo della serie) riporterà una breve panoramica su alcuni dei pattern più utili durante la fase di definizione della architettura della applicazione; lo scopo non è tanto quello di offrire l’ennesimo compendio sui pattern, per i quali è possibile trovare moltissimo materiale in rete (e su MokaByte se ne è parlato a lungo e in maniera molto dettagliata), ma piuttosto cercare di rivederli considerandoli come i vari “materiali da costruzioni” della architettura finale.
Il pattern Session Facade
Il pattern viene usualmente presentato come il modo migliore per fornire un punto di accesso univoco e semplice a una struttura complessa. Tale struttura può essere a volte oggetto di un processo di continua rivisitazione, processo che finisce per diventare la principale preoccupazione del progettista: la architettura può evolversi nel tempo, e l’implementazione può non essere coerente. Il costo di apprendimento della struttura complessa da parte di un utilizzatore può essere considerevole.
Figura 2 – Compito del Session Facade è nascondere la complessità di quello che sta dietro il front-end del sistema. Il componente di facciata inoltra a tutti i destinatari della chiamata, senza che il chiamante debba conoscere i dettagli strutturali interni del sistema.
Si immagini ad esempio di avere un sistema come quello raffigurato nella figura 2, in cui la parte “core” sia organizzata in modo più o meno complesso: in assenza di un intermediario fra chiamante e componenti del sistema, si crea un accoppiamento stretto che impone ai client di conoscere struttura interna, interdipendenze e di mantenere un riferimento diretto con ognuno di tali componenti.
Questo design, porta a un sistema fortemente coeso, e di conseguenza con un bassissimo livello di disaccoppiamento: il costo di manutenzione in genere esplode esponenzialmente quando la complessità generale aumenta (anche non di poco). Per ogni variazione di uno dei componenti della architettura è necessario infatti modificare ogni cliente invocante.
Centralizzando il punto di accesso con un nodo centrale (il Facade appunto) si limita l’interdipendenza fra l’utilizzatore e il fornitore di una funzionalità/servizio. Chi accede ai servizi esposti ha conoscenza solo della classe Facade e delle classi usate come argomenti o tipi restituiti (pattern DTO che affronteremo in seguito) e delle eventuali eccezioni.
Figura 3 – In questo schema si evidenzia come il facade funga da unico punto di accesso al sistema, operando un inoltro delle chiamate ai componenti sottostanti. Deve essere proibito ogni accesso diretto da parte del client. A livello architettuale, il pattern facade è in genere situato sullo strato di frontiera fra la business logic e la parte di presentation.
Una volta definito il punto di accesso centralizzato al sottosistema si deve verificare/proibire che non vengano effettuati accessi che scavalchino la Facade, ad esempio lavorando sulla visibilità delle classi del sottosistema (p.e.: visibilità package) oppure usando opportuni tool di verifica di regole architetturali (p.e.: Macker, Checkstyle, etc.).
Una possibile implementazione degenere del Facade è il blob, ossia “la-classe-che-fa-tutto” che porta a codice non leggibile e scarsamente gestibile (accessi concorrenti, problemi di merge o di lock sul sistema di versioning).
Il Facade rappresenta un punto di demarcazione anche per le eccezioni che trasporta: può essere utilizzato per mascherare o gestire le eccezioni di tipo tecnologico tramite il meccanismo del rewrapping [REW] in funzione del layer logico. Per esempio, un errore a livello dati non ha alcuna necessità di essere propagato allo strato di presentation sotto forma di SQLException: in questo caso una eccezione applicativa DataAccessException potrà essere certamente più utile ed efficace.
Il Facade è quindi un ottimo candidato a implementare una “politica di gestione” per comportamenti comuni a tutta l’applicazione: si pensi al log dell’applicazione o alla possibilità di verificare le performance a run-time del sistema, alla possibilità di introdurre meccanismi di autenticazione e autorizzazione sugli accessi.
Il Facade ha benefici influssi anche per quello che concerne lo sviluppo della applicazione nel suo complesso, dato che consente di ridurre il set di conoscenze necessaria a sviluppare determinate funzionalità. Questo significa che è possibile avere una evoluzione asimmetrica delle varie parti del sistema: una volta definito il punto di accesso, posso procedere a sviluppare front layer e business layer in modo indipendente. Si ha quindi un innegabile beneficio in termini di disaccoppiamento dei sottosistemi. Per lo stesso motivo è possibile differire determinate scelte implementative a una fase successiva a quella di prima stesura, favorendo la modularità complessiva
Il disaccoppiamento delle operazioni di implementazione può essere estremizzato tanto da consentire di lavorare anche con versioni provvisorie ed incomplete: in questo caso il pattern è realmente efficace solo se è stata definita correttamente l’interfaccia in termini di semantica delle operazioni, delle classi usate come argomento e come risultato e se le eccezioni restituite sono ben progettate.
Il pattern Delegate
Il pattern Facade, come mostrato efficacemente nelle figure 2 e 3, nasconde il reticolo delle chiamate interne nel sistema. Rimane completamente da risolvere il problema relativo alla gestione della complessità infrastrutturale derivante dal processo di reperimento degli oggetti (remoti oppure no) che espongono la logica sotto forma di servizi. Il Facade in pratica non dice come il client possa reperire il Facade stesso.
Sia che si tratti di un oggetto remoto RMI, di un session bean EJB, oppure di un web service, la complessità infrastrutturale rimane a carico dell’invocante: nascondere tale complessità è tipicamente un compito che viene brillantemente risolto dal pattern Business Delegate, il quale si occupa di disaccoppiare l’accesso ai servizi di business dai client, nascondendo e centralizzando le operazioni di localizzazione e utilizzo del servizio di business: lo scopo di questo pattern è quindi quello di inoltrare richieste a oggetti (in genere risiedenti su layer differenti da quello dell’invocante) senza che vi sia conoscenza reciproca tra l’esecutore e il richiedente.
Figura 4 – il Business Delegate lavora in collaborazione con il session Facade. Il suo compito specifico è quello di mascherare la complesità legata alla connessione con gli oggetti dello strato di business logic. In genere si hanno grossi benefici se il layer di logica è basato su una tecnologia completamente differente come EJB o WS.
Questo pattern fornisce un’interfaccia uniforme ai client, nascondendo e centralizzando le problematiche di reperimento (lookup + create) e utilizzo dei componenti di business. Da un punto di vista logico, il pattern delegate ha un’importante conseguenza per quello che concerne la gestione delle eccezioni che potranno essere trasformate da tecnologiche (p.e.: java.rmi.RemoteException) in eccezioni applicative: il delegate infatti può essere visto come l’ultimo avamposto dello strato server, ed è qui quindi che verranno effettuate le eventuali riconversioni di errori e messaggi (piuttosto che all’interno del Session Facade, dove la scelta appare invece piuttosto forzata).
Da un punto di vista architetturale il business delegate si pone di fronte al Session Facade, in modo da nasconderne la complessità. A volte business delegate e Session Facade sono le due facce della stessa medaglia tanto che si possono accoppiare secondo uno schema uno-a-uno: i metodi del Facade sono replicati nel nome e nelle firme (con opportuna gestione delle eccezioni).
Figura 5 – Delegate e Facade possono lavorare in maniera molto coesa: in questo caso la corrispondenza è diretta 1-a-1: il business delegate funziona come proxy, mascherando i dettagli relativi alla comunicazione remota. Il client invoca uno dei metodi del business delegate con l’illusione che questo causi una invocazione locale.
Conclusione
Giunti circa a metà del lungo cammino verso lo sviluppo simulato di un software tipo, questo mese si sono introdotti i concetti base di architettura e della sua costruzione grazie all’utilizzo di alcuni importanti pattern: la trattazione non voleva essere esaustiva sugli scopi e sugli obiettivi dei vari pattern, dato che rimandiamo per questo alla ricca bibliografia. Continueremo il mese prossimo con altri utili pattern (il DTO, il Proxy/Adapter, il Locator) e inizieremo a mostrare degli esempi di implementazione basati sulla tecnologia EJB.
Riferimenti