Nel
linguaggio Java, la metafora degli oggetti viene incorporata
in modo chiaro ed elegante, cosa che permette di trattare
l'argomento con relativa facilità. D'altra parte,
il rigore progettuale del linguaggio Java è tale
da non lasciar spazio alle improvvisazioni: chi desidera
raggiungere lo stato dell'arte nella programmazione
ad oggetti deve per forza di cose investire del tempo
nello studio del design Object Oriented, un'insieme
di pratiche che aiutano il programmatore a scomporre
un problema e ad individuare le architetture adatte
ad ogni sottoproblema. Non è questa la sede per
trattare in modo esaustivo un tema complesso come il
design Object Oriented: in questa prima fase ci concentreremo
sulla creazione di oggetti come strumenti per la rappresentazione
di dati.
I Dati
Negli articoli precedenti ci siamo occupati di direttive
che modellano il comportamento di un calcolatore. Poche
semplici strutture di controllo sono sufficienti a descrivere
complesse procedure, che una volta tradotte in algoritmi
possono essere automatizzate grazie ad un computer.
Ma per poter realizzare programmi di maggior complessità,
è necessario introdurre un aspetto fondamentale:
la rappresentazione e il trattamento delle informazioni
Negli
esempi presentati fino ad ora sono stati utilizzati
formati semplici per la rappresentazione dei dati: numeri
o stringhe. Ma nei problemi del mondo reale, ci troviamo
spesso a dover trattare informazioni composite, ovvero
insiemi eterogenei di dati correlati tra loro. Per creare
un archivio anagrafico, ad esempio, abbiamo bisogno
di un sistema che ci premetta di memorizzare nome, cognome,
sesso e data di nascita di ogni singolo iscritto. La
soluzione più semplice per questo problema è
quella di realizzare su carta un modulo di questo tipo:
Nome
____________________
Cognome ____________________
Sesso ____________________
Data di Nascita ____________________
Successivamente,
per ogni nuovo iscritto compiliamo una fotocopia del
modulo e lo inseriamo in uno schedario rispettando un
certo ordine prestabilito. In situazioni come questa,
l'unità base di informazione è la scheda,
non i singoli elementi che la compongono. Se vogliamo
creare un sistema per l'archiviazione e il trattamento
di informazioni di questo genere, non possiamo trascendere
dalla natura composita dell'informazione stessa: in
pratica abbiamo bisogno di un costrutto di programmazione
capace di rappresentare strutture di dati composite.
Progettazione di una Struttura Dati
Nel paragrafo precedente abbiamo analizzato il problema
di costruire una valido modello per la rappresentazione
di schede anagrafiche. Abbiamo individuato un insieme
di campi che caratterizzano una scheda: Nome, Cognome,
Sesso e Data di Nascita. Ogni elemento di questo insieme
ha un senso solo se associato agli altri: per questo
motivo è opportuno che i dati ad essa associati
viaggino sempre raggruppati.
Ma
c'è un altro problema da considerare: le informazioni
che possiamo scrivere in ogni campo della scheda devono
rispettare un particolare formato: Nome e Cognome vengono
rappresentati mediante stringhe, il Sesso viene solitamente
rappresentato con le lettere M o F; la data di nascita
è una stringa del tipo "10/12/1976",
o più in generale "gg/mm/aaaa", dove
gg mm e aaaa sono tre numeri, il primo dei quali compreso
tra 1 e 31, il secondo tra 1 e 12 il terzo tra 1900
e 9999.
In
conclusione, per descrivere la scheda anagrafica di
una persona dobbiamo stabilire un insieme di dati e
un insieme di regole per la formattazione di tali dati.
I dati e le regole devono viaggiare assieme: se per
qualunque ragione i dati dovessero essere modificati
in maniera non conforme alle regole, ci ritroveremmo
con una scheda anagrafica inconsistente, praticamente
priva di significato.
Implementazione mediante una Classe
La classe è un costrutto di programmazione che
permette di raggruppare in un'unica entità indivisibile
un insieme di variabili ed un gruppo di metodi che hanno
accesso esclusivo a tali variabili. Nei linguaggi Object
Oriented, la classe è la matrice sulla quale
vengono generati gli oggetti veri e propri.
Nel
precedente paragrafo abbiamo enumerato gli attributi
di una scheda anagrafica e le relative regole di formattazione:
possiamo ora studiare il codice che ci permette di rappresentare
una simile struttura dati. Vediamo anzitutto l'intestazione
e l'elenco delle variabili:
public
class SchedaAnagrafica {
private String nome;
private String cognome;
private char sesso;
private String dataDiNascita;
// qui vanno i metodi
}
La
classe SchedaAnagrafica appena definita contiene cinque
attributi. Si noti che essi vengono classificati come
"private", una clausola che li rende inaccessibili
all'esterno della classe stessa. Per permettere l'accesso
agli attributi, dobbiamo predisporre degli gli opportuni
metodi getter e setter. La creazione dei getter è
piuttosto banale:
public
String getNome() {
return nome;
}
public String getCognome() {
return cognome;
}
public char getSesso() {
return sesso;
}
public String getDataDiNascita() {
return dataDiNascita;
}
Questi
metodi si limitano a restituire i valori contenuti nelle
corrispondenti variabili. Ogni dichiarazione di metodo
è preceduta dalla clausola public, che rende
accessibile il metodo anche al di fuori della classe.
La realizzazione dei metodi setter presenta qualche
problema in più: come già visto nel paragrafo
precedente, alcuni attributi possono assumere solamente
i valori che rispettano un certo formato. Per impostare
gli attributi "nome" e "cognome"
sono sufficienti metodi da una sola riga:
public
void setNome(String n) {
nome = n;
}
public void setCognome(String c) {
cognome = c;
}
Per
gestire la modifica dell'attributo "sesso"
dobbiamo imporre delle regole più restrittive.
La specifica prevede che esso possa assumere solamente
i valori M o F, pertanto sarà necessario includere
una condizione di sbarramento:
public
void setSesso(char s) {
if(
s != 'M' && s != 'F' )
System.out.println("Il
sesso può essere solo M o F ");
else
sesso
= s;
}
La
data di nascita ha delle restrizioni ancor più
complesse:
public
void setDataDiNascita(int giorno, int mese, int anno)
{
if(
giorno < 1 || giorno > 31 )
System.out.println("Il
valore del giorno è errato");
else
if( mese < 1 || mese > 12 )
System.out.println("Il
valore del mese è errato");
else
{
dataDiNascita
= giorno + "/" + mese + "/" + anno;
}
}
Per
completare la classe, dobbiamo definire il costruttore,
uno speciale metodo che ha lo stesso nome della classe
e non ha valore di ritorno, che ha il compito di inizializzare
tutti gli attributi in un colpo solo:
public
Persona(String n , String c , char s ,
int g , int m , int a ){
setNome(nome);
setCognome(cognome);
setSesso(s);
setDataDiNascita(g
, m , a);
}
Ecco
dunque il codice completo della nostra prima classe:
public
class SchedaAnagrafica {
private String nome;
private String cognome;
private char sesso;
private String dataDiNascita;
public SchedaAnagrafica(String n , String
c ,
char
s , int g , int m , int a ) {
setNome(n);
setCognome(c);
setSesso(s);
setDataDiNascita(g , m , a);
}
public String getNome() {
return nome;
}
public String getCognome() {
return cognome;
}
public char getSesso() {
return sesso;
}
public String getDataDiNascita() {
return dataDiNascita;
}
public void setNome(String n) {
nome = n;
}
public void setCognome(String c) {
cognome = c;
}
public void setSesso(char s) {
if( s != 'M' && s !=
'F' )
System.out.println("Il
sesso può essere solo M o F ");
else
sesso = s;
}
public void setDataDiNascita(int giorno,
int mese, int anno) {
if( giorno < 1 || giorno
> 31 )
System.out.println("Il
valore del giorno è errato");
else if( mese < 1 || mese
> 12 )
System.out.println("Il
valore del mese è errato");
else {
dataDiNascita =
giorno + "/" + mese + "/" + anno;
}
}
}
NOTA:
Le condizioni di sbarramento presenti nell'esempio sono
piuttosto deboli: esse non prevedono una vera e propria
procedura per la gestione dei casi di errato assegnamento.
Prossimamente studieremo le eccezioni, un costrutto
che permette di gestire in modo adeguato questo problema
Salvataggio e compilazione della classe
Per poter utilizzare la classe appena creata, è
necessario salvare il sorgente in un file di nome "SchedaAnagrafica.java",
da compilare con il comando:
javac
SchedaAnagrafica.java
La
classe appena creata non è eseguibile, dal momento
che non contiene il metodo main. Come vedremo nel prossimo
paragrafo, essa può essere utilizzata come all'interno
di altre classi, a patto che vengano create e compilate
all'interno della stessa directory.
Utilizzo di una classe
La classe appena creata può essere utilizzata,
come qualunque altro oggetto Java, all'interno di altre
classi. Per effettuare un semplice test, è sufficiente
creare, nella stessa directory di "SchedaAnagrafica.java"
una nuova classe dotata di metodo main:
public
class ProvaSchedaAnagrafica {
public static void main(String argv[]) {
SchedaAnagrafica p;
p = new SchedaAnagrafica("Marshal
Bruce" ,
"Mathers III" ,
'M'
, 17 , 10 , 1972 );
System.out.println("Nome:
" + p.getNome());
System.out.println("Cognome:
" + p.getCognome());
System.out.println("Sesso:
" + p.getSesso());
System.out.println("Data
di Nascita: " + p.getDataDiNascita());
}
}
Il
programma di esempio non è particolarmente complesso.
Ovviamente è possibile creare programmi di gran
lunga più complessi, tipo un programma grafico
per l'inserimento e l'archiviazione di dati, o uno che
effettui dei calcoli statistici su un elenco di schede
anagrafiche.
Raffinamento della Struttura Dati
La struttura dati appena creata può essere raffinata
ulteriormente. L'attributo "data" presenta
un grado di dettaglio tale da suggerire la creazione
di una ulteriore struttura dati che incorpori tutta
la logica necessaria. Proviamo allora a vedere come
si possa formulare una classe Data:
public
class Data {
private int giorno;
private int mese;
private int anno;
public Data( int g , int m , int a ) {
if( giorno < 1 || giorno
> 31 )
System.out.println("Il
valore del giorno è errato");
else if( mese < 1 || mese
> 12 )
System.out.println("Il
valore del mese è errato");
else {
mese = m;
giorno = g;
anno = a;
}
}
public int getGiorno() {
return giorno;
}
public int getMese() {
return mese;
}
public int getAnno() {
return anno;
}
public String getRappresentazioneData()
{
return giorno + "/"
+ mese + "/" + anno;
}
}
Grazie
a Data, possiamo modificare il sorgente della classe
SchedaAnagrafica in questo modo:
private
Data dataDiNascita;
public Data getDataDiNascita() {
return dataDiNascita;
}
public void setDataDiNascita(Data d) {
dataDiNascita = d;
}
Il
sorgente della classe Data presenta delle particolarità
interessanti. Anzitutto si nota l'assenza di metodi
setter: in questo modo imponiamo che il valore degli
attributi possa essere stabilito solamente in fase di
creazione, e che non possa essere modificato successivamente.
Questa scelta progettuale è garantisce un maggior
controllo durante il trattamento dei dati: ogni oggetto
Data manterrà inalterato il proprio contenuto
per tutto il ciclo di vita dell'oggetto stesso. Se di
desidera rappresentare una data differente, è
necessario creare esplicitamente un nuovo oggetto Data.
La
seconda particolarità di questa classe è
che invece di memorizzare la data sotto forma di stringa,
la memorizza in una tripla di variabili intere: giorno,
mese ed anno. La rappresentazione in formato Stringa
viene creata dal metodo getRappresentazioneData.
Si
noti che la classe Data è per sua natura completamente
svincolata dal problema della rappresentazione di schede
anagrafiche, e che pertanto ha un contesto di utilizzo
molto più generale. In un autentico programma
di gestione dati anagrafici, la classe Data potrebbe
essere riutilizzata in decine di contesti differenti.
Il
raffinamento di una struttura dati può proseguire
ulteriormente: si può pensare di creare una classe
per rappresentare l'attributo Sesso, o si può
addirittura raffinare Data introducendo nuove classi
per trattare la rappresentazione di Giorno, Mese ed
Anno. A quale livello di dettaglio è opportuno
fermarsi? La risposta dipende esclusivamente dal contesto
applicativo: da una parte, i tipi primitivi sono estremamente
pratici ed efficienti, dall'altra'altra parte, l'uso
di classi ultra specializzate fornisce un grado maggiore
di sicurezza. L'importanza della precisione in determinati
contesti applicativi può essere chiarita da una
storia realmente accaduta.
Il
23 Settembre '99, dopo un viaggio di più di 10
mesi, la sonda spaziale Mars Climate Orbiter accese
i motori per inserirsi nell'orbita del pianeta Marte.
Il Mars Climate Orbiter Spacecraft Team in Colorado,
responsabile della comunicazione con la sonda, seguiva
da terra la traiettoria, e verificava la corrispondenza
tra i dati rilevati e quelli previsti dalle simulazioni.
La spinta dei motori ebbe inizio, come pianificato,
cinque minuti prima che l'astronave fosse oscurata dal
pianeta; ma nel momento in cui ci si sarebbe aspettati
che la sonda uscisse dall'orbita del pianeta, i controllori
di volo denunciarono l'assenza di segnale.
Da
una revisione delle ultime 8 ore di dati prodotti dai
sensori presenti nella sonda, venne fuori che essa si
trovava a circa 60 chilometri di altitudine, contro
i 150 previsti dal piano di volo. Per un oggetto in
orbita attorno al pianeta Marte, la minima altezza di
sopravvivenza è 85 chilometri: al di sotto di
questa distanza, la forza centripeta generata dalla
rotazione orbitale non è in grado di compensare
la forza di attrazione gravitazionale esercitata dal
pianeta. Poche ore dopo, la sonda entrò a contatto
con la rarefatta atmosfera marziana, e andò incontro
alla propria fine prematura.
Due
mesi dopo il fatto, un comunicato stampa della NASA
rivelò la natura del problema. La rotta della
sonda veniva seguito da due team a terra: il Mars Climate
Orbiter spacecraft team in Colorado, responsabile della
comunicazione con la sonda, e il team di navigazione
in California. I due team erano in costante contatto
tra loro, ed operavano sugli stessi identici dati numerici:
purtroppo, senza che nessuno ci avesse fatto caso, i
navigatori a terra assumevano che la spinta fosse espressa
in Newton e le distanze in metri, mentre il team del
Colorado interpretava gli stessi dati come se fossero
libbre e piedi. Una libbra è uguale a circa 4.5
newton, e sebbene ogni spinta sia piccola di per se,
nell'arco di nove mesi si accumularono abbastanza errori
da spingere la sonda a più di 100 chilometri
dentro l'atmosfera marziana.
La
morale è che a volte i dettagli possono creare
un'enorme differenza: un semplice problema di rappresentazione
di dati generò un malinteso nella conversione
tra sistema metrico decimale e sistema anglosassone,
che ebbe come risultato un danno da 125 milioni di dollari.
(fonte
http://mars.jpl.nasa.gov/msp98/orbiter/)
Conclusioni
Questo mese abbiamo imparato come costruire classi adatte
alla rappresentazione di dati composti, una situazione
che si presenta abbastanza di frequente in contesti
applicativi reali. Il mese prossimo studieremo in maniera
formale ed approfondita tutti i dettagli relativi alla
definizione di classi, in modo da fornire gli strumenti
per utilizzare il costrutto in tutti i contesti in cui
viene normalmente utilizzato.
|