|
Dr. Carlo Pescio Architettura di Sistemi Record-Oriented, Parte 3 |
Completiamo la descrizione del mini-framework per la realizzazione ad oggetti di sistemi record-oriented, discutendo le problematiche di modifica e validazione dei record, nonché i campi calcolati e la sincronizzazione di viste multiple.
Nelle due precedenti puntate di questa mini-serie
abbiamo visto come le più comuni e diffuse implementazioni ad
oggetti per i sistemi record-oriented non offrano grandi vantaggi
di stabilità al variare delle più comuni condizioni al
contorno, tanto da far sostenere ad alcuni che gli oggetti "non
funzionino" nel settore business. Abbiamo anche iniziato ad
impostare una soluzione diversa, basata sul concetto di reflection
e studiata per essere resiliente di fronte agli eventi più
comuni (es. aggiunta di campi alle tabelle relazionali).
In questa puntata concluderò la discussione del mini-framework
sinora impostato, esaminando alcuni ingredienti essenziali per
l’editing e la validazione dei record. Dedicherò poi un
po’ di spazio ad alcune riflessioni sulla complessità
strutturale dei diagrammi contrapposta alla complessità
procedurale del codice, sul ruolo di piccoli framework
applicativi (come quello sinora discusso) all’interno dei
progetti medio-grandi, ed al ruolo che andrebbe invece ricoperto
dalle librerie orizzontali.
Modificare i record
A dire il vero modificare un record è molto
semplice: possiamo pensare che le classi record concrete offrano i
naturali metodi per modificare un campo, dato il nome del campo
stesso. Meno ovvio è come mantenere una coerenza tra tutte le
presentazioni di un record (o recordset) a fronte di una modifica,
ed ancora meno ovvio è come rendere un po’ meno rigida
la classica soluzione "a form", la cui implementazione va rivista
ogni volta che cambiano le regole di validazione, il numero di
campi, ecc.
Il primo problema (coerenza di viste) è talmente classico da
aver generato un pattern (observer, [GoF94]). In effetti, in molti
progetti reali ho visto soluzioni più o meno eleganti ed
efficienti, e quelle meno brillanti erano conseguenza di una
sovrapposizione "a posteriori" di uno strato di codice che gestisse
la coerenza. Quando un problema è così comune è
sempre meglio prevedere una soluzione, almeno nei suoi elementi
fondamentali, all’interno del framework.
Nel nostro caso, significa predisporre un meccanismo affinché
ogni RecordSetPresentation ed ogni RecordPresentation possa
ricevere (senza sforzi aggiuntivi) una notifica nel momento in cui
l’oggetto presentato (il recordset o il record) subisce delle
modifiche. Notate che ho escluso il singolo oggetto
FieldPresentation: questa è una decisione assolutamente
arbitraria, anche se ponderata e convalidata dall’esperienza.
In sostanza, nulla impedirebbe di trasformare ogni campo di una
form in un "osservatore intelligente" del Field sottostante, che
viene notificato quando il Field stesso cambia valore per una
qualsivoglia ragione. Sarebbe la soluzione più omogenea
possibile: se un oggetto RecordSetPresentation viene notificato a
fronte di modifiche al recordset, ed un RecordPresentation viene
notificato dopo eventuali modifiche ad un record, perché non
il FieldPresentation? Fondamentalmente perché dentro un record
vi sono relativamente pochi campi (paragonati ai record in un
recordset), e quindi l’oggetto RecordPresentation può
facilmente gestire la notifica di una modifica ad un campo
qualunque, senza complicare eccessivamente il diagramma delle
classi. Ovviamente la soluzione che vi presenterò non è
però l’unica possibile, e vi invito a pensare da soli
alle possibili alternative.
L’idea di fondo è molto semplice: aggiungiamo due classi
interfaccia, RecSetObserver e RecObserver. Ogni classe concreta di
presentazione di un recordset che si voglia abilitare
all’update automatico dovrà derivare da RecSetObserver;
analogamente, potremo derivare una presentazione concreta di un
record da RecObserver. Questa derivazione deve essere
opzionale: finiremmo altrimenti per forzare alcune classi non
interattive (come l’ExportedFile introdotto nella scorsa
puntata) a fornire una implementazione "fasulla" delle funzioni
dichiarate in RecSetObserver. Questo tipo di rigidità dà
sempre una pessima impressione agli utilizzatori di un
framework.
Le due interfacce di cui sopra contengono essenzialmente la
funzione UpdatePresentation(), che dovrà essere definita nelle
classi derivate. Sia la classe RecordSet che la classe Record vanno
quindi aggiornate, in modo da contenere una lista di RecSetObserver
(e di RecObserver rispettivamente), e dovremo prevedere anche dei
metodi per aggiungere un oggetto concreto di presentazione alla
lista degli observer.

