UML - Diagramma delle classi
La progettazione di una qualunque applicazione informatica,
basata sul paradigma ad oggetti,
inizia con l'individuazione delle classi, da cui saranno
originati gli oggetti che costituiranno
l'applicazione stessa e che interagiranno tra loro, attraverso le rispettive caratteristiche
(attributi) e funzionalità (metodi).
Un diagramma delle classi UML permette di rappresentare
graficamente le classi che costituiscono
l'applicazione ed i tipi di legami/interazioni tra esse.
Esso rappresenta uno strumento fondamentale al progettista di un
sistema software durante la
fase di analisi e progettazione/modellazione del sistema.
Successivamente, esso costituisce un documento fondamentale
dell'applicazione, nonché lo strumento di dialogo tra i
progettisti e gli sviluppatori dell'applicazione stessa, ossia coloro
che si occuperanno dell'implementazione dell'applicazione attraverso un linguaggio specifico di
programmazione ad oggetti (es. Java).
In un diagramma UML, la classe si rappresenta con un rettangolo
diviso in tre sezioni, contenenti, rispettivamente:
-
il nome della classe
-
gli attributi della classe (caratteristiche e/o informazioni
di stato)
-
i metodi della classe (funzionalità)
Graficamente possiamo sintetizzare quanto detto con la figura che segue:
Nota:
Convenzioni di
naming
Il termine convenzioni di naming indica una serie di regole, universalmente accettate, per
l'attribuzione dei nomi alle classe, agli attributi ed ai metodi.
Le convenzioni di naming più usate, soprattutto in ambito diagrammi UML, per la modellazione di
applicazioni software basate sul paradigma O.O. sono:
- PascalCase: consiste nello scrivere i nomi
con l'iniziale maiuscola; se esso è composto da più parole, queste sono concatenate
senza nessun carattere di separazione, con l'iniziale di ciascuna parola in
maiuscolo.
Esempio: Automobile, Studente, MezzoDiTrasporto,
EdificioScolastico, ...
- camelCase: consiste nello scrivere i nomi
con l'iniziale minuscola; se esso è composto da più parole, queste sono concatenate
senza nessun carattere di separazione, con l'iniziale della prima parola minuscola e
delle parole successive alla prima in maiuscolo.
Esempio: produttore, modello, numeroDiTelaio,
numeroPorte, cilindrata, ...
Dopo aver introdotto le convenzioni di naming, si precisa che, nei diagrammi delle classi UML, i
nomi delle classi seguono la convenzione PascalCase, mentre i nomi degli attributi e dei metodi,
la convenzione camelCase.
Mettendo insieme tutti i concetti visti fino a questo punto, possiamo sintetizzare la
rappresentazione di una classe,
attraverso un diagramma UML come raffigurato di seguito:
Modificatori di visibilità di attributi/metodi
In aggiunta a quanto detto finora, la figura precedente mostra, a sinistra degli attributi e dei
metodi, un segno (+/-) che prende il nome di indicatore di visibilità.
L'indicatore di visibilità può assumere i valori ed i significati seguenti:
-
+, che indica il livello di visibilità pubblico, ossia
l'attributo o il metodo sono visibili (accessibili o utilizzabili) sia nella classe in cui
sono definiti che nelle altre classi che la utilizzano; in altri termini essi sono visibili
sia all'interno che all'esterno della classe
-
-, che indica il livello di visibilità privato, ossia
l'attributo o il metodo sono visibili (accessibili o utilizzabili) solo nella classe in cui
sono definiti; in altri termini essi sono visibili solo
internamente alla classe, ma non
all'esterno della stessa
-
#, che indica il livello di visibilità protetto, ossia
l'attributo o il metodo sono visibili (accessibili o utilizzabili) sia nella classe in cui
sono definiti che nelle altre classi che estendono o derivano da essa; in altri termini essi
sono visibili
internamente alla classe e nella gerarchia delle classi derivate
A questo punto della trattazione, possiamo provare a realizzare il primo esempio reale.
Supponiamo di dover modellare il concetto di televisore.
Attraverso un procedimento di
astrazione possiamo individuare una serie di informazioni
rilevanti (caratteristiche/stato)
ed una serie di funzionalità.
Le informazioni individuate andranno a costituire gli atttributi, le funzionalità i metodi
della classe che ne deriverà e che chiameremo, in accordo con le convenzioni precedentemente
citate, Televisore (notare l'iniziale maiuscola, convenzione PascalCase).
Le informazioni di rilievo possono essere, ad esempio:
-
la marca
-
il modello
-
la dimensione dello schermo, in pollici
-
lo stato acceso/spento
-
il canale sul quale è sintonizzato
-
il livello del volume
Ci possiamo fermare alle informazioni elencate, ma se ne potrebbero individuare diverse altre,
più o meno importanti, a seconda del contesto nel quale l'applicazione in esame dovrà trattare
oggetti del tipo Televisore.
Le informazioni marca, modello e dimensione in pollici costituiscono delle caratteristiche, in
quanto esse non possono cambiare nella vita di un televisore.
In altri termini, un televisore di
una certa marca non potrà mai diventare di un'altra marca e così anche per le altre
caratteristiche.
Viceversa, le informazioni acceso/spento, il canale ed il volume costituiscono informazioni di
stato, perchè esse possono assumere
valori
differenti durante la vita di uno stesso televisore.
Concentriamo ora la nostra attenzione sulle funzionalità che un oggetto televisore può avere nel
semplice contesto analizzato finora.
Possiamo elencare:
-
accendi, per accendere il televisore
-
spegni, per spegnere il televisore
-
imposta su un determinato canale
-
avanza di un canale
-
torna indietro di un canale
-
aumenta il livello del volume
-
diminuisci il livello del volume
Queste sono solo alcune delle funzionalità che si possono individuare a titolo di esempio.
Ovviamente è compito del progettista, mettere in azione le sue
abilità, per determinare quali
sono le funzionalità necessarie, nel contesto di utilizzo degli oggetti da modellare, nonché il
tipo di definizione (tipo di funzionalità svolta, informazioni in ingresso ed in uscita) più
indicate per rendere l'utilizzo degli oggetti di tale tipo, il
più agevole possibile.
A questo punto possiamo provare a modellare quanto analizzato attraverso un diagramma delle
classi UML.
Il risultato potrebbe essere quello evidenziato nella figura seguente:
Notare la distinzione tra attributi che rappresentano delle caratteristiche da quelli che
rappresentano informazioni di stato.
Questa distinzione è importante, perchè, come vedremo più avanti, potrebbe determinare alcuni
dettagli relativi ad altri metodi che aggiungeremo alla classe.
Infatti, in aggiunta ai metodi relativi a funzionalità che abbiamo ritenuto rilevanti in fase di
analisi, ve ne sono degli altri che hanno delle funzioni particolari.
Tra questi:
-
il metodo o i metodi costruttore
-
i metodi getter
-
i metodi setter
Approfondiamoli nel dettaglio uno alla volta.
Il metodo costruttore è un metodo fondamentale per la creazione di oggetti dell tipo della
classe.
Esso non deve specificare un tipo di output, in quanto il tipo
restituito è la classe stessa.
Potrebbe non avere parametri di input (costruttore vuoto o costruttore di default), oppure
avere quelli rilevanti, che
l'analista ha ritenuto
importanti per la costruzione degli oggetti concreti del tipo della classe.
Solitamente, se non si specifica un costruttore, si sottintende l'esistenza
implicita del
costruttore di default, ossia senza parametri di input.
L'analista potrebbe, comunque, prevedere la presenza di più costruttori, con differenti
parametri di input per la creazione di oggetti del tipo classe inizializzati in modo differente.
Un tipo particolare di costruttore è il costruttore di copia che,
come approfondiremo più
avanti, ha un solo parametro di input dello stesso tipo della
classe ed è utilizzato per creare
una copia dell'oggetto ricevuto come parametro.
Ricollegandoci a quanto visto nella sezione relativa al polimorfismo, della parte introduttiva,
possiamo concludere che i diversi costruttori costituiscono un overload
del metodo costruttore,
in quanto lo stesso metodo è sovraccaricato di parametri che differiscono per il tipo, l'ordine
o il numero.
I metodi getter/setter, come si può intuire considerando il
significato dei termini in inglese,
hanno il compito di permettere la lettura (get) o la scrittura (set) del valore di un attributo.
Più specificatamente:
-
il metodo getter di un attributo è un metodo che non ha parametri di input ed ha, come tipo
di output, lo stesso tipo dell'attributo del quale consente
la lettura.
Il suo nome è composto dalla parola get seguita dal nome
dell'attributo (es. getModello è il
nome del metodo getter dell'attributo di nome modello).
Fanno eccezione i metodi getter degli attributi di tipo
boolean, in quanto usano la parola
is seguita dal nome dell'attributo (es. isAcceso è il nome
del metodo getter dell'attributo di nome acceso).
Esso è necessario, con visibilità pubblica, qualora si voglia permettere la lettura del
valore di un attributo privato.
Potrebbe, tuttavia, anche essere presente con visibilità privata, per questioni di comodità
di codifica dello sviluppatore della classe.
-
il metodo setter di un attributo è un metodo che non ha valore di output (void) ed ha
un unico parametro di input dello stesso tipo dell'attributo del quale consente la
scrittura.
Il suo nome è composto dalla parola set seguita dal nome
dell'attributo (es. setModello).
Esso è necessario, con visibilità pubblica, qualora si voglia permettere la scrittura del
valore di un attributo privato.
Potrebbe, tuttavia, anche essere presente con visibilità privata, per questioni di comodità
di codifica dello sviluppatore della classe (es. per brevità, correttezza ed efficienza del
codice, qualora esso debba compiere più azioni/controlli ogni volta che viene modificato il
valore dell'attributo).
Torniamo ora all'esempio del televisore e completiamo il diagramma UML con l'aggiunta dei
costruttori e dei metodi getter/setter.
L'esempio della classe televisore, visto in precedenza, è stato esteso aggiungendo un
costruttore, che prende in input gli attributi che rappresentano le caratteristiche del
televisore e che, quindi, sono note alla sua creazione e non variano durante il corso della vita
dell'oggetto.
Quanto detto è anche riscontrabile dalla visibilità privata dei metodi setter di tali attributi.
Il secondo costruttore inserito rappresenta il costruttore di
copia, infatti, come anticipato
poc'anzi,
possiede un unico attributo dello stesso tipo della classe in cui è
definito, ossia della quale
costituisce il costruttore di copia.
Seguono i metodi getter/setter dei rimanenti attributi, che rappresentano informazioni di stato
del televisore, ossia se esso è acceso o spento, il canale su cui è sintonizzato ed il livello
di volume impostato.
I getter hanno solitamente visibilità pubblica, in quanto la loro funzione è proprio permettere
l'accesso ai valori degli attributi (solitamente con visibilità privata) all'esterno della
classe.
Tra i setter, invece, ho preferito definire con visibilità privata il setter dello stato e del
volume, in quanto ho lasciato i metodi accendi/spegni e aumentaVolume/diminuisciVolume.
Ho, invece, definito, con visibilità pubblica, il setter del canale, in quanto l'ho sostituito
al metodo impostaCanale presente nalla versione precedente.
Avrei potuto, altresì, lasciare il metodo impostaCanale e definire anche il setter
dell'attributo canale con visibilità privata.
Sottolineo che i metodi presenti e la loro visibilità sono una scelta del
progettista, che deve
condurre, ad ogni modo, ad un modello il più chiaro, intuitivo e comprensibile possibile, che
renda l'utilizzo della classe, agli altri sviluppatori, semplice ed immediato, esonerandoli,
nella maggior parte dei casi, dalla dispendiosa lettura della documentazione della classe.
Nota
La capacità di definire i metodi con cui la classe si interfaccia verso l'esterno (metodi
public) rappresenta una delle competenze fondamentali di coloro
che si occuperanno dell'analisi
e progettazione delle classi delle applicazioni informatiche basate sul paradigma
O.O.
Attributi statici e costanti
Estendiamo ora l'esempio del televisore, aggiungendo informazioni per introdurre ulteriori
concetti della programmazione object oriented.
Supponiamo che il costo di un televisore sia determinato dalla sua dimensione in pollici
moltiplicata per il costo unitario di un pollice, mentre il prezzo di vendita sia calcolato
aggiungendo al costo del televisore una percentuale fissa di guadagno.
Si
tratta, ovviamente
di un esempio poco realistico, ma che si presta bene alla comprensione dei concetti e
dell'utilità degli attributi statici e costanti.
Analizzando quanto detto, possiamo concludere che il costo per pollice è una informazione che
riguarda tutte le istanze della classe Televisore, essendo il suo valore il medesimo per tutti i
televisori.
Si tratta di una caratteristica differente rispetto, ad esempio, alla marca.
Quest'ultima, infatti, poteva essere diversa da televisore a televisore.
Questa esigenza di unicità di valore tra tutte le istanze della classe, è tecnicamente
indirizzabile utilizzando un attributo statico (detto anche attributo di classe, a differenza
degli esempi
visti finora di attributi di istanza).
Un attributo statico si rappresenta nel diagramma UML sottolineando la definizione
dell'attributo stesso, come evidenziato nella figura che segue.
Essendo un attributo di classe, quindi che si non si riferisce ad una istanza particolare, ma
bensì a tutte le istanze della classe, la sua definizione, in Java, deve avvenire utilizzando la
parola
chiave static ed il suo utilizzo, in lettura ed in scrittura, utilizzando il nome della classe e
non la parola chiave this, come visto finora per gli attributi di istanza.
Di seguito il codice Java che illustra quanto detto.
// la dichiarazione dell'attributo statico
// si effettua utilizzando la parola chiave static
// prima del tipo dell'attributo
// come mostrato nella riga seguente
private static double costoPerPollice;
// L'utilizzo del valore dell'attributo statico, ad esempio
// nell'implementazione del metodo costo()
// si effettua anteponendo il nome della
// classe, come mostrato di seguito
public double costo() {
return this.dimensione * Televisore.costoPerPollice;
// nota: non this.costoPerPollice
}
Un attributo costante, invece, si rappresenta scrivendone, per
convenzione, il nome con
caratteri maiuscoli (CAPITALCASE).
Agli attributi costanti può essere assegnato un valore nella
definizione dell'attributo stesso, oppure, in alternativa,
soltanto nei metodi costruttori.
Ogni altra assegnazione di valori genererà un errore di
compilazione.
Per il resto, un attributo costante è un attributo di istanza
(come le altre caratteristiche
viste finora) a tutti gli effetti (quindi si utilizza tramite parola chiave this).
La dichiarazione in Java deve premettere la parola chiave final,
come mostrato nel frammento di codice seguente:
// la dichiarazione dell'attributo costante
// si effettua utilizzando la parola chiave final
// prima del tipo dell'attributo
// come mostrato nella riga seguente
private final double PERCENTUALE_GUADAGNO = 0.2;
// nota: 0.2 equivale in percentuale al 20%
// L'utilizzo del valore dell'attributo statico, ad esempio
// nell'implementazione del metodo prezzoVendita()
// si effettua, come visto finora, per
// gli attributi di istanza, anteponendo
// la parola chiave this
public double prezzoVendita() {
// calcolo del costo incrementato del 20%
return this.costo() * (1 + this.PERCENTUALE_GUADAGNO);
}
Spostiamo ora la nostra attenzione sui metodi getter e setter
dell'attributo di classe (o
statico) costoPerPollice.
Essendo l'attributo statico, quindi non di istanza, ma di classe, di conseguenza anche i suoi
metodi getter e setter devono essere metodi di classe e non di
istanza.
Questo vuol dire che essi dovranno usare la parola chiave static,
prima della specifica del tipo restituito, nella loro definizione.
Inoltre, la loro invocazione dovrà essere effettuata utilizzando il nome
della classe e non la parola chiave this, come mostrato nel codice Java che segue.
// getter e setter dell'attributo costoPerPollice
// notare l'uso della parola chiave static
public static double getCostoPerPollice() {
return Televisore.costoPerPollice;
}
public static void setCostoPerPollice(double costoPerPollice) {
Televisore.costoPerPollice = costoPerPollice;
}
// l'invocazione di tali metodi all'esterno della classe Televisore
// deve avvenire utilizzando il nome della classe al posto
// del nome della variabile che contiene l'istanza di Televisore
public static void main(String[] args) {
...
Televisore t1 = new Televisore(...);
...
Televisore.setCostoPerPollice = 20.0;
// nota: non t1.setCostoPerPollice = 20.0;
System.out.println("Il prezzo del televisore è " + t1.prezzo());
...
}
Nota:
I metodi
statici non sono soltanto utilizzati per leggere (getter) o modificare
(setter) il valore di un attributo statico.
Spesso si utilizzano metodi statici per implementare funzionalità appartenenti a tutti gli
oggetti di una classe, a prescindere dalle loro caratteristiche concrete (ossia dai valori
assunti dagli attributi).
Si può optare per un metodo statico ogni volta che la funzionalità da implementare non
richiede di accedere ai valori degli attributi di una istanza specifica.
Un esempio classico di utilizzo dei metodi statici potrebbe essere una libreria di funzioni
matematiche, che
per eseguire il proprio lavoro non necessitano di una istanza specifica, ma semplicemente di
valori che possono essere passati attraverso i parametri del metodo stesso.
I metodi statici, infatti, si possono utilizzare senza creare una
istanza specifica
della classe, ma semplicamente utilizzando il nome della
classe.
Di seguito è mostrato un esempio di classe con metodi statici.
// Esempio di libreria di funzioni matematiche
public class Matematica {
// calcolo della potenza
public static int potenza(int base, int esponente) {
// implementazione algoritmo di calcolo
// della potenza per moltiplicazioni successive
int potenza = 1;
for (int i = 0; i < esponente; i++) {
potenza *= base;
}
return potenza;
}
// calcolo del resto intero della divisione
public static int modulo(int dividendo, int divisore) {
// implementazione algoritmo di calcolo
// del resto per sottrazioni successive
int resto = dividendo;
while (resto >= divisore) {
resto -= divisore;
}
return resto;
}
// ... altre funzioni matematiche
}
Nota:
Si evidenziano
l'assenza di attributi, l'assenza di un costruttore e l'uso della parola chiave static
nella definizione dei metodi.
Un esempio di utilizzo, nel main, delle funzioni definite nella classe Matematica, potrebbe
essere il seguente:
// Utilizzo delle funzioni definite nella classe Matematica
public static void main(String[] args) {
// Notare l'utilizzo delle funzioni della
// classe Matematica senza la creazione di
// una istanza della classe stessa
// Per stampare il risultato di 2 elevato alla 5
// si può semplicemente utilizzare la funzione
// statica potenza
// Notare l'uso dell'espressione NomeDellaClasse.metodoStatico
System.out.println(Matematica.potenza(2, 5));
// Per stampare il resto della divisione tra 17 e 3
// si può semplicemente utilizzare la funzione
// statica modulo
// Notare l'uso dell'espressione NomeDellaClasse.metodoStatico
System.out.println(Matematica.modulo(17, 3));
}
Nota:
Alcuni
linguaggi, tra cui Java, consentono di invocare metodi statici utilizzando istanze della classe
(variabili del tipo della classe). Anche se permesso, questo non è concettualmente corretto in
quanto i metodi statici, essendo metodi di classe e non di istanza, si utilizzano con la
notazione NomeDellaClasse.metodoStatico.
Molti linguaggi (es. C#) segnalano un errore di compilazione nel caso di utilizzo di un metodo
statico attraverso una istanza della classe.