Dr. Carlo Pescio
Architettura di Sistemi Record-Oriented, Parte 3

Pubblicato su Computer Programming No. 80

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.


Reader's Map
Molti visitatori che hanno letto
questo articolo hanno letto anche: