MokaByte 73- Aprile 2003 
Corso di programmazione Java
XII parte: strutture dati orientate agli oggetti
di
Andrea Gini
Dopo aver imparato come si utilizzano gli oggetti in un programma, č finalmente giunto il momento di imparare a progettare gli oggetti partendo da zero. Questo passaggio č un po' pių complesso del precedente; d'altra parte anche nel mondo reale č pių facile utilizzare un oggetto piuttosto che costruirlo. Chiunque desideri imparare a programmare un computer deve prima o poi affrontare anche gli argomenti pių avanzati, al fine di raggiungere il maggior controllo possibile.

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.

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it