Parte III — Organizzare
Capitolo 11 — Classi e oggetti
Finora abbiamo tenuto i dati da una parte (variabili, liste, dizionari) e il comportamento dall'altra (funzioni). Spesso, però, le due cose vanno insieme: un conto in banca ha un saldo e sa versare e prelevare; una persona ha un nome e sa salutare. Le classi permettono di unire dati e comportamento in un unico stampo, da cui creare tanti oggetti indipendenti. È il cuore della cosiddetta «programmazione a oggetti», e in Leo ha una forma particolarmente leggibile.
🦁 Curiosità — Nata in Norvegia, battezzata pensando alle cellule
Le idee alla base della programmazione a oggetti nascono negli anni '60 col linguaggio Simula, creato a Oslo da Ole-Johan Dahl e Kristen Nygaard. L'espressione «object-oriented» la coniò invece Alan Kay, che raccontò di essersi ispirato alle cellule biologiche: tante unità autonome, ciascuna coi propri dati, che collaborano scambiandosi messaggi. Non è poi così diverso da quello che facciamo noi con gli oggetti.
Definire una classe
Una classe si definisce con classe. Dentro, i dati si dichiarano con ha e i comportamenti con azione. All'interno di un'azione, l'oggetto su cui stai lavorando si chiama questo:
classe Persona
ha nome
ha eta = 0
azione saluta()
scrivi "Ciao, sono {questo.nome} e ho {questo.eta} anni"
fine
azione compleanno()
questo.eta = questo.eta + 1
fine
fine
variabile p = Persona("Ada", 36)
p.saluta()
p.compleanno()
scrivi "Dopo il compleanno: {p.eta}"
variabile q = nuovo Persona("Marco")
q.saluta()Ciao, sono Ada e ho 36 anni
Dopo il compleanno: 37
Ciao, sono Marco e ho 0 anniSmontiamo con calma. ha nome dichiara un dato (un campo) obbligatorio; ha eta = 0 dichiara un campo con valore predefinito, che puoi quindi omettere alla creazione. Le azione sono funzioni che appartengono all'oggetto: dentro, questo.nome e questo.eta leggono e cambiano i dati di quel particolare oggetto.
Per creare un oggetto — un'istanza della classe — scrivi il nome della classe con i valori tra parentesi: Persona("Ada", 36). Se preferisci una forma più discorsiva, Leo accetta anche nuovo Persona("Marco"): le due scritture sono equivalenti, scegli quella che si legge meglio. L'oggetto creato risponde ai metodi con il punto: p.saluta(), p.compleanno().
Nota che p e q sono indipendenti: il compleanno di Ada non tocca Marco. Ogni oggetto porta con sé la propria copia dei dati. È questo che rende le classi così utili: uno stampo, infiniti oggetti distinti.
💡 L'idea — Lo stampino dei biscotti
Una classe è come lo stampino per i biscotti, e gli oggetti sono i biscotti. Lo stampino decide la forma — quali campi e quali azioni ci saranno — ma non è qualcosa che si mangia; ogni biscotto che ne esce è invece concreto, con la sua glassa e le sue decorazioni. Allo stesso modo
Personadescrive la forma, e Ada e Marco sono due biscotti ben distinti usciti dallo stesso stampino.
Campi, tipi e protezioni
I campi possono avere un tipo, che Leo controlla alla creazione:
classe Punto
ha x: intero
ha y: intero
fineCon questa definizione, Punto(3, 4) va bene ma Punto("ciao") dà un errore chiaro: manca y, e x dovrebbe essere un intero. Dichiarare i tipi è facoltativo, ma nei programmi seri aiuta a cogliere gli sbagli subito.
Vediamo una classe un po' più ricca, un conto corrente che si difende dagli usi scorretti sollevando un errore (ricordi lancia dal Capitolo 10?):
classe Conto
ha intestatario
ha saldo = 0
azione versa(importo)
questo.saldo = questo.saldo + importo
fine
azione preleva(importo)
se importo > questo.saldo allora
lancia ErroreValore("Fondi insufficienti")
fine
questo.saldo = questo.saldo - importo
fine
azione descrivi() dà testo
restituisci "{questo.intestatario}: {questo.saldo} euro"
fine
fine
variabile c = Conto("Giulia")
c.versa(100)
c.versa(50)
c.preleva(30)
scrivi(c.descrivi())Giulia: 120 euroUn'azione può anche restituire un valore con restituisci, come una funzione qualunque; la notazione azione descrivi() dà testo dichiara — facoltativamente — che il risultato è un testo. L'oggetto Conto racchiude i suoi dati e li protegge: l'unico modo di muovere il saldo è attraverso versa e preleva, che possono imporre le regole (niente scoperti). Questo è il senso dell'incapsulamento: tenere insieme i dati e le regole che li governano.
L'ereditarietà: costruire sul già fatto
Capita spesso che diverse classi condividano gran parte del comportamento e differiscano solo in qualche dettaglio. Un cane e un gatto sono entrambi animali: hanno un nome, si presentano allo stesso modo, ma fanno un verso diverso. L'ereditarietà, con la parola estende, permette a una classe di partire da un'altra e cambiare solo ciò che serve:
classe Animale
ha nome
azione verso() dà testo
restituisci "..."
fine
azione presentati()
scrivi "{questo.nome} fa {questo.verso()}"
fine
fine
classe Cane estende Animale
azione verso() dà testo
restituisci "bau"
fine
fine
classe Gatto estende Animale
azione verso() dà testo
restituisci "miao"
fine
fine
per ogni a in [Cane("Fido"), Gatto("Romeo"), Animale("Misterioso")] fai
a.presentati()
fineFido fa bau
Romeo fa miao
Misterioso fa ...Cane e Gatto estendono Animale: ereditano il campo nome e il metodo presentati, ma ridefiniscono verso a modo loro. La magia è nella riga presentati, scritta una volta sola in Animale: quando chiama questo.verso(), Leo usa la versione giusta per quell'oggetto — bau per il cane, miao per il gatto, ... per l'animale generico. Questo meccanismo, per cui lo stesso metodo si comporta diversamente a seconda del tipo reale dell'oggetto, si chiama polimorfismo ed è ciò che rende l'ereditarietà potente: aggiungere un Mucca che fa muu non richiede di toccare presentati.
Richiamare il genitore
Quando ridefinisci un metodo ma vuoi comunque usare la versione della classe da cui erediti, c'è la parola genitore:
classe Veicolo
ha nome
azione descrivi() dà testo
restituisci "Veicolo {questo.nome}"
fine
fine
classe Auto estende Veicolo
azione descrivi() dà testo
restituisci genitore.descrivi() + " (un'automobile)"
fine
fine
scrivi(Auto("Panda").descrivi())Veicolo Panda (un'automobile)genitore.descrivi() chiama la versione di Veicolo, e Auto ci costruisce sopra aggiungendo la sua parte. È il modo pulito per estendere un comportamento invece di riscriverlo da zero.
Quando usare le classi
Una parola di equilibrio, perché le classi sono utili ma non obbligatorie. Se un dato è semplice, un dizionario basta e avanza. Le classi guadagnano terreno quando i dati e le regole che li governano cominciano a intrecciarsi: quando hai bisogno di garantire invarianti (il saldo non va sotto zero), di tanti oggetti simili ma distinti, o di una gerarchia di tipi che condividono comportamento. Non sentirti obbligato a incapsulare tutto in una classe fin dal primo giorno: introducile quando il programma te le chiede.
Prova tu
- Scrivi una classe
Rettangolocon i campibaseealtezzae le azioniarea()eperimetro(). Creane un paio e stampane le misure. - Aggiungi alla classe
Contoun metodotrasferisci(altro, importo)che preleva daquestoe versa su un altro conto. Collauda un trasferimento tra due conti. - Crea una classe
Quadratoche estendeRettangoloe, nel costruttore o in un metodo, imposta base e altezza uguali. Verifica che ereditiareaeperimetro.