Palle che girano attorno ad altre palle

È passato un po’ di tempo dall’ultima lezione in cui avevamo parlato di design pattern e commenti. Questa volta torniamo a parlare di oggetti con una di quelle loro caratteristiche fondamentali che senza uno non si pone il problema, ma con uno si chiede come faceva prima senza. Parliamo di polimorfismo, e in particolare di ereditarietà.

Abbiamo detto che gli oggetti ci servono per lavorare con concetti astratti ma fin’ora, tra gatti e particelle, ne abbiamo visti di molto concreti. In realtà questi due esempi sono concreti o astratti a seconda del lato da cui li si guarda. Per esempio di gatti c’è una grande varietà di razze, ma tutti i gatti sono mammiferi che sono a loro volta vertebrati, animali, e via così. La parte interessante del ragionamento è che, se parliamo di mammiferi, questi hanno una serie di caratteristiche comuni, tipo le zampe e la voce [1], ma non tutti sono interamente ricoperti di pelo, per esempio. Comunque, prima di mettermi in ulteriore imbarazzo biologico, assumiamo l’esempio classico in cui un animale ha una certa età, fa dei versi, mangia se ha fame, e dorme se ha sonno. Con una classe lo descriveremmo così:

class Animal {
  int age;
  bool hungry;
  bool sleepy;

  Animal() {
    age = 0;
    hungry = false;
    sleepy = false;
  }

  void eat() {
    if(hungry) {
      println("I'm eating.");
      hungry = false;
    }
  }

  void sleep() {
    if(sleepy) {
      println("I'm sleeping.");
      sleepy = false;
  }

  void vocalise() {
    println("I'm vocalising.");
  }
}

che è pericolosamente simile alla classe Cat che avevamo visto nella prima lezione dedicata agli oggetti. La bellezza dell’ereditarietà è che ora possiamo creare la classe che rappresenta il gatto, dichiarare che è un animale, e dormire sugli allori.

class Cat extends Animal {

}

Quando una classe dichiara di estenderne un’altra tramite la parola chiave extends, quello che succede è che la classe in questione eredita tutti i metodi e le proprietà della classe estesa [2]. Per questo se ora istanziamo la classe Cat e ci chiamiamo un metodo, otterremo l’effetto di chiamare il corrispondente metodo della classe Animal.

Cat c = new Cat();
c.vocalise();

>> I'm vocalising.

Per quanto sappia che anche voi odiate UML quanto lo odio io, qualcosa di buono c’è anche in lui, nascosto sotto la montagna di simboli privi di intuitività e diagrammi inutilmente ridondanti. Questo caso si rappresenta così: una freccia con la punta chiusa e vuota che tocca la classe da estendere, e la linea unita che parte dalla classe che estende [3]:

Il diagramma UML delle classi Animal e Cat

Comunque, se tutto il giochino si riducesse a questo, non sarebbe molto interessante: di fatto abbiamo creato una copia della classe Animal che si chiama Cat.

Quello che lo fa diventare interessante è il polimorfismo. Il polimorfismo è un concetto dalle molte facce profondamente radicato nella programmazione orientata agli oggetti ed è improbabile che si riesca ad esaurirlo in una o due lezioni, quindi procediamo per piccoli passi: questa volta vedremo l’aspetto che in gergo si chiama method override, ossia la sovrascrittura di un metodo. L’override si attua reimplementando un metodo della classe estesa all’interno della classe che la estende, normalmente per fargli fare qualcosa di diverso. Nel nostro caso, per esempio, non è che i gatti parlano, e quindi potremmo scrivere

class Cat extends Animal {
  void vocalise() {
    println("MEOW!");
  }
}

che ha una certa dose di senso in più. A questo punto, ogni volta che istanzieremo la classe Cat e chiameremo il metodo vocalise(), il codice che verrà eseguito sarà proprio quello contenuto nella classe Cat. Come regola generale, se chiamiamo un metodo in una classe, prima il computer cercherà un’implementazione all’interno della classe stessa, se non la trova andrà a cercarla all’interno della classe da lei estesa, la cosiddetta superclasse [4], e se non la trova nemmeno lì risale di un altro livello nella gerarchia finché non trova un’implementazione. E se non la trova? In questo caso non c’è molto da fare, il programma termina con un errore [5].

I più attenti tra voi avranno notato che un Animal non ignora gli umani, e quindi neanche il Cat. Oltre a sovrascrivere i metodi, possiamo anche aggiungerne di nuovi [6] e quindi

class Cat extends Animal {
  void vocalise() {
    println("MEOW!");
  }

