Tipi di dato astratti: valori, entità e schemi realizzativi
Progettazione del Software
Formulario
Valore
funzionale, con condivisione
equals, hashCode, no clone
Collezione con side effect
senza condivisione
anche clone
Entità
side effect, senza condivisione
niente da ridefinire
Interferenza
condivisione + side effect
Uguaglianza fra insiemi
A=B⟺A⊆B∧B⊆A
Astrazione per valore e per entità
Un'astrazione può essere per valore o per entità, e la scelta decide come si scrive la classe Java.
Per valore. L'informazione si tratta come in matematica: un valore non cambia. Assegnare 1 e poi 2 a una variabile non trasforma 1 in 2, ma dà alla variabile un altro valore. Sono valori il numero complesso, detto semplice, e l'insieme, detto collezione perché aggrega valori più elementari. Aggiungere un elemento a un insieme dà un altro insieme.
Per entità. L'informazione rappresenta un individuo del mondo reale, come una persona, un conto corrente o un'automobile. L'individuo cambia stato nel tempo e resta se stesso: chi cambia residenza è la stessa persona.
Il tipo di dato astratto
Un tipo di dato astratto (TDA) ha dei domini, gli insiemi di valori su cui lavora, e delle funzioni. Ogni funzione ha una segnatura, una precondizione (pre) che deve valere prima della chiamata e una postcondizione (post) che vale dopo. result indica il risultato. NumeroComplesso ha i domini C, dei complessi, e R, dei reali: creaComplesso(R r, R i) ↦ C dà il numero con parte reale r e parte immaginaria i. L'insieme ha i domini Ins, di interesse, e T, degli elementi. Due delle sue funzioni:
inserisci(Ins i, T e) ↦ Ins: pre nessuna; postresult è i con e aggiunto, oppure i se e c'è già.
scegli(Ins i) ↦ T: prei non è vuoto; postresult è un elemento qualsiasi di i.
La specifica non dice come sono rappresentati i dati.
Dal tipo astratto alla classe
In Java un TDA diventa una classe, che raccoglie dati e funzioni nello stesso costrutto.
Il dominio si rappresenta con attributi private, per l'occultamento dell'informazione.
Le funzioni di creazione diventano costruttori, le altre metodi public.
Uno schema realizzativo è un insieme di regole di progetto che portano a una classe coerente con la definizione matematica. Il numero complesso è un valore semplice, fatto di due double che nessun metodo modifica. Il costruttore realizza creaComplesso:
public class NumeroComplesso { // rappresentazione degli oggetti private double re; // parte reale private double im; // parte immaginaria // realizzazione delle funzioni del tipo astratto public NumeroComplesso(double r, double i) { re = r; im = i; } public double reale() { return re; } public double immaginaria() { return im; } public double modulo() { return Math.sqrt(re * re + im * im); }}
Side effect e condivisione della memoria
Per ogni TDA si fanno due scelte indipendenti.
Side effect o funzionale. Con side effect un metodo modifica l'oggetto su cui è invocato, come in C un parametro passato per puntatore. Un metodo funzionale non modifica nulla e restituisce un nuovo valore: è il caso naturale dei valori, perché 1 non diventa 2.
Condivisione della memoria. Senza condivisione le rappresentazioni di oggetti diversi non hanno memoria in comune. Con condivisione possono riferire le stesse strutture, per esempio gli stessi nodi di una lista, e si risparmia spazio nelle collezioni grandi. La condivisione espone al rischio di interferenza solo insieme ai side effect: una realizzazione funzionale condivide senza modificare nulla.
Con condivisione
Senza condivisione
Con side effect
no: interferenza
sì
Funzionale
sì
sì, ma può sprecare memoria
In Java si privilegiano side effect senza condivisione e funzionale con condivisione. Sono regole di massima, non automatismi.
Interferenza e sharing
L'interferenza è l'effetto per cui una modifica fatta attraverso una struttura altera anche un'altra. Richiede condivisione e side effect insieme: se due liste condividono un nodo e un metodo con side effect lo toglie da una delle due, anche l'altra cambia.
ListaStringheF è una lista di stringhe funzionale con condivisione: inserire in testa dà una lista che condivide con quella di partenza tutti i nodi dopo quello inserito. Basta un metodo con side effect per avere interferenza:
public class ListaStringheF { ... // ERRORE GRAVISSIMO !!! public void setFirstInfo(String x) { nodoinit.info = x; }}
ListaStringheF la = new ListaStringheF().add(0, "A").add(1, "B") .add(2, "C");// la = ( A B C )ListaStringheF lb = la.add(0, "Z");// lb = ( Z A B C )la.setFirstInfo("X"); // ha effetto collaterale su lb// la = ( X B C )// lb = ( Z X B C )
Due liste che condividono un nodo
nodoinitnextnodoinit
Il primo nodo di la è anche il secondo di lb: setFirstInfo lo modifica e cambia tutte e due le liste.
Condividere i nodi di una struttura è quindi una scelta da controllare. Condividere un'entità tra più strutture è invece voluto, ed è uno dei fondamenti dell'orientamento agli oggetti: lo sharing. Due liste di persone possono contenere la stessa persona, nella figura Paperino, perché una persona è un'entità e ne esiste una sola: se una lista ne cambia la residenza, l'altra la vede cambiata.
Due liste che condividono un'entità
nodoinitinfonodoinitinfo
Le due liste riferiscono la stessa persona. Togliere un nodo da una lista non tocca la persona né l'altra lista.
Lo schema decide cosa ridefinire
Lo schema realizzativo decide quali metodi ereditati da Object si ridefiniscono:
valore semplice o collezione, funzionale con condivisione:equals e hashCode; non clone, perché si favorisce la condivisione;
collezione con side effect, senza condivisione: anche clone, per evitare la condivisione;
entità: nessuno, perché un'entità è uguale solo a se stessa e una sua copia sarebbe un'altra entità.
Un'entità: la classe Studente
Lo studente è un'entità, con domini Studente, Esame e PianoDiStudi. Lo schema è side effect senza condivisione.
Gli esami stanno in un array di EsameVoto, classe ausiliaria non pubblica che associa un esame a un voto. numEsami conta le celle usate; la capacità di 30 è un'ipotesi d'esempio.
// classe ausiliaria per rappresentare gli esami con votoclass EsameVoto { private Esame es; private int vo; public EsameVoto(Esame e, int v) { es = e; vo = v; } public Esame esame() { return es; } public int voto() { return vo; }}public class Studente { // rappresentazione dell'oggetto private String nome; private String facolta; private PianoDiStudi piano_studi; private EsameVoto[] esamiFatti; private int numEsami; // realizzazione delle funzioni del tipo astratto public Studente(String n, String f, PianoDiStudi p) { // realizza la funzione creaStudente nome = n; facolta = f; piano_studi = p; esamiFatti = new EsameVoto[30]; // al più 30 esami numEsami = 0; } public String nome() { return nome; } public String facolta() { return facolta; } public PianoDiStudi piano() { return piano_studi; } public void assegnaFacolta(String f) { facolta = f; } public void inserisciEsame(Esame e, int v) { esamiFatti[numEsami] = new EsameVoto(e, v); numEsami++; } public double media() { double somma = 0; for (int i = 0; i < numEsami; i++) somma = somma + esamiFatti[i].voto(); return (double) somma / numEsami; } public int votoEsame(Esame e) { for (int i = 0; i < numEsami; i++) if (esamiFatti[i].esame().equals(e)) return esamiFatti[i].voto(); return 0; // lo studente non ha sostenuto l'esame e } // equals(Object) e hashCode() ereditati da Object sono corretti}
La classe Studente e le sue ausiliarie
piano_studi0..*1esamiFatti10..30es0..*1
Lo studente contiene i propri esami con voto e riferisce un piano di studi, che può essere condiviso con altri studenti.
assegnaFacolta e inserisciEsame non restituiscono nulla: modificano l'oggetto di invocazione, anche se la specifica dà assegnaFacolta come funzione che restituisce uno studente. L'array degli esami fa parte dell'oggetto e non è condiviso. Il piano di studi è invece in sharing: lo studente ne conserva il riferimento. Esame e PianoDiStudi sono classi elementari, con una stringa ciascuna, e non ridefiniscono equals:
public class Esame { private String nomeEsame; public Esame(String id) { nomeEsame = id; } public String toString() { return nomeEsame; }}
PianoDiStudi p = new PianoDiStudi("A27512");Esame fond1 = new Esame("Fondamenti 1");Esame fond2 = new Esame("Fondamenti 2");Esame fisica1 = new Esame("Fisica 1");Studente s = new Studente("Paolo", "ingegneria", p);s.inserisciEsame(fond1, 20);System.out.println(s.media()); // 20.0s.inserisciEsame(fond2, 28);System.out.println(s.media()); // 24.0
fisica1 non è collegato a nessuno studente: finito il blocco non è più riferito, e il garbage collector può raccoglierlo.
Il cliente chiama equals
votoEsame confronta gli esami con equals e non con ==. Chi scrive il cliente non sa come è fatta Esame: con == supporrebbe che non ridefinisca equals, e violerebbe l'occultamento dell'informazione. Se Esame cambiasse, il cliente non funzionerebbe più.
Chi scrive una classe risponde del significato del suo equals, anche se sceglie quello ereditato. Chi la usa risponde di chiamarlo. È il design by contract: se entrambi rispettano il contratto la modularità si conserva, e con essa la manutenibilità.
Un valore collezione: InsiemeFC
InsiemeFC realizza l'insieme in modo funzionale con condivisione. Il dominio T diventa Object, quindi gli elementi possono essere di qualunque classe. La lista collegata di NodoLista parte da inizio, che è null per l'insieme vuoto.
class NodoLista { Object info; NodoLista next;}public class InsiemeFC { // rappresentazione dell'oggetto private NodoLista inizio; // realizzazione delle funzioni del tipo astratto public InsiemeFC() { // realizza la funzione insiemeVuoto inizio = null; } public boolean estVuoto() { return inizio == null; } public boolean membro(Object e) { return appartiene(e, inizio); } public InsiemeFC inserisci(Object e) { if (appartiene(e, inizio)) return this; else { InsiemeFC ins = new InsiemeFC(); ins.inizio = new NodoLista(); ins.inizio.info = e; ins.inizio.next = inizio; // condivisione di memoria return ins; } } public InsiemeFC elimina(Object e) { if (!appartiene(e, inizio)) return this; else { InsiemeFC ins = new InsiemeFC(); ins.inizio = cancellaFC(e, inizio); // copia parziale return ins; } } public Object scegli() { if (inizio == null) return null; else return inizio.info; } // uguaglianza public boolean equals(Object o) { if (o == null || !(getClass().equals(o.getClass()))) return false; else { InsiemeFC ins = (InsiemeFC) o; NodoLista l = inizio; // ogni elemento di this sta in ins while (l != null) { if (!appartiene(l.info, ins.inizio)) return false; l = l.next; } l = ins.inizio; // ogni elemento di ins sta in this while (l != null) { if (!appartiene(l.info, inizio)) return false; l = l.next; } return true; } } public int hashCode() { return 1; // banale ma coerente con equals } // metodi ausiliari private static boolean appartiene(Object e, NodoLista l) { return (l != null) && (l.info.equals(e) || appartiene(e, l.next)); } private static NodoLista cancellaFC(Object e, NodoLista l) { if (l == null) return null; else if (l.info.equals(e)) return l.next; else { NodoLista ll = new NodoLista(); ll.info = l.info; ll.next = cancellaFC(e, l.next); return ll; } }}
appartiene è ricorsivo e private static: static perché lavora su nodi e non su un InsiemeFC, private perché nessun cliente deve chiamarlo.
scegli restituisce il primo elemento: la specifica non dice quale scegliere.
inserisci e elimina restituiscono this se non c'è nulla da cambiare, altrimenti un nuovo InsiemeFC.
inserisci mette un nodo in testa e condivide per intero la lista esistente, come add(0, "Z") nella prima figura. Qui la condivisione è sicura, perché nessun metodo di InsiemeFC modifica i nodi.
elimina non può condividere tutto: cancellare un nodo modificando i collegamenti altererebbe anche l'insieme di partenza. cancellaFC copia allora i nodi che precedono l'elemento e condivide quelli che lo seguono.
InsiemeFC t = new InsiemeFC().inserisci(3).inserisci(7).inserisci(8);// t = ( 8 7 3 )InsiemeFC r = t.elimina(7);// r = ( 8 3 ), t resta ( 8 7 3 )
Dopo elimina(7)
inizionextnextinizionext
Il nodo che precede l'elemento eliminato viene copiato, quelli che lo seguono sono condivisi: l'insieme di partenza non cambia.
Uguaglianza fra insiemi
Un insieme è un valore, quindi equals fa l'uguaglianza profonda: due oggetti distinti con gli stessi elementi sono uguali. Siano A gli elementi di this e B quelli di ins:
A=B⟺A⊆B∧B⊆A
I due cicli di equals controllano le due inclusioni, ciascuno con appartiene. L'ordine dei nodi non conta.
hashCode restituisce sempre 1. È coerente con equals, perché oggetti uguali hanno lo stesso codice, ma non distribuisce gli oggetti.