Esempio: conti correnti bancari
In questa lezione vedremo un esempio reale di applicazione dove andremo ad applicare quanto appreso nelle lezioni precedenti, ed in particolare:
- come costruire una gerarchia di oggetti;
- quando e come usare le interfacce;
- come costruire un grafo di dipendenze funzionante;
- applicare i principi SOLID
Il progetto è un esempio di “compito di realtà”; una buona palestra per imparare in un contesto verosimile cosa significa ed a cosa serve la OOP.
Attività richiesta
Nel progetto si richiede di creare una applicazione in grado di gestire conti correnti di diverso tipo per persone differenti, identificate da un nominativo. Le utenze possibili sono:
- lo studente che ha una scuola di appartenenza, che può gestire un ContoDeposito dove può solo versare e prelevare denaro;
- il lavoratore dipendente che ha una azienda di appartenenza e può eseguire anche bonifici;
Il tutto è poi gestito da una Banca (o più di una) che memorizza tutti i conti bancari e permette di effetturare bonifici tra conti correnti.
Infine per ogni conto corrente deve essere possibile sapere il tipo di intestatario, il numero di conto e il saldo.
Analisi
Prima di scrivere il codice progettiamo la soluzione in modo visuale con UML tramite il diagramma di classe. Prima di disegnarlo però facciamo alcune ipotesi aggiuntive:
- è evidente che Studente e Dipendente sono persone, quindi possono avere una classe base e classi derivate. Stiamo applicando SRP e OCP.
- è evidente inoltre che i conti bancari hanno in comune una classe base. Tuttavia possiamo individuare un tipo base ContoBancario, che permette operazioni di base, un ContoDeposito da esso derivato, ed un ContoCorrente per i bonifici per il solo lavoratore. In questo caso applichiamo OCP (creiamo una nuova classe derivata per ogni nuova funzione) ma anche LSP (in quanto sarà necessario l’override del metodo che ci dice chi è l’intestatario di uno conto).
- Ogni tipo di Conto è associato ad un tipo specifico di Persona, quindi ogni classe specifica dei conti bancari è associata ad una classe specifica di Persona.
- la Banca è una classe a se stante, che dipende sia dalle persone che dai conti correnti.
Fin qui è abbastanza lineare. Ora veniamo al punto critico.
Il problema della dipendenza circolare
Il Conto Corrente, per effettuare i bonifici, deve necessariamente appoggiarsi alla Banca. Tuttavia abbiamo detto sopra che la Banca deve avere un riferimento al Conto Corrente. Si crea quindi una dipendenza circolare (la Banca usa la classe ContoCorrente, ma anche il ContoCorrente usa la Banca).
Cos’è una dipendenza circolare? Si verifica quando nel grafo delle dipendenze una classe A dipende dalla classe B, ma anche la classe B dipende dalla classe A. Nei casi più complessi la dipendenza circolare si può verificare anche indirettamente (ad esempio A dipende da B che dipende da C che dipende di nuovo da A). La dipendenza circolare è un errore di progettazione, perché vincola eventuali modifiche di una classe a tutte le altre classi della sua dipendenza, costringendo ad un enorme lavoro inutile di riscrittura del codice. In altri termini si vanifica il principio di singola responsabilità (SRP).
Per risolvere questo problema useremo quindi le interfacce. La banca anziché usare un contoCorrente, userà una interfaccia che chiameremo IConto, da cui deriverà tutta la geraerchia di conti correnti. In questo modo la Banca dipenderà da quella interfaccia, e rimarrà solo la dipendenza del conto corrente dalla banca. Questo è un modo per applicare la DIP.
Adesso che abbiamo risolto tutti i problemi, vediamo il grafo delle dipendenze.
UML