  void ignoreHumans() {
    if(hungry) {
      vocalise();
    }
  }
}

e il gioco è fatto.

Detto ciò, è venuta l’ora di guardare al codice di questa lezione. La prima versione non è molto diversa da quella della lezione precedente, ho apportato qualche lieve modifica qui e là che potete divertirvi a cercare e capire. Se eseguite il tutto, si comporterà un po’ diversamente ma sostanzialmente nello stesso modo che nella lezione precedente.

Nella seconda versione del codice ho aggiunto una nuova classe, MagParticle

class MagParticle extends Particle {
  float magnitude;

  MagParticle() {
    super();
    colour = color(210, 100, 100);
    magnitude = 1.0f;
  }

  void applyForce(PVector force) {
    acceleration.set(new PVector(0, 0));
  }

  void update() {
    magnitude = 0.01*radius;
  }

  void draw() {
    noStroke();
    fill(colour);
    ellipse(position.x, position.y, radius*2, radius*2);
  }
}

Questa è una particella speciale: vogliamo che sia ferma nello spazio e funzioni da attrattore per le altre particelle, quindi le facciamo estendere la classe Particle per ereditarne le proprietà, e ne rimpiazziamo i metodi in modo che faccia quello che vogliamo: se per caso le applicassimo una forza, questa non avrebbe effetto, e ogni volta che chiamiamo il metodo update(), la magnitudo viene aggiornata a seconda del raggio [7]. Avrei potuto lasciare stare il metodo draw() perché… beh, perché è sostanzialmente identico a quello di Particle.

Un’ultima cosa. Nel costruttore MagParticle() c’è una funzione che non è definita da nessuna parte: super(). In realtà si tratta di una parola chiave che serve a chiamare il metodo corrispondente all’interno della superclasse, quindi in questo caso l’effetto è di chiamare il costruttore Particle(). Il motivo di ciò è che altrimenti avremmo dovuto riscrivere tutto il codice che stava nel costruttore di Particle, mentre così prima lo eseguiamo e poi procediamo con le nuove inizializzazioni, in questo caso un diverso colore di default e la magnitudo.

Le modifiche al ParticleController sono abbastanza facili da individuare: ho aggiungo un ArrayList per le particelle magnetiche, e ho modificato il codice nel metodo update() in modo che queste attirino le altre particelle (righe 64-72), e che le particelle si attraggano tra loro invece che respingersi (riga 101). Eseguiamo e… magia!

La versione finale del codice contiene alcune modifiche estetiche per disegnare le scie delle particelle bianche ma, dato che non è interessante dal punto di vista della lezione, ve la lascio da esplorare. Altra cosa che potete provare è giocare coi parametri del ParticleController per controllare il numero di particelle e attrattori: io li ho messi a caso ad ogni esecuzione ma voi potete sperimentare, escono risultati carini.

Insomma, questa volta abbiamo cominciato a vedere un po’ di polimorfismo e abbiamo aggiunto ancora un po’ di astrazione ai nostri oggetti. Questa cosa ci prenderà un certo numero di lezioni, e in parallelo vedremo altri design pattern e altre cose interessanti.

  1. Con buona pace della correttezza scientifica e tassonomica.
  2. Non è proprio così semplice ma per il momento facciamo finta di sì.
  3. Lo so, neanche per me ha senso, ma che ci volete fare…
  4. Non nel senso che è più bella della classe che la estende ma nel senso che è quella che ci “sta sopra” o che la “contiene”, ma ne riparleremo.
  5. In teoria. In pratica, più sì che no, il compilatore è in grado di accorgersene e quindi interrompe la compilazione e ci avvisa del problema, ma ci sono casi limite in cui questo non può avvenire e quindi il programma termina con un errore.
  6. No, non possiamo eliminarne di vecchi per svariate ragioni, ma possiamo sovrascriverli con implementazioni “nulle” se vogliamo bloccare la risalita gerarchica.
  7. Quindi in teoria potremmo cambiarne il raggio per cambiarne la forza di attrazione, il che non è molto sorprendente, concettualmente.

Commenti

Rispondi