|
Dr. Carlo Pescio Design: Gestione dei Dati |
Pubblicato su Computer Programming No. 42
Introduzione
Nella puntata precedente abbiamo visto come affrontare il dominio
del problema (e della soluzione) dal punto di vista del design:
in effetti, abbiamo posto al centro del discorso gli algoritmi
per la risoluzione del problema, le strutture dati necessarie
al loro corretto funzionamento, ma abbiamo lasciato in disparte
due aspetti fondamentali: i dati e le relazioni.
In questa puntata ci occuperemo delle modifiche che la fase di
design introduce a livello dei dati, parleremo di normalizzazione
ed ottimizzazione delle relazioni, della persistenza dei dati
e delle diverse soluzioni per ottenerla, dai database object oriented
a tecniche più tradizionali di serializzazione dei dati.
Relazioni ed Oggetti
Come era stato anticipato nella terza puntata della serie, oltre
alle relazioni tutto/parti e generalizzazione/specializzazione,
che trovano un corrispondente immediato a livello di design e
di implementazione (ovvero rispettivamente contenimento ed ereditarietà),
abbiamo anche relazioni di associazione generiche, come "una
persona abita in una città". Questo tipo di
relazioni può essere modellato direttamente nei linguaggi
per database object oriented (che vedremo in seguito in questa
puntata); tuttavia, ciò non è normalmente possibile
nei linguaggi object oriented "tradizionali", come il
C++ o Smalltalk. Poiché il fine del design è di
ottenere una descrizione della soluzione che possa essere implementata
senza ulteriori modifiche strutturali, è necessario avere
ben chiaro quali siano le possibilità del linguaggio target;
ciò significa che, in molti casi, ci troveremo a dover
riconsiderare le relazioni generiche in termini delle strutture
comunemente disponibili nei linguaggi di programmazione.
Esistono diversi metodi per modellare una relazione generica all'interno
dei normali costrutti di un linguaggio, alcune delle quali sono
applicabili solo in casi particolari: modellare le relazioni come
attributi (eventualmente con puntatori), come classi, oppure utilizzare
il metodo relazionale all'interno del framework object oriented.
Vedremo di seguito le diverse soluzioni, analizzandone i diversi
aspetti positivi e negativi.
Relazioni come attributi diretti
Riprendendo l'esempio precedente, "una persona abita
in una città", abbiamo una cardinalità (1,1)
tra la classe persona e la classe città,
ed una cardinalità (0,n) tra la classe città
e la classe persona: ciò riflette l'idea che una
persona abita in una sola città (almeno dal punto di vista
del domicilio legale) e che in una città abita un numero
imprecisato di persone.
Il lato interessante è quello che ha cardinalità
(1,1): in effetti, ciò significa che è teoricamente
possibile "immergere" la relazione all'interno dell'oggetto
persona: semplicemente aggiungendo un membro di tipo città
all'oggetto persona, avremo modellato la relazione rimanendo
all'inteno dei costrutti elementari dei linguaggi. Esistono purtroppo
diversi problemi associati a questo modello di rappresentazione,
alcuni di tipo concettuale, altri di ordine pratico:
Relazioni come attributi indiretti
Riconsiderando il punto precedente, possiamo vedere che alcune
delle anomalie sorgono proprio dall'aver rinunciato alla possibilità
di sharing degli oggetti: due persone che abitano nella
stessa città conterranno due istanze distinte di
città aventi gli stessi valori (il concetto di object
identity contrapposto all'uguaglianza tra oggetti verrà
ripreso più avanti). Se fosse possibile condividere un
solo oggetto, elimineremmo immediatamente il problema 4 (anomalie
di modifica), e come vedremo altre verrebbero eliminate come effetto
collaterale. In ogni linguaggio O.O. è in effetti possibile
condividere un oggetto con altri oggetti tramite i reference
o i puntatori: in questo caso, potremmo inserire un reference
(o un puntatore) a città all'interno della classe
persona, e fare in modo che se due persone abitano nella
stessa città, lo stesso oggetto venga referenziato all'interno
di entrambe. L'uso degli attributi indiretti (così chiamati
in quanto introducono un livello di indirezione) comporta anche
le seguenti conseguenze:
Relazioni come classi
Proprio per eliminare alla radice le problematiche di riuso, possiamo
pensare di modellare la relazione in modo totalmente esterno alle
classi coinvolte nella stessa: in tal modo, le classi non avranno
conoscenza del contesto in cui le impiegheremo, e pertanto potranno
essere riutilizzate più facilmente in contesti differenti.
Modellare una relazione come classe è molto semplice: la
classe "relazione" sarà costituita da un insieme
di coppie (o tuple nel caso di relazioni non binarie) di puntatori
ai singoli elementi; ad esempio, la relazione "una persona
abita in una città" verrà rappresentata
attraverso una coppia (puntatore/reference a persona, puntatore/reference
a città). Così come la soluzione basata su
attributi indiretti, l'uso dei reference permette lo sharing degli
oggetti ed elimina le anomalie relative; la memorizzazione dei
puntatori esternamente alle classi permette invece una più
semplice manutenzione ed un migliore riutilizzo. Nuovamente,
abbiamo un overhead in termini di spazio (un ulteriore puntatore
rispetto al caso degli attributi indiretti) ed anche di tempo
rispetto alla soluzione precedente. Come sempre, la scelta del
design più adeguato va eseguita sui singoli casi, e non
esiste la soluzione perfetta per ogni esigenza; talvolta, le prestazioni
sono più importanti della riusabilità; spesso,
è vero il viceversa. Modellare le relazioni come classi
è di norma la soluzione più elegante, mantenibile
e flessibile, con maggiori opportunità di riutilizzo per
le classi coinvolte nella relazione.
Usare il metodo relazionale
Un approccio molto diverso consiste invece nella fusione del metodo
relazionale [4] con l'approccio object oriented. Nel modello relazionale
un'entità, così come una relazione, è rappresentata
attraverso una tupla di attributi elementari (per evitare le anomalie
di cui sopra); ogni tupla ha una sua chiave, formata dalla
concatenazione di un certo numero di campi: la chiave determina
in modo univoco il record. Una relazione è quindi rappresentata
concatenando le chiavi degli elementi che partecipano alla relazione
stessa.
Vi è una profonda differenza tra il modello relazionale
e quello object oriented, determinato proprio dal concetto di
chiave e di esistenza degli oggetti; nel modello relazionale,
un elemento esiste come tupla di valori: se due elementi hanno
gli stessi valori in ogni campo, sono lo stesso oggetto. Nel modello
object oriented, un oggetto esiste in sé, indipendentemente
dal valore che assume (che di norma è incapsulato); pertanto
due oggetti possono essere uguali come valore ma diversi come
identità. Se cerchiamo di modellare gli oggetti basandoci
sui concetti relazionali, dobbiamo gestire questa differenza manualmente,
in quanto non avremo alcun supporto dal formalismo O.O e dal linguaggio
di programmazione; per una analisi più approfondita delle
differenze tra i due modelli, potete consultare [5].
Come si riflette, a livello pratico, la scelta di usare il metodo
relazionale? Per ogni classe nel dominio del problema, dovremo
definire una classe ausiliaria nel dominio della soluzione, atta
a contenere la chiave della classe originale. Ad esempio, per
la classe città del dominio del problema potremmo
utilizzare il nome o il CAP come chiave della stessa. La classe
originale verrà poi modellata nel dominio della soluzione
inserendo un oggetto della relativa classe chiave come attributo,
in sostituzione degli attributi che abbiamo inserito nella classe
chiave stessa. Notiamo che in alcuni casi sarà necessario
introdurre una chiave "artificiale", ad esempio un contatore;
osserviamo inoltre che, essendo le chiavi per definizione tutte
distinte, non si pone il problema dello sharing di oggetti pur
utilizzando un attributo diretto.
Dovremo poi definire un metodo class-level (ad esempio come metodo
statico in C++) che data una chiave ritrovi l'elemento (se esiste)
corrispondente a tale chiave; questo metodo può facilmente
essere implementato per funzionare sia in memoria centrale che
di massa, semplificando così la gestione della persistenza.
Le relazioni non saranno a questo punto diverse da ogni altra
classe, e verranno rappresentate tramite aggregazione di oggetti
chiave delle classi che partecipano alla relazione stessa. È
importante notare che all'interno del modello relazionale esiste
un'ampia teoria della normalizzazione dei dati, di cui la prima
forma normale citata sopra costituisce una parte molto ridotta;
alcuni concetti, come quello di dipendenza funzionale,
hanno in effetti rilevanza anche ai fini dei modelli object oriented,
mentre altri (es. forma normale di Boyce-Codd) sono più
strettamente legati al modello relazionale. Si tratta di argomenti
molto importanti per chiunque si accinga a progettare una base
dati, ma troppo ampi per essere adeguatamente trattati in questa
sede: il lettore interessato può consultare un qualunque
testo moderno sui database relazionali, ad esempio [6]
I lati negativi dell'approccio sono il notevole overhead di codice,
dovuto sia alle classi chiave, che alla necessità di alcune
classi "adattatore" verso l'ambiente relazionale (oltre
naturalmente all'engine relazionale stesso); per contro, come
vedremo, la gestione della persistenza dei dati è spesso
semplificata.
Persistenza dei dati
Le considerazioni e le scelte su esposte si applicano al momento
della definizione degli attributi e delle classi, all'interno
della fase di design; esse influenzeranno direttamente sia l'esistenza
di alcune classi che il layout interno di altre. Rimane comunque
in sospeso un problema molto sentito, ovvero come memorizzare
gli oggetti su disco e come recuperarli: questo problema, che
viene in genere detto di "persistenza dei dati", è
di soluzione tutt'altro che immediata, e può essere affrontato
a due differenti livelli:
Serializzazione
Rimanendo all'interno dei linguaggi tradizionali, un metodo molto
semplice per memorizzare un oggetto consiste nel "serializzarlo",
ovvero emettere in un formato memorizzabile su file la sequenza
(serie) dei suoi campi. L'operazione è invertibile per
recuperare gli oggetti. Questa soluzione lascia scoperti numerosi
problemi, che possono essere affrontati in modi diversi:
Uso di un Database Relazionale
Come visto in precedenza, uno dei metodi per rappresentare gli
oggetti è quello di far riferimento al modello relazionale;
in questo caso, è molto vantaggioso sfruttare una delle
tante librerie esistenti per la gestione relazionale dei dati.
Di norma, ciò richiederà comunque la creazione di
alcune classi "adattatore" che isolino la visione relazionale
(verso il database) e quella orientata agli oggetti (verso i clienti
della classe). Può comunque essere una scelta vantaggiosa
in numerosi casi, ad esempio quando esistano esigenze di condivisione
con una base dati esistente o di interoperabilità con altri
prodotti basati sul modello relazionale.
Object Oriented Databases
Pur con i limiti evidenziati sopra, talvolta la scelta migliore
consiste nell'utilizzo di un completo database obiect oriented:
ciò è vero specialmente nei casi in cui un numero
molto elevato di oggetti, con interconnessioni non banali, abbia
requisiti di persistenza, flessibilità e di mantenibilità.
Va detto che si tratta spesso di prodotti dal costo piuttosto
elevato e talvolta con limitazioni piuttosto pesanti; proprio
per evitare l'errore di considerare "object oriented"
un database tradizionale con alcune estensioni orientate agli
oggetti, magari sulla base delle affermazioni del produttore,
ho riportato in una side-bar il decalogo dei database orientati
agli oggetti, come riferimento per la valutazione di un prodotto
prima dell'eventuale acquisto.
Una possibile alternativa è l'uso di un prodotto CORBA-compliant
[8]: in questo caso, l'object-request broker è in grado
di gestire la persistenza e la distribuzione degli oggetti. Attualmente,
il problema maggiore associato a tali sistemi è quello
delle prestazioni: se tuttavia state progettando un sistema che
dovrà evolversi nei prossimi anni, e che abbia requisiti
di interoperabilità con altri prodotti CORBA, potrebbe
essere molto interessante considerare l'alternativa CORBA tra
le possibili soluzioni.
Conclusioni
Ancora una volta, il passaggio al paradigma object oriented non
significa il rifiuto dei concetti già consolidati in altri
settori dell'informatica: in particolare, il trattamento dei dati
e delle relazioni ha molto in comune, ma anche grandi punti di
distacco, con il modello relazionale. Non è raro che esperti
di diverso calibro affermino che il paradigma object oriented
rappresenta una totale rottura con il passato, e che conoscenze
precedenti possono essere controproducenti nella transizione ai
nuovi schemi di pensiero: mentre ciò può essere
vero per alcune tecniche di programmazione a basso livello, spesso
le conoscenze acquisite in campi più astratti, come la
teoria relazionale, non solo non sono di impaccio, ma costituiscono
un utile strumento per la comprensione e la progettazione dei
sistemi.
Bibliografia
[1] W. Kent: "Data and Reality: Basic Assumptions in Data
Processing Reconsidered", North-Holland, 1978.
[2] Christian Tanzer: "Remarks on object oriented modeling
of associations", Journal of Object-Oriented Programming,
Febbraio 1995.
[3] Carlo Pescio: "C++ Manuale di Stile", Edizioni Infomedia,
1995.
[4] E. F. Codd: "A Relational Model of Data for Large Shared
Data Banks", Communication of ACM, Giugno 1970.
[5] Tsichritzis, Nierstrasz: "Fitting Round Objects into
Square Databases", Technical Report, Centre Universitaire
d'Informatique, Geneva, Switzerland, 1988.
[6] Stefano Ceri: "Progettazione di basi di dati", Cooperativa
Libraria Universitaria del Politecnico.
[7] Al Stevens: "C++ Database Development", MIS Press,
1993.
[8] Object Management Group: The Common Object Service Specification,
Luglio 1994.
Biografia
Carlo Pescio (pescio@acm.org) svolge attività di consulenza in ambito internazionale nel campo
delle tecnologie Object Oriented. Ha svolto la funzione di Software Architect in grandi progetti per
importanti aziende europee e statunitensi. È incaricato della valutazione dei progetti dal
Direttorato Generale della Comunità Europea come Esperto nei settori di Telematica e Biomedicina.
È laureato in Scienze dell'Informazione ed è membro dell'ACM, dell'IEEE e della New York Academy
of Sciences.