Come si può vedere grazie a questo diagramma si ha subito un quadro chiaro della nostra applicazione. Il grafo delle dipendenze è aciclico, e permette quindi una evoluzione e modifica delle due gerarchie di classi indipendente. Se un domani volessimo per esempio aggiungere una nuova persona, ad esempio il professionista, con un conto Corrente dedicato, sarebbe sufficiente aggiungere entrambe le classi con poco sforzo, senza toccare nient’altro.
Scrivere il codice diventa inoltre una operazione lineare, in cui andiamo a scrivere le classi nel giusto ordine, seguendo l’ordine delle dipendenze.
Il codice Java
Creiamo prima le classi relative alle persone.
abstract class Persona {
private String nome;
private String cognome;
public Persona(String nome, String cognome) {
this.nome = nome;
this.cognome = cognome;
}
public String getNominativo() {
return this.nome + " " + this.cognome;
}
abstract public String getTipo();
}
class Studente extends Persona {
private String scuola;
public Studente(String nome, String cognome, String scuola) {
super(nome, cognome);
this.scuola = scuola;
}
public String getScuola() {
return this.scuola;
}
public String getTipo() {
return "Studente";
}
}
class Lavoratore extends Persona {
private String azienda;
public Lavoratore(String nome, String cognome, String azienda) {
super(nome, cognome);
this.azienda = azienda;
}
public String getAzienda() {
return this.azienda;
}
public String getTipo() {
return "Dipendente";
}
}
Persona è astratta, non vogliamo che sia implementabile, inoltre usiamo un metodo astratto.
Vediamo ora i i conti bancari.
interface IConto {
void setNumeroConto(int numeroConto);
void versa(double quantita);
void preleva(double quantita);
String getInfo();
Persona getIntestatario();
}
abstract class ContoBancario implements IConto {
protected int numeroConto;
protected double saldo;
protected Persona intestatario;
public ContoBancario(Persona intestatario) {
this.intestatario = intestatario;
this.saldo = 0;
}
public void setNumeroConto(int numeroConto) {
this.numeroConto = numeroConto;
}
public void versa(double quantita) {
this.saldo += quantita;
}
public void preleva(double quantita) {
this.saldo -= quantita;
}
public double getSaldo() {
return this.saldo;
}
public Persona getIntestatario() {
return this.intestatario;
}
public String getInfo() {
String result = "Intestatario: " + this.intestatario.getTipo() + " - Numero Conto: " + this.numeroConto + "\n";
result += "Nome: " + this.intestatario.getNominativo() + "\n";
result += "Saldo: " + this.saldo;
return result;
}
}
class ContoDeposito extends ContoBancario {
public ContoDeposito(Studente studente) {
super(studente);
}
public Studente getIntestatario() {
return (Studente) super.intestatario;
}
}
class ContoCorrente extends ContoBancario {
public ContoCorrente(Lavoratore persona) {
super(persona);
}
public void bonifico(Banca banca, double quantita, int numeroContoDestinatario) {
this.preleva(quantita);
banca.bonifico(this.numeroConto, numeroContoDestinatario, quantita);
}
public Lavoratore getIntestatario() {
return (Lavoratore) super.intestatario;
}
}
Anche questa gerarchia è definita in modo lineare, con la dipendenza verso la gerarchia di persone, come si è visto nell’UML.
Ora vediamo la Banca:
class Banca {
private IConto[] conti;
private int indiceCorrente;
public Banca() {
this.conti = new IConto[100];
this.indiceCorrente = -1;
}
public void aggiungiConto(IConto conto) {
this.indiceCorrente++;
conto.setNumeroConto(indiceCorrente);
this.conti[indiceCorrente] = conto;
}
public void bonifico(int numeroContoOrigine, int numeroContoDestinatario, double quantita) {
this.conti[numeroContoDestinatario].versa(quantita);
}
public void stampaInfo() {
for (int i = 0; i <= this.indiceCorrente; i++) {
System.out.println(this.conti[i].getInfo());
}
}
}
Come si può vedere è la banca ad assegnare un numero di conto al conto corrente e ad inserirlo nella propria lista di conti correnti.
Passiamo infine all’applicazione per vedere un esempio applicativo.
public class ContoCorrenteApplication {
public static void main(String[] args) {
Banca banca = new Banca();
// --- Studenti
ContoDeposito stud1 = new ContoDeposito(new Studente("Luca", "Rossi", "ITIS"));
ContoDeposito stud2 = new ContoDeposito(new Studente("Giulia", "Verdi", "Liceo Classico"));
ContoDeposito stud3 = new ContoDeposito(new Studente("Marta", "Bianchi", "Scientifico"));
// --- Lavoratori
ContoCorrente lav1 = new ContoCorrente(new Lavoratore("Marco", "Neri", "Acme SpA"));
ContoCorrente lav2 = new ContoCorrente(new Lavoratore("Sara", "Galli", "OpenTech"));
ContoCorrente lav3 = new ContoCorrente(new Lavoratore("Pietro", "Conti", "SoftDev"));
// --- Aggiunta dei conti alla banca
banca.aggiungiConto(stud1);
banca.aggiungiConto(stud2);
banca.aggiungiConto(stud3);
banca.aggiungiConto(lav1);
banca.aggiungiConto(lav2);
banca.aggiungiConto(lav3);
// --- Operazioni Studenti
stud1.versa(1000);
stud2.versa(1500);
stud3.versa(500);
stud1.preleva(300);
stud2.preleva(200);
stud3.preleva(600); // saldo insufficiente
// --- Operazioni Lavoratori
lav1.versa(2000);
lav2.versa(2500);
lav3.versa(1800);
lav1.bonifico(banca, 500, 0); // verso stud1
lav2.bonifico(banca, 1000, 1); // verso stud2
lav3.bonifico(banca, 2000, 2); // errore: saldo insufficiente
// --- Stato finale dei conti
banca.stampaInfo();
}
}
Conclusioni
La OOP è uno strumento potente per semplificare un problema complesso tramite una modellizzazione che si basa su oggetti e classi che ci consentono di avere tanti moduli che collaborano tra loro ed ognuno è responsabile di una piccola parte del problema.
I principi SOLID ci aiutano a progettare le applicazioni in modo razionale ed oculato, evitando errori di progettazione che al principio sembrano invisibili, ma che emergono quando si vuole modificare o estendere il programma con nuove funzioni.
Una cosa che dobbiamo osservare è che in programmazione ad oggetti è molto importante fare attenzione, più che alla complessità degli algoritmi, a trovare una soluzione semplice, estendibile e coerente.
Permangono comunque dei limiti che bisogna osservare:
- molto codice è ripetuto, ad esempio tra le varie sottoclassi di ContoBancario cambia veramente poco tra un conto e l’altro, ma è necessario perché ogni conto è specifico per un tipo di Persona.
- anche il codice delle classi Persona contiene parti ripetute come in getIntestatario, perché siccome usiamo una proprietà presente nella classe base, dobbiamo eseguire il cast esplicito alla classe derivata per restituire il giusto tipo di intestatario.
Come vedremo nella prossima lezione questi problemi si risolvono con l’uso dei Generics, uno strumento molto potente che permette di ridurre la verbosità e le ripetizioni e rende il polimorfismo realmente efficace.