Gatti, particelle e oggetti
La volta scorsa abbiamo visto come rendere un po’ più flessibile e gestibile il nostro ammasso di cerchietti usando gli array, ma abbiamo anche visto che il tutto diventava più incasinato, con mille array che contenevano ciascuno un pezzetto di informazione relativa a ciascun cerchietto e ogni volta ce la dovevamo andare a cercare in un posto diverso. Sarebbe bello se tutte queste informazioni fossero raggruppate in modo da poter chiamare “cerchietto” quel gruppo e al suo interno trovarci solo le sue informazioni. Ebbene, a questo servono gli oggetti.
Programmazione orientata agli oggetti
La cosiddetta Object-Oriented Programming (OOP per gli amici) è uno dei paradigmi di programmazione più importanti e discussi degli ultimi cinquant’anni: importante perché ci permette di astrarci ancora di più dalla macchina e organizzare i concetti in modo più familiare; discussi perché, come tutte le idee di grande impatto, ciascuno la interpreta e la realizza a modo suo, generando un florilegio di implementazioni che condividono la stessa idea di base e differiscono in mille minuscoli dettagli filosofici. Poi dice che la OOP è roba noiosa.
Il fulcro attorno a cui tutto ruota è l’oggetto, cioè un’entità astratta che possiede proprietà e capacità molto concrete. Nella migliore tradizione filologica attorno alla OOP, non ci sono esempi classici ma ognuno ha il suo che va bene tanto quanto gli altri quindi, siccome a me piacciono gli animali e qui siamo su Internet, prendiamo un gatto:
Un gatto ha un sacco di proprietà, per esempio il colore del pelo, l’età, il sesso, la fame e mille altre che però fermiamoci qui. D’altro canto un gatto ha anche un sacco di capacità tra cui le più importanti sono: mangiare, dormire, miagolare e ignorare gli umani, tranne se ha fame.
Se volessimo creare un oggetto in Processing che rappresenta un gatto nelle sue caratteristiche essenziali, dovremmo creare una classe che altro non è che il modo OOP di raggruppare concetti collegati sotto un unico nome:
class Cat { color furColour; int age; bool hungry; Cat() { furColor = color(255, 255, 255); age = random(0, 10); hungry = false; } void eat() { if(hungry) { println("I'm eating."); hungry = false; } } void sleep() { println("zzz"); } void vocalise() { println("MEOW!"); } void ignoreHumans() { if(hungry) { vocalise(); } } }
Vediamo una cosa per volta. Intanto la prima riga dice che stiamo dichiarando una classe usando la parola chiave class
seguita dal nome della classe, in questo caso Cat
. A seguire si apre un blocco dentro al quale definiremo la classe attraverso i suoi membri, cioè le sue proprietà e le sue capacità. I membri si dividono in due categorie: le proprietà (o campi) che altro non sono che variabili, e le funzioni, che nella terminologia ad oggetti si chiamano metodi e rappresentano le azioni che può compiere la classe, ovvero le sue capacità. In questo caso le proprietà del nostro gatto sono il colore del pelo, l’età e la fame, e le azioni che può compiere sono mangiare, se ha fame, dormire, miagolare e ignorare gli umani, sempre che non abbia fame, in tal caso miagola.
C’è però una cosa che abbiamo saltato, ed è la funzione speciale che ha lo stesso nome della classe. Questa si chiama costruttore e la sua responsabilità è assicurarsi che, una volta che creiamo un nuovo gatto, questo ci venga con tutte le proprietà inizializzate in modo da non doverlo fare noi ogni volta. In questo caso ogni nuovo gatto sarà bianco, avrà un’età compresa tra zero e dieci anni e non sarà, fortunatamente, affamato. Se vogliamo un gatto diverso dovremo modificarlo una volta creato: mi rendo conto che la metafora comincia a non funzionare più tanto bene ma portate pazienza. A questo punto vediamo come si crea un nuovo gatto:
Cat myCat = new Cat();
Niente di più facile e allo stesso tempo potente: abbiamo creato un nuovo tipo di dato e possiamo usarlo per creare variabili! L’unica vera novità qui è la parola chiave new
, anche se l’avevamo già vista per gli array ma non facciamoci caso per ora, che serve a creare nuovi oggetti di una certa classe. Parlando in codice, abbiamo creato una istanza della classe gatto. Per i più filosofici tra voi, possiamo fare finta che le classi siano gli archetipi che fluttuano nell’iperuranio (ingiustamente) e che le istanze siano la loro manifestazione nel mondo sensibile. Ma siccome io volevo un gatto nero e lui me l’ha dato bianco [1] vediamo come rimediare:
myCat.furColour = color(0, 0, 0);
il puntino dopo il nome di un’istanza di classe (che da ora in poi chiameremo oggetto) è il modo con cui si accede ai suoi membri, in questo caso abbiamo assegnato un nuovo valore al colore del pelo e d’ora in poi il gatto rappresentato dall’oggetto myCat
avrà il pelo nero. Allo stesso modo si accede ai metodi, quindi se vogliamo che il nostro gatto miagoli basterà scrivere
myCat.vocalise();
e goderci il risultato. Teniamo a mente che proprietà e metodi di una classe sono associati ad una particolare istanza, quindi se adesso creassimo un altro gatto, questo avrebbe di nuovo il pelo bianco e il gatto precedente resterebbe nero.
La bellezza delle classi sta nel fatto che realizzano i concetti di incapsulamento ed astrazione, nel senso che raggruppano in un’unica entità tutte le sue proprietà e le sue capacità, e ci permettono di ragionare non più con dati sparsi per la memoria ma con oggetti che hanno un significato per noi umani.
Particelle
A questo punto uno si chiede che relazione ha tutto ciò con i cerchietti che ci siamo trascinati fin’ora. Ebbene, ora possiamo definire una classe che rappresenti un cerchietto e ne racchiuda tutte le sue proprietà tipo la posizione, il raggio, il colore e via dicendo.
class Particle { PVector position; PVector direction; float acceleration; float speed; float radius; color colour; Particle() { position = new PVector(random(width), random(height)); direction = new PVector(random(-1, 1), random(-1, 1)); direction.normalize(); acceleration = 0.0f; radius = random(10, 30); speed = random(10) / radius; colour = color(random(360), random(100), random(100)); } void update() { speed = speed + acceleration; PVector displacement = direction.get(); displacement.mult(speed); position.add(displacement); } void draw() { noStroke(); fill(colour); ellipse(position.x, position.y, radius, radius); } }
Ho deciso che d’ora in poi i cerchietti li chiameremo particelle, da cui il nome della classe Particle
, e questa scelta sarà più chiara tra qualche lezione, ma per ora andiamo con ordine. In cima troviamo una fila di proprietà tra cui la posizione, il raggio e il colore. La posizione introduce un nuovo tipo di dato, PVector
, che è una classe a sua volta e che rappresenta quello che in matematica si chiama un vettore a tre dimensioni — anche se noi ne useremo due, per il momento — e che possiamo pensare più facilmente come un punto nel piano cartesiano, che sono sicuro tutti avete studiato prima di finire le medie. C’è solo un piccolo problema che già abbiamo affrontato ma che è bene tenere a mente d’ora in poi: le ordinate sono rovesciate.
Dato che a questo punto tutto è un po’ più facile, ho pensato che potevamo aggiungere un po’ di movimento e quindi ho inserito anche le proprietà relative: direzione, accelerazione e velocità. Niente paura, un po’ di fisica classica che sono sicuro avrete studiato in prima superiore e ce la caviamo.
Procedendo, troviamo il costruttore che inizializza la particella con un po’ di valori a caso, tipo la posizione [2], la direzione [3], l’accelerazione la mettiamo a 0 per praticità, poi il raggio a caso, la velocità la impostiamo sempre più o meno a caso ma comunque in rapporto inverso al raggio così le particelle più grosse si muoveranno più lentamente, e infine il colore, sempre a caso.
A questo punto entra in gioco la funzione cruciale, update()
. Questa è la funzione deputata ad aggiornare i parametri delle particelle col passare del tempo. I calcoli in realtà sono ben poco interessanti, li trovate in qualsiasi libro di fisica, mentre quello che mi interessa è farvi notare i metodi della classe PVector
, cioè get()
che serve a fare una copia del vettore direction
e metterla in displacement
, quindi effettivamente creando un nuovo vettore per evitare di modificare quello membro della classe [4]; poi c’è mult()
che serve a moltiplicare per un numero il vettore su cui lo chiamiamo, in questo caso la velocità; e infine add()
che non fa altro che sommare al vettore su cui lo chiamiamo il vettore che passiamo come parametro. La classe PVector
ha molti altri metodi interessanti elencati nella documentazione ufficiale di Processing, ma per ora limitiamoci a questi tre — senza contare che abbiamo già visto anche normalize()
poco fa [5]. Infine menzioniamo la funzione draw()
che consente alla particella di disegnare sé stessa sullo schermo, il che è un bel vantaggio [6] che apprezzeremo a fondo esaminando il codice principale del programma.
ArrayList particles; float fps; void setup() { fps = 30.0f; size(800, 600); frameRate(fps); colorMode(HSB, 360, 100, 100); particles = new ArrayList(); for(int i = 0; i < 100; i++) { Particle p = new Particle(); particles.add(p); } } void update() { for(int i = 0; i < particles.size(); i++) { Particle p = (Particle) particles.get(i); p.update(); } } void draw() { update(); background(0, 0, 0); for(int i = 0; i < particles.size(); i++) { Particle p = (Particle) particles.get(i); p.draw(); } }
Ignoriamo per un momento l’ArrayList
che, neanche a dirlo, è una classe (questa volta di Java, quindi accessibile anche a Processing) che implementa di fatto un array simile a quelli che abbiamo visto la volta scorsa, solo più flessibile. Avremo modo di approfondire questo genere di tipi di dato astratti più avanti. Tornando a noi, all’interno della funzione setup()
troviamo un ciclo for
[7] in cui creiamo cento particelle e le aggiungiamo all’array usando il suo metodo add()
. A questo punto, visto quello che sappiamo sulle particelle e sugli array, i contenuti delle funzioni update()
e draw()
del programma principale non dovrebbero stupirci neanche un po’! Scorriamo l’array da cima a fondo e chiamiamo i metodi update()
e draw()
su tutti gli elementi che contiene. C’è una cosa a cui fare attenzione, però:
Particle p = (Particle) particles.get(i);
per quanto possiamo immaginare che il metodo get()
dell’array ci restituisca l’elemento contenuto alla posizione passata come parametro [8] c’è quella Particle
tra parentesi che non quadra. Quella, signore e signori, è la notazione che realizza ciò che tecnicamente si chiama cast, cioè prendere dei dati da una certa variabile o posizione e assegnarli ad un’altra variabile di un altro tipo. Ora, questa è un’operazione estremamente pericolosa e non è che possiamo farla tra ogni tipo e ogni altro perché quasi certamente risulterebbe in conseguenze bizzarre, quando non addirittura nell’impossibilità di compilare ed eseguire i nostri programmi. In questo caso però i più acuti tra voi si staranno chiedendo perché dovremmo usare un cast per prendere un elemento che sappiamo essere di tipo Particle
contenuto nell’ArrayList
e assegnarlo ad una variabile di tipo Particle
. Ebbene, la ragione è semplice: l’ArrayList
non sa cosa contiene, è solo una sequenza di locazioni di memoria, e quindi non può dirlo al compilatore che avrebbe il compito di interpretare correttamente l’assegnamento, e di conseguenza è nostra responsabilità fare il lavoro al posto suo [9].
Esercizi
Con questa lezione abbiamo fatto un balzo enorme verso una programmazione più a misura di essere umano, ma abbiamo anche messo un sacco di nozioni nel calderone e quindi è bene lasciarle depositare e farci qualche esercizio sopra. Per esempio, senza modificare la classe Particle
, provate a riprodurre il nostro solito programma con i cerchietti in griglia che cambiano colore e dimensione a seconda della loro posizione e distanza dal mouse.
Una nota: nella finestra di Processing, sul lato destro in alto c’è una freccina che punta a destra. Se ci schiacciate e selezionate “New Tab”, lì potete scrivere le nuove classi che andremo a creare, una per tab possibilmente, e queste saranno magicamente disponibili nel programma principale. Oppure potete scrivere tutto di fila in un solo tab ma ve lo sconsiglio se avete a cuore la vostra sanità mentale.
EDIT: il buon Cico qui sotto nei commenti suggerisce un esercizio col gatto. Approvo e sottoscrivo, sarò felice (anche Cico, immagino :) di vedere le vostre soluzioni.
- Lo so, dovrei vergognarmi…[↑]
- La funzione
random()
usata senza parametri restituisce unfloat
tra 0 e 1, con un parametro restituisce unfloat
tra 0 e il valore del parametro, e con due parametri restituisce unfloat
compreso tra quei due valori. Se qualcuno si stesse chiedendo la distribuzione, come norma generale le sorgenti di numeri pseudo-casuali sono uniformi, se non altrimenti specificato.[↑] - Che, come sappiamo dalla fisica, è un versore, cioè un vettore di lunghezza unitaria, quindi prima lo generiamo in un intervallo quadrato, quindi tra -1 e 1 in entrambe le coordinate, e poi lo normalizziamo, che è l’operazione con cui si riduce un vettore qualsiasi alla lunghezza 1 mantenendone verso e direzione.[↑]
- Utile per svariate ragioni che vedremo più avanti.[↑]
- Qui si potrebbe aprire una lunga parentesi sulla bruttura di queste notazioni quando si potrebbe sommare due vettori usando l’operazione di somma
+
come facciamo coi numeri, ma purtroppo Processing è Java e Java è quello che è. Ci sono linguaggi molto più interessanti, ma molto più complessi e meno adatti ai principianti, che permettono queste magie.[↑] - Seppure una scelta di progetto discutibile, ma per ora va bene così.[↑]
- Preceduto dalla creazione dell’ArrayList di cui sopra, dato che è un oggetto e quindi va creato.[↑]
- Non è la stessa notazione che conosciamo, ma diciamo che quegli array e questi sono implementati diversamente, e Java, e quindi Processing, non permettono quello che in questo caso si chiamerebbe zucchero sintattico, cioè adattare la notazione dei “vecchi” array a questi “nuovi”. Ne riparleremo… un bel po’ più in là.[↑]
- Questa è un’altra stortura dovuta a Java, e il come sia possibile che funzioni lo vedremo tra qualche lezione, ma sono abbastanza convinto che iniziate a capire perché vado in giro a dire che odio Java sebbene l’abbia scelto perché lasciate fare che le alternative ci avrebbero sommerso di talmente tanti dettagli che ci saremmo persi il gusto di programmare, quindi per ora teniamoci Java e le sue peculiarità.[↑]
Cico dice:
Una piccola pignoleria da professorino di matematica: quelle ad essere invertite nel monitor, rispetto al piano cartesiano, non sono le ascisse ma le ordinate.
Poi, visto che stai entrando nel vivo del corso, e stai uscendo dallle nozioni terra terra, alcune osservazioni personali che puoi prendere o lasciare.
Da buon demiurgo hai creato (istanziato) un gatto, ma lo hai lasciato al suo destino senza fargli fare niente. Ciò vuol dire che conosci molto bene i gatti ma dal punto di vista della lezione la cosa mi sembra che resti là appesa. Ad esempio il tuo gatto non miagola mai; io suggerirei agli studenti di svolgere un esercizio in cui il gatto viene messo in una condizione di hungry=true, magari usando un ciclo.
“Un gatto vive mediamente 10 anni e passa la sua vita a fare le cose da gatto.
Tale gatto una volta ogni otto ore diviene affamato, quindi mangia e, satollo, si mette a dormire per 5 ore, il resto del tempo lo passa ad ignorare gli umani.
Lo studente scriva un programma che rappresenti la vita di un gatto.”
Seconda cosa, una tirata di orecchi: stai iniziando a scrivere codice non banale e non c’è una riga di commento… sarà perché il codice è autoesplicativo ;-) (chi non intuisce subito cosa fa una particella che si aggiorna?)
Cico dice:
scusa, volevo dire che il tuo gatto non ha mai fame, non che non miagola mai (se uno vuole fare le pulci deve essere preciso).
Andrea Franceschini dice:
Leggevo “ascisse” e sapevo che c’era qualcosa che non andava, ma non riuscivo a collegare. Corretto :)
La questione del gatto l’ho volutamente lasciata sospesa perché voleva essere più che altro esemplificativa, però mi sono assicurato di mostrare un accesso ad una proprietà e un’invocazione di metodo. L’esercizio che dici tu è valido e incoraggio chi vuole a provarlo.
Circa i commenti, ho una buona ragione, anzi due: nell’economia delle lezioni mi allungano i post che già sono lunghi abbastanza, e quindi ho risolto “commentando” il codice nel post stesso, sotto forma di spiegoni; la seconda ragione è che i commenti sono una cosa talmente importante che meritano una lezione a parte ma cacciare una lezione del genere all’inizio è come se ti dicessi “prima di cominciare a studiare fisica devi spingere su per quella salita un carretto pieno di un quintale di piuma d’oca”: è una fatica che non si giustifica se non dopo molto tempo.
Alessio dice:
Mi sarei aspettato anche una variabile con un enum alive – death – both :D A livello di tutorial è assolutamente inutile ma era una chicca non da poco :D
Andrea Franceschini dice:
Eh, lo so! :) ma non avendo parlato di enum, ancora…