Lezione 7 · Canale 1 · giovedì 8 ottobre 2026
Visite ospedaliere: classi e incapsulamento
Progettazione del Software
Dallo schema alle classi
Il caso di studio è un gestionale di visite ospedaliere. Di ogni persona interessano nome, cognome e anno di nascita, quindi pazienti e medici hanno una superclasse Persona. Per convenzione i nomi delle classi sono al singolare e iniziano con la maiuscola.
Medicoaggiunge la specializzazione, mentrePazientenon aggiunge attributi.- Una visita ha un codice, è prenotata da esattamente un paziente e appartiene a un solo reparto, di cui interessa il nome. Il reparto non si realizza qui.
- Un medico ha al più 3 visite assegnate: un limite basso permette di verificare il vincolo con poche prove.
Lo schema delle visite
La gerarchia è completa, perché le uniche persone che interessano sono medici e pazienti. In UML lo si indica scrivendo {complete} accanto alla generalizzazione. In Java la superclasse è abstract, con lo stesso significato: non esistono oggetti che siano solo Persona, quindi new Persona(…) non compila, mentre una variabile di tipo Persona può riferire un medico o un paziente.
In Java due classi sono una dentro l'altra oppure disgiunte. Medico e Paziente sono quindi disgiunte: nessun oggetto è insieme medico e paziente. Un medico che si fa visitare è rappresentato da due oggetti, un Medico e un Paziente, con gli stessi dati.
Che cosa la specifica non dice
Una specifica formale, oltre al diagramma, elenca le proprietà note alla nascita dell'oggetto e quelle che non cambiano. Un dato noto alla nascita è un argomento del costruttore, un dato modificabile ha un setter. Compilare l'elenco fa emergere ciò che chi scrive la specifica dà per scontato.
| Proprietà | Alla nascita | Poi |
|---|---|---|
nome, cognome, nascita di Persona | noti | non cambiano |
specializzazione di Medico | nota | non cambia |
codice di Visita | noto | non cambia |
paziente di Visita | noto | non cambia: le visite sono personali |
visite di Paziente | nessuna | se ne aggiungono |
medico di Visita | nessuno | si assegna, si toglie, cambia |
visite di Medico | nessuna | se ne aggiungono e se ne tolgono |
La specifica non scrive che il paziente esista prima della visita, né che nome e cognome non cambino, mentre nella realtà possono cambiare. Ciò che sembra ovvio può anche essere falso: un codice fiscale ha una forma di lettere e cifre per le persone fisiche, ma quello di una società è di 11 cifre e coincide con la partita IVA.
Campi privati e dati immutabili
Con un campo non privato v.prenota = null compila, ma una visita deve avere sempre un paziente. Con prenota privato non compila: private significa che il nome del campo si può usare solo fra la graffa di apertura e quella di chiusura della classe. Il dato resta leggibile con getPrenota(), che è pubblico. Senza setter, l'unica istruzione che lo scrive sta nel costruttore, e così il dato è immutabile.
Lo stesso vale per il codice della visita e per nome, cognome e nascita di Persona. Un campo privato non si usa nemmeno nelle sottoclassi, che leggono i dati ereditati con i getter: protected darebbe loro accesso diretto.
Una classe si scrive una volta e poi la usano altri, magari un gruppo che non può modificarla. Deve restare corretta anche contro chi sbaglia o cerca di farla sbagliare: se resiste a questo, resiste anche agli errori involontari.
Controlli nel costruttore
Il compilatore accetta la stringa vuota e null come argomenti String: null non è una stringa, ma è un valore valido per un riferimento String. Non si possono vietare a compilazione, quindi il costruttore li controlla a esecuzione e lancia un'eccezione. Un errore deve essere subito evidente: se nessuno la intercetta, l'eccezione interrompe il programma e ne stampa il messaggio.
Il controllo esclude prima null, poi la stringa vuota, perché su un riferimento nullo non si può chiamare equals. La stringa vuota si confronta con equals, non con ==, che confronta i riferimenti. == sembra funzionare, perché "" scritto due volte è lo stesso oggetto, ma new String("") è un'altra stringa vuota e == non la riconosce.
public abstract class Persona {
private String nome;
private String cognome;
private int nascita;
public Persona(String nome, String cognome, int nascita) {
if (nome == null || nome.equals(""))
throw new RuntimeException("nome non valido");
if (cognome == null || cognome.equals(""))
throw new RuntimeException("cognome non valido");
this.nome = nome;
this.cognome = cognome;
this.nascita = nascita;
}
public String getNome() { return nome; }
public String getCognome() { return cognome; }
public int getNascita() { return nascita; }
}
Il controllo sta nella classe che protegge i dati. I costruttori di Paziente e Medico lo eseguono chiamando super(...). Nome e cognome non sono mai nulli né vuoti perché li scrive solo il costruttore, dopo il controllo: con un setter, il controllo andrebbe ripetuto anche lì.
Paziente, Visita e Medico
Il campo che realizza un'associazione si chiama come l'associazione, con lo stesso nome nelle due classi: prenota in Paziente e in Visita, assegnato in Medico e in Visita.
Il paziente di una visita è noto alla creazione: è un argomento del costruttore e ha solo il getter. Le visite di un paziente si associano dopo la sua creazione: l'insieme non è un argomento e si crea vuoto con new, perché dichiarare il campo crea solo un riferimento. In questa prima versione il getter restituisce l'insieme.
import java.util.HashSet;
public class Paziente extends Persona {
private HashSet<Visita> prenota;
public Paziente(String nome, String cognome, int nascita) {
super(nome, cognome, nascita);
this.prenota = new HashSet<Visita>();
}
public HashSet<Visita> getPrenota() { return prenota; }
public String toString() {
return getNome() + " " + getCognome() + " (" + getNascita()
+ "), visite " + prenota;
}
}
public class Visita {
private String codice;
private Paziente prenota;
private Medico assegnato;
public Visita(String codice, Paziente prenota) {
this.codice = codice;
this.prenota = prenota;
}
public String getCodice() { return codice; }
public Paziente getPrenota() { return prenota; }
public Medico getAssegnato() { return assegnato; }
public void setAssegnato(Medico assegnato) { this.assegnato = assegnato; }
public String toString() {
return codice + " di " + prenota.getNome() + " "
+ prenota.getCognome();
}
}
Il medico di una visita non è noto alla creazione e può cambiare: non sta nel costruttore, e null indica la visita non assegnata. Qui il campo privato con getter e setter non serve alla correttezza, perché ogni valore è ammesso. È una convenzione: chi usa la classe si aspetta getAssegnato e setAssegnato, anche quando non servono.
Medico passa nome, cognome e nascita a super e aggiunge la specializzazione, privata e senza setter. Il suo toString stampa anche i dati ereditati, letti con i getter.
Stampa nei due versi
Se Paziente.toString stampa le visite e Visita.toString ristampa il paziente, la stampa non finisce: il paziente stampa la visita, che stampa il paziente, e così via fino a un StackOverflowError. Basta che uno dei due stampi solo qualche dato dell'altro: la visita stampa nome e cognome del paziente.
Programma di prova
Il programma di prova ha un main che crea gli oggetti, li collega e li stampa. Una visita creata non è fra quelle del paziente finché l'associazione non si aggiunge esplicitamente. Il programma non si butta: ricrea subito i dati e prova le classi isolate. Con molti oggetti conviene leggere i dati da un file.
Paziente p = new Paziente("Mario", "Rossi", 1980);
Visita v = new Visita("V001", p);
System.out.println(p); // Mario Rossi (1980), visite []
p.getPrenota().add(v); // l'altro verso di prenota
System.out.println(p); // Mario Rossi (1980), visite [V001 di Mario Rossi]
Insiemi: la copia difensiva
Le visite di un medico sono un insieme, inizialmente vuoto, con al più 3 elementi. Il campo assegnato privato non basta: private impedisce di scrivere la variabile fuori dalla classe, ma se il getter restituisce l'insieme, chi lo riceve ha un riferimento allo stesso oggetto e può modificarlo.
m.getAssegnato().add(v4); // con il getter che restituisce l'insieme
Con tre visite già assegnate, la quarta entra senza errore. Il getter deve restituire una copia, e le modifiche passano da metodi della classe. HashSet ha un costruttore che riceve una collezione e crea un nuovo insieme con gli stessi elementi.
Il getter restituisce una copia
import java.util.HashSet;
public class Medico extends Persona {
private String specializzazione;
private HashSet<Visita> assegnato;
public Medico(String nome, String cognome, int nascita,
String specializzazione) {
super(nome, cognome, nascita);
this.specializzazione = specializzazione;
this.assegnato = new HashSet<Visita>();
}
public String getSpecializzazione() { return specializzazione; }
public HashSet<Visita> getAssegnato() {
return new HashSet<Visita>(assegnato);
}
public void addAssegnato(Visita v) {
if (assegnato.size() >= 3)
throw new RuntimeException("troppe visite");
assegnato.add(v);
}
public void removeAssegnato(Visita v) { assegnato.remove(v); }
public String toString() {
return getNome() + " " + getCognome() + " (" + getNascita()
+ "), " + specializzazione;
}
}
Il metodo di aggiunta controlla il limite e lancia un'eccezione, quindi la quarta visita non entra. La rimozione ha il suo metodo, perché le visite si devono poter disassegnare. Un vincolo minimo, per esempio almeno una visita, si controllerebbe lì. remove riconosce l'elemento con equals: se ogni visita è rappresentata da un solo oggetto basta l'identità, altrimenti si ridefinisce equals e, per gli insiemi, anche hashCode.
Medico m = new Medico("Laura", "Verdi", 1975, "cardiologia");
v.setAssegnato(m);
m.addAssegnato(v); // l'altro verso di assegnato
m.addAssegnato(new Visita("V002", p));
m.addAssegnato(new Visita("V003", p));
System.out.println(m.getAssegnato().size()); // 3
m.addAssegnato(new Visita("V004", p)); // RuntimeException: troppe visite
Regole di protezione
- Campi semplici (stringhe, interi): privati, con getter. Il setter c'è solo se il campo può cambiare, e i controlli stanno nei metodi che lo scrivono.
- Insiemi e liste: privati, con un getter che restituisce una copia. Aggiunta e rimozione passano da metodi che applicano i vincoli.
- Un'operazione illegale o non compila, come scrivere un campo privato, oppure viene rifiutata a esecuzione con un'eccezione.
Formulario
Gerarchia
{complete}: copre la superclasseabstract: nientenew- classi Java: incluse o disgiunte
Proprietà
- nota alla nascita: costruttore
- modificabile: setter
- immutabile: solo getter
Campi
private: si usa nella classe- sottoclasse: legge con i getter
Controlli
- nel costruttore e nei setter
- prima
null, poiequals("") ==: stesso oggettoequals: stesso contenutothrow new RuntimeException(...)
Insiemi
- campo
private - getter: copia
adderemovecon controlli