Potete vedere questi elementi in figura 1, dove troverete anche i
nuovi elementi che discuteremo tra poco (ho riportato in un'unica
figura l’interno framework, per dare una vista di
insieme).
I vari recordset e record concreti avranno quindi la
responsabilità aggiuntiva di notificare, a fronte di una
modifica, tutti i loro observer circa i cambiamenti avvenuti.
Attenzione: questo è un punto più complesso di quanto
sembra, perché attraverso un recordset costruito come join di
diverse tabelle possiamo modificare i record appartenenti ad un
recordset diverso. Si possono ideare diverse strategie per gestire
il problema, ed ho evitato di riportare una soluzione privilegiata
nell’architettura di figura 1. In pratica, vista la
semplicità di SQL, se i nostri recordset sono tutti risultato
di query SQL possiamo facilmente creare una classe in grado di
analizzare, al limite in modo conservativo, gli eventuali overlap
tra recordset diversi.
Notate inoltre che rispetto al pattern observer non ho
introdotto una classe Subject (perché non ho alcun interesse a
trattare i diversi subject, ovvero RecordSet e Record, in modo
polimorfo), e che non ho nemmeno "fuso" RecSetObserver e
RecObserver in un’unica classe Observer, come alcuni
framework pretendono di fare.
Le ragioni sono decisamente pragmatiche: anche se questi dettagli
non sono mostrati nel diagramma di figura 1, in una implementazione
reale le funzioni UpdatePresentation delle due interfacce
prenderanno dei parametri, sicuramente di tipo diverso. Ad esempio,
la funzione UpdatePresentation di RecObserver dovrà dire quali
campi sono cambianti, l’analoga in RecSetObserver quali
record sono cambiati. Inoltre possiamo cancellare o inserire
record, ma solo cambiare il valore dei campi. Se non
vogliamo che gli osservatori concreti debbano ri-analizzare tutto
l’insieme di dati per capire le modifiche, dobbiamo passare
informazioni precise in UpdatePresentation, e queste cambiano da
una tipologia di soggetti osservati ad un’altra. Ricordiamo
sempre che i pattern vanno adattati ad ogni situazione, non
applicati come ricette di cucina; peraltro, al di là dei
pattern, le interfacce observer si possono pensare come semplici
elementi di disaccoppiamento introdotti tramite Asymmetric
Splitting [Pes99].
A questo punto, supportare un aggiornamento automatico della
presentazione diventa piuttosto semplice: basta derivare da (es.)
RecSetObserver, aggiungersi alla lista degli observer del
recordset, ed implementare la funzione UpdatePresentation. Ciò
che manca ancora è un modo semplice e flessibile per
modificare i singoli record, con tutta la logica di validazione che
questo comporta.
Form, calcoli e validazioni
Ricorderete che per introdurre i problemi di
presentazione dei campi ho fatto riferimento ad un semplice caso
reale (una griglia spreadsheet-like per visualizzare i record).
Analogamente, vorrei discutere i problemi di validazione e modifica
di un record usando un altrettanto realistico esempio basato su una
form.
Come vedete in figura 1, la nostra classe Form dovrà
innanzitutto derivare da RecordPresentation (in quanto fornisce una
vista di un singolo record). Se vogliamo mantenere una vista
coerente tra la form ed altre visualizzazioni (es. una griglia con
la vista di insieme del recordset) dovremo anche derivare la classe
Form da RecObserver ed implementare la funzione
UpdatePresentation.
Una form non è però una semplice presentazione dei dati
appartenenti ad un record. Tipicamente può mostrare anche
campi calcolati, non modificabili (un semplice esempio:
l’età di una persona, calcolata come differenza tra la
data attuale e la data di nascita presa dal record). Inoltre
possono esistere regole di validazione su ogni singolo campo (es.
codice fiscale, numero di carta di credito) o regole più
complesse che riguardano più campi. Come sempre, vorremmo
poter modificare queste particolarità senza intervenire in
modo intenso sul codice; idealmente, non vorremmo modificarlo
affatto.
Questo significa innanzitutto estrarre alcune parti dal codice e
muoverle in uno script esterno: in questo caso, in ciò che ho
definito RulesProfile. Il profilo delle regole dovrà dare, per
ogni campo visualizzato, il nome di una classe concreta di
validazione o di calcolo, seguito dai parametri richiesti per
l’istanziazione della classe concreta relativa. Sia i calcoli
che le validazioni potrebbero essere molto semplici
(l’età dell’esempio precedente) o anche molto
complicate: a questo punto dobbiamo necessariamente decidere sino a
dove spingerci con le possibilità di definire le regole nello
script.
Da un lato, potremmo pensare di aggiungere un linguaggio di
scripting molto potente in modo da definire (quasi) ogni regola in
script esterni; all’estremo opposto, ogni regola sarebbe
hard-coded nel programma.
Per esperienza, posso dire che un piccolo interprete, ben
progettato, può bastare per risolvere una buona quantità
di problemi reali. Nei casi rimanenti, è più semplice
risolvere il problema con un po’ di codice custom,
purché ciò significhi soltanto aggiungere una
nuova classe custom e non modificare il codice esistente.
Per andare oltre, è necessario prevedere non solo un
interprete più potente, ma anche esporre buona parte del
modello ad oggetti dell’applicazione allo script language, e
questo può non essere banale salvo casi (e linguaggi)
particolari.
Vediamo come questo si può tradurre in pratica: la form
possiederà uno o più oggetti di classe derivata da
CalcFieldRule (per i campi calcolati), o da ValidFieldRule (per la
validazione dei campi).
Tra le classi "di libreria" troveremo un SimpleInterpreter,
derivato da CalcFieldRule, che sarà in grado di supportare
alcune operazioni elementari (aritmetica, stringhe, date). Il
SimpleInterpreter può avere bisogno di accedere ad alcuni
campi della form, o di modificare il valore di alcuni di essi: per
fare questo passerà dall’interfaccia
CalculatedFieldConsumer, che ha il compito di disaccoppiare
l’interprete dalla form e di consentirne il riuso anche con
altre classi di presentazione dei record.

Non entrerò nei minimi dettagli realizzativi del
SimpleInterpreter; in molti casi potremo però utilizzare una
struttura ispirata al quasi omonimo pattern ("Interpreter",
[GoF94]), con qualche aggiunta che ho riportato in figura 2. Mi
sembra infatti opportuno aggiungere qualche linea guida per
l’implementazione delle cosiddette funzioni intrinseche,
perché ho riscontrato in molti progetti delle implementazioni
troppo cablate e poco estendibili.
Le funzioni intrinseche dell’interprete danno accesso alle
operazioni di base (come la somma) ma anche a funzionalità
accessorie molto utili, come la data corrente. È importante
che siano introdotte nel sistema in modo da poter facilmente
aggiungere nuove classi, registrarne il nome in una tabella, e
gestire nuove operazioni (es. la conversione ad euro!) senza dover
intervenire sul parser. In sostanza, il pattern Interpreter prevede
una classe per ogni simbolo terminale, che ridefinisce una funzione
di validazione; non dice però come costruire l’albero
sintattico a partire da una espressione in forma di stringa, come
avremmo nel nostro script. È ovviamente possibile costruire un
parser a discesa ricorsiva o usare uno strumento come yacc;
tuttavia la nostra implementazione deve prevedere un
meccanismo di estendibilità per l’insieme dei simboli
terminali. Molto spesso una implementazione table-driven del
riconoscitore di funzioni intrinseche è tutto ciò che
serve, soprattutto in casi come questo dove le espressioni saranno
semplici e le prestazioni non critiche.
Quando le regole di alcuni campi calcolati diventano troppo
difficili per il SimpleInterpreter, abbiamo due strade a
disposizione: potenziare l’interprete (e spesso non ne avremo
il tempo o l’interesse) o derivare una classe custom (come
BR1) da CalcFieldRule, che infatti è uno dei punti di
estendibilità del framework. Ciò che conta è che il
framework fornisca un supporto per i casi più semplici e
comuni attraverso l’interprete, e la possibilità
di scrivere codice custom quando se ne presenta la necessità,
anche solo in termini di tempo di consegna di un prototipo prima di
potenziare l’interprete. Credo sia abbastanza evidente come
alcuni semplici accorgimenti possano rendere lo sviluppo
decisamente più armonico, dando una base solida che prevede
sia le estensioni eleganti che le customizzazioni di emergenza,
isolando in modo netto le classi relative ed evitando che
l’intero progetto degeneri sotto i colpi di poche modifiche
affrettate.
Le regole di validazione (anziché di calcolo dei campi)
possono seguire uno schema in larga parte analogo, che in figura 1
ho semplificato introducendo semplicemente una classe interfaccia
ValidFieldRule ed un esempio di classe derivata ConcreteRule. Anzi,
in larga misura il codice usato per l’interprete dei campi
calcolati si potrà utilizzare anche per l’interprete
delle regole di validazione "semplici e comuni", con lo stesso
concetto di estensione e customizzazione discusso prima. In genere,
regole complesse basate su molti campi, da cui si costruiscono
talvolta query ulteriori in base alle quali definire la
validità dei campi, sono più facilmente implementabili
come classi concrete derivate da ValidFieldRule piuttosto che
attraverso un linguaggio di script che altrimenti va reso
decisamente potente.
Una form estendibile non si riduce a questo: occorre
inevitabilmente parlare anche dell’aspetto visuale. Ciò
richiederebbe un intero articolo, con l’analisi di diverse
soluzioni, a partire dalle più semplici (un dialog box in una
DLL di sole risorse) a quelle più complesse (un intero
linguaggio di script, magari basato su HTML, per descrivere
l’aspetto visuale delle form). Vi invito a pensare da soli
all’impatto che ogni scelta avrebbe sulla difficoltà di
realizzazione ma anche sulla flessibilità di
utilizzo.
Considerazioni
Chi ha avuto modo di sviluppare una applicazione
record-oriented con uno dei modelli "tradizionali" discussi nella
prima puntata noterà una differenza abbastanza marcata tra il
semplice schemino "classe base CRecordSet da cui derivo i recordset
concreti" ed il framework di figura 1.
Indubbiamente, lo schema che abbiamo costruito è decisamente
più complesso, come già accennavo nella scorsa puntata.
Lo sarebbe ancora di più se scendessimo nei dettagli di
progettazione del SimpleInterpreter o di altri elementi come
l’analizzatore delle query: in fondo, la figura 1 è un
diagramma a livello architetturale, non di design
dettagliato.
È giusto chiedersi quali siano i costi ed i vantaggi di una
simile complessità: non è meglio una soluzione
semplice rispetto ad una complessa? Indubbiamente
sì; diverso è il caso di una soluzione
semplicistica. Il problema di realizzare una applicazione
record-oriented facilmente manutenibile è
intrinsecamente complesso: noi possiamo nascondere parte di questa
complessità, o ridistribuirla tra vari elementi, ma non
possiamo farla sparire magicamente: se la togliamo dalla struttura,
riapparirà nel codice, come numero di righe e come
rigidità di tali righe. Se la togliamo in modo troppo deciso
dalla fase di sviluppo, apparirà in fase di
manutenzione.
In effetti, la figura 1 contiene un buon numero di classi (più
di trenta), connesse in modo controllato ma non banale. Comprendere
a fondo il funzionamento del framework richiede un po’ di
tempo e di riflessione. Ma il codice relativo, in una
implementazione ragionevole, è molto contenuto. Parliamo di
una o due decine di migliaia di righe, non di centinaia di
migliaia. Viceversa, le applicazioni che si sviluppano sopra al
framework sono molto flessibili, manutenibili ed estendibili con
facilità, spesso senza modificare il codice.
Da cosa deriva il numero basso di righe? Anche se può
sorprendere chi non ha troppa esperienza con gli oggetti, deriva
proprio dal numero non troppo basso di classi. Questa conclusione,
a cui si arriva quasi inevitabilmente dopo aver realizzato un
numero consistente di progetti secondo il paradigma ad oggetti,
è stata riassunta ottimamente da Booch nel suo testo "Object
Solutions": "se la vostra applicazione sta diventando troppo
complessa, aggiungete altre classi".
Può sembrare paradossale, ma non lo è: esistono molti
tipi di complessità, dalla complessità strutturale
(quante classi, quante relazioni), a quella procedurale (quante
righe di codice devo comprendere per capire una funzione/classe), a
quella temporale (quanto devo sapere del comportamento precedente
per capire quello presente e futuro), eccetera.
In moltissimi programmi mi è capitato di riscontrare un netto
sbilanciamento della complessità strutturale (troppo bassa)
rispetto a quella procedurale e temporale (troppo alte). Spesso
è una conseguenza di esperienze precedenti col paradigma
strutturato, che ancora influenzano il modo di pensare "ad
oggetti". Ancora più spesso lo sbilanciamento è dovuto
alla mancata adozione di una notazione grafica per il design: il
progettista pensa in termini di codice e non di diagrammi,
sfruttando solo in parte le proprie capacità cognitive. Quando
imparano ad usare nel modo giusto le rappresentazioni
grafiche (come UML) molti progettisti iniziano ad apprezzare la
capacità di ridistribuire la complessità tra molti
più elementi, incluso l’aspetto strutturale, e di
ottenere in cambio meno codice, classi più compatte, testabili
e riusabili, e così via. La notazione grafica aiuta moltissimo
a ragionare sulla struttura, purché non si facciano diagrammi
talmente dettagliati da essere semplicemente una forma diversa di
codice: troppi dettagli in un diagramma oscurano la percezione
dell’essenziale, ovvero della struttura fondamentale del
progetto.
Una ulteriore considerazione riguarda il processo attraverso il
quale siamo arrivati al diagramma di figura 1: cercando di
sfruttare le poche stabilità di un dominio intrinsecamente
instabile. Non basta mettere un’interfaccia intorno ad un
insieme di funzioni per realizzare una applicazione "ad oggetti".
Se le interfacce non portano alla stabilità,
all’estendibilità e ad un giusto partizionamento delle
responsabilità tra i diversi elementi, stiamo solo
implementando in un linguaggio ad oggetti una soluzione
procedurale.
Una ulteriore caratterizzazione del diagramma ottenuto è la
gestione delle dipendenze: mentre i dettagli interni di ogni classe
sono stati volutamente trascurati, le cooperazioni e le dipendenze
sono state gestite per ottenere estendibilità dove serve e
riusabilità dove necessario. Gran parte del Systematic Object
Oriented Design ruota intorno alla gestione delle dipendenze tra le
classi, la cui giusta soluzione consente anche di posizionare al
meglio le responsabilità all’interno di ogni singola
classe.
Concedetemi infine un’ultima riflessione sulla differenza tra
il framework che abbiamo visto e librerie più o meno
orizzontali come MFC. È evidente che MFC non può e non
deve fornire una soluzione a problemi applicativi come quelli
che abbiamo affrontato in queste puntate. È giusto che
fornisca solo delle classi recordset primitive e lasci il resto del
lavoro agli sviluppatori.
Ciò che è profondamente sbagliato è fornire dei
wizard che generino codice per recordset concreti in pochi istanti,
lasciando agli sviluppatori tutti gli oneri futuri. È
sbagliato promuovere un modello di sviluppo centrato sulla
produttività a brevissimo termine contro perdite notevoli a
medio-lungo termine. È sbagliato abbagliare i manager con
prospettive di consegna ultrarapida tramite copia&incolla e
wizard, magari anche di deskilling degli sviluppatori, e poi
lasciare in eredità solo un cumulo di codice da manutenere
faticosamente a mano non appena cambiano le regole del gioco.
La strada giusta, per ogni azienda che abbia un proprio
settore applicativo, passa sempre per la costruzione di uno o
più framework domain-specific, che si possono tranquillamente
appoggiare sulle funzionalità offerte dalle librerie
orizzontali, ma che devono essere progettati con i giusti
criteri di resilienza, testabilità, estendibilità,
eccetera. Framework di dimensioni contenute, che si possano capire
in giorni e non in mesi, che forniscano una struttura di fondo per
ogni applicazione, con tutti i vantaggi conseguenti in termini di
uniformità dei prodotti. Un buon framework richiede una
progettazione accurata, e quindi progettisti esperti; se bene
realizzato, tuttavia, è anche uno dei migliori investimenti
che una azienda software-intensive può fare.
Bibliografia
[GoF94] Gamma ed altri, "Design Patterns", Addison-Wesley,
1994.
[Pes99] Carlo Pescio, "Systematic Object Oriented Design", Computer
Programming No. 76.
Biografia
Carlo Pescio (pescio@eptacom.net) svolge attività di consulenza, progettazione e formazione in ambito internazionale. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. È autore di numerosi articoli su temi di ingegneria del software, programmazione C++ e tecnologia ad oggetti, apparsi sui principali periodici statunitensi, nonché dei testi "C++ Manuale di Stile" ed "UML Manuale di Stile". Laureato in Scienze dell'Informazione, è membro dell'ACM, dell'IEEE e dell'IEEE Technical Council on Software Engineering.