Tecnologie a confronto: Polimorfismo ed ereditarietà multipla
Le potenzialità del poliformismo e dell'ereditarietà multipla.

Tecnologie a confronto: Polimorfismo ed ereditarietà multipla
Polimorfismo
Con il termine polimorfismo in informatica si intende la capacità di un’espressione di assumere valori di diversi tipi.
Nel mondo della programmazione ad oggetti un metodo di una classe X è polimorfico se può assumere valori definiti in una classe Y, sottoclasse di X. Il polimorfismo così definito prende il nome di polimorfismo per inclusione ed è uno dei punti cardini di tutti i linguaggi orientati agli oggetti.
Un esempio di polimorfismo per inclusione
Ereditarietà Multipla
Con il termine ereditarietà multipla si intende invece, nei linguaggi ad oggetti, la capacità di una classe di ereditare funzionalità da più di una superclasse.
L’ereditarietà multipla rappresenta da sempre una delle principali armi a doppio taglio del mondo dello sviluppo software e tutti i principali linguaggi di programmazione hanno dovuto farci i conti e trovare una soluzione per mantenere stabilità e consistenza.
Da un punto di vista puramente teorico la possibilità di ereditare da più classi sembrerebbe basilare. Vediamo un esempio che ne dimostri le potenzialità:
Immaginiamo di avere una classe SoccerPlayer che espone i metodi getRole e getGoals per ottenere rispettivamente il ruolo ed il numero di goal del giocatore.
Parallelamente avremo una classe Vip con i metodi getClicks e getNews per ottenere il numero di click e le ultime novità sul vip in questione.
Avrebbe quindi senso poter avere una classe VipPlayer che rappresenti quei calciatori che sono anche vip.
Grazie all’ereditarietà multipla sarebbe sufficiente far si che VipPlayer estenda sia SoccerPlayer che Vip per ottenere gratuitamente tutte le funzionalità di entrambe le classi.
Problema del diamante
Purtroppo l’ereditarietà multipla oltre a molti benefici porta, soprattutto, a molte problematiche. Prima tra queste il problema del diamante.
Rifacendoci all’esempio precedente immaginiamo che sia SoccerPlayer che Vip estendano dalla stessa classe Person. Si verrebbe a creare una situazione di questo genere:
Cosa succede se viene invocato il metodo VipPlayer.speak(); ? Da quale classe base viene eseguito? Ed in particolare: cosa succede se SoccerPlayer e Vip presentano due differenti implementazioni di tale metodo?
Tale problema prende il nome proprio di problema del diamante a causa della forma del diagramma che lo rappresenta. Per far fronte a questa problematica i principali linguaggi di programmazione hanno introdotto funzionalità e primitive, in modo da poter andare a limitare le potenzialità dell’ereditarietà multipla.
Java
Java è da sempre uno dei linguaggi di programmazione più vincolanti nella scrittura del codice, in esso molte “libertà” sono sacrificate a favore di una maggiore stabilità e della possibilità di ridurre al minimo gli errori a runtime.
Per questo motivo il supporto all’ereditarietà multipla è sempre stato assolutamente parziale.
In Java una classe può ereditare da una sola classe base, ed il concetto di ereditarietà multipla è possibile solo grazie all’uso delle interfacce. Un interfaccia permette di definire la “forma” di una classe, indicando nomi di metodi, argomenti, tipi restituiti e proprietà, ma senza permettere alcuna implementazione. Saranno poi le classi che ne faranno uso a dover implementare la logica per ciascun metodo definito nell’interfaccia. Questa soluzione permette di risolvere il problema alla radice, in caso di ereditarietà multipla il metodo sarà comunque sempre implementato nella classe eliminando così problemi di ambiguità.
Il diagramma precedente può quindi essere rivisto in questo modo:
È facile però notare come questo diagramma non presenta tutte le possibilità che presentava il precedente. Se ora volessimo istanziare un vip, che però non è anche giocatore di calcio, non potremmo farlo. Di conseguenza per poter raggiungere lo stesso valore funzionale precedente saremmo obbligati a realizzare una serie di classi aggiuntive, non strettamente relazionate tra di loro.
Vantaggi
Seppur con molti limiti questo approccio garantisce le basi per l’ereditarietà multipla. Con un esempio è facile mostrare il vero punto di forza delle interfacce.
public class Match {
//...
public void setBestPlayer(SoccerPlayer pl){
bestPlayer = pl;
}
//...
}
//...
Match m = new Match();
VipPlayer p = new VipPlayer('Messi');
m.setBestPlayer(p);
L’esempio dimostra in breve come sia possibile dichiarare un metodo che riceva in input un parametro il cui tipo è un’interfaccia (SoccerPlayer) e come sia possibile invocare tale metodo passandogli l’istanza di una classe che implementa tale interfaccia.
Problemi
Nonostante questa imposizione forte possono però sorgere dei conflitti.
interface I1 {
public void something();
}
interface I2 {
public int something();
}
public class Cls implements I1, I2 {
}
Nell’esempio I1 ed I2 definiscono entrambe il metodo something, ricevente gli stessi parametri ma con tipo di ritorno differente. Siccome Cls le implementa entrambe il compilatore Java segnalerà un errore, in quanto non è possibile che due metodi della stessa classe abbiano lo stesso nome e gli stessi parametri di input.
Java 8
Con Java 8 è stata introdotta la possibilità di dichiarare delle implementazioni di default per metodi definiti all’interno di un’interfaccia.
interface SoccerPlayer {
default public int getGoals(){
return this.goals;
}
}
public class VipPlayer implements SoccerPlayer {}
VipPlayer p = new VipPlayer();
p.getGoals();
Questo permette di risparmiare parecchie classi intermedie e di fare un passo avanti verso il concetto di ereditarietà multipla.
Ovviamente anche in questo caso se si implementano più interfacce che definiscono lo stesso metodo, verrà generato un errore in compilazione.
Questo approccio è particolarmente utile per la generalizzazione dei metodi. Immaginiamo di voler aggiungere un metodo ad un’interfaccia: con Java 7 avremmo dovuto riscrivere quel metodo in ogni classe che implementa l’interfaccia stessa, in Java 8 invece l’impatto è molto inferiore, dovendo modificare solo l’interfaccia stessa.
Java dalle sue origini supporta quindi l’ereditarietà multipla “di tipo”, mentre con l’introduzione di Java 8 è supportata anche quella “di comportamento”; nonostante questo però ancora non è supportata l’ereditarietà multipla “di stato”.
Scala
In Scala l’ereditarietà multipla è decisamente più supportata rispetto a Java ed è ottenibile tramite l’uso dei traits.
I traits sono simili alle interfacce Java ma permettono l’implementazione dei metodi. Una delle grandi potenzialità dei traits è però quella di poter essere eseguiti in cascata, ottenendo come valore della superclasse il risultato della cascata fino a quel punto e permettendo a più trait di ridefinire lo stesso trait base.
Ecco un esempio per mostrare le potenzialità dei traits
trait Info {
def getInfo = ""
}
abstract class Animal extends Info {
val animalType: String
override def getInfo = animalType
}
trait Omnivorous extends Info {
override def getInfo = super.getInfo + " is Omnivorous"
}
trait Dangerous extends Info {
override def getInfo = super.getInfo + " and also very dangerous!"
}
class Bear extends Animal with Omnivorous with Dangerous {
val animalType = "Bear"
}
class Fire extends Dangerous {
override def getInfo = "Fire is hot " + super.getInfo
}
object AnimalApp {
def main (args: Array[String]) = {
println(new Bear().getInfo)
println(new Fire().getInfo)
}
}
/*
* Stampa
* Bear is Omnivorous and also very dangerous!
* Fire is hot and also very dangerous!
*/
Come vediamo dall’esempio la priorità tra metodi con la stessa signature è definita dall’ordine di inclusione. Per primo viene chiamato quello della classe principale, poi quello della classe estesa ed infine quello delle interfacce in ordine di definizione tramite la parola chiave with.
All’interno dei metodi il valore ritornato da super.getInfo non è mai la chiamata al metodo diretto della superclasse, bensì il risultato della catena di chiamate avvenuta prima della corrente.
L’esempio porta alla luce proprio la stessa situazione del problema del diamante. Info è la classe base dalla quale estendono Animal, Omnivorous e Dangerous. Infine Animal estende contemporaneamente da tutte e 3. La problematica del diamante è quindi brillantemente risolta grazie alla definizione delle priorità. Inoltre l’approccio di scala consente anche allo sviluppatore se scegliere l’esecuzione di tutti i metodi (super) oppure solo dell’ultimo.
PHP
Dalla versione 5.4 anche PHP ha introdotto il supporto ai traits. L’uso dei traits in PHP è del tutto analogo a quello che abbiamo trovato in scala, con la possibilità dei metodi contenuti all’interno dei traits di chiamare la superclasse ed ottenere il risultato dello stack fino a quel momento.
<?php
class Animal {
public $animalType;
public function getInfo(){
return $this->animalType;
}
}
trait Omnivorous {
public function getInfo(){
return parent::getInfo() . " is Omnivorous";
}
}
class Bear extends Animal {
use Omnivorous;
public $animalType = "Bear ";
}
$bear = new Bear();
echo $bear->getInfo(); //Bear is omnivorous
Ci sono però alcuni limite e differenze tra i traits di PHP e quelli di Scala.
In PHP è possibile avere solo in cascata la classe principale ed un solo trait. Se più trait dovessero implementare lo stesso metodo sarà la classe principale a dover risolvere l’ambiguità come segue
<?php
class Animal {
public $animalType;
public function getInfo(){
return $this->animalType;
}
}
trait Omnivorous {
public function getInfo(){
return parent::getInfo() . " is Omnivorous";
}
}
trait Dangerous {
public function getInfo(){
return parent::getInfo() . " is Dangerous";
}
}
class Bear extends Animal {
use Omnivorous, Dangerous {
Dangerous::getInfo insteadof Omnivorous;
}
public $animalType = "Bear ";
}
$bear = new Bear();
echo $bear->getInfo(); //Bear is dangerous
Allo stesso tempo però è possibile usare degli alias per permettere ad una classe di avere entrambi i metodi a disposizione.
class Bear extends Animal {
use Omnivorous, Dangerous {
Omnivorous::getInfo as omnivorousInfo;
Dangerous::getInfo as dangerousInfo;
}
public $animalType = "Bear ";
public function getInfo(){
return $this->omnivorousInfo() . $this->dangerousInfo();
}
}
$bear = new Bear();
echo $bear->getInfo(); //Bear is omnivorousBear is dangerous
L’effetto è buono ma presenta ancora un problema: entrambe le funzioni continuano a chiamare la parent class, in questo caso Animal, replicando quindi l’output per 2 volte.
Infine c’è da sottolineare come essendo un linguaggio interpretato PHP permetterebbe di definire una chiamata a parent dentro un trait anche se in concreto il reale parent non dovesse esporre quel metodo. Nel caso il metodo dovesse poi venire invocato ci si troverebbe di fronte ad un runtime error.
In conclusione l’ereditarietà multipla è ben supportata in PHP, ma non è completa come visto in precedenza per Scala.
C++
L’ultima sezione di questa guida è dedicata a C++. Questo linguaggio merita uno spazio in quanto è l’unico linguaggio che supporta nativamente l’ereditarietà multipla senza la necessità di strutture dati addizionali.
//I costruttori sono omessi per brevità
class SoccerPlayer {
private: int goals;
int getGoals() {
return goals;
}
};
class Vip {
private: int news;
int getNews() {
return news;
}
};
class VipPlayer: public SoccerPlayer, public Vip {};
int main(){
VipPlayer vp('Messi');
cout << vp.getNews();
return 0;
}
La classe VipPlayer estende fisicamente dalle classe SoccerPlayer e Vip. Ma ovviamente questa soluzione è tanto potente quanto pericolosa, in quanto possiamo ricadere facilmente nei problemi di ambiguità e diamante.
Il linguaggio permette di gestire l’ambiguità nella classe che estende oppure nell’uso dell’istanza, specificando esplicitamente quale superclasse chiamare
vp.Vip.speak(); //Messi è un vip
vp.SoccerPlayer.speak(); //Messi è un giocatore di calcio
Ma questa soluzione non è esaustiva e soprattutto rende il codice particolarmente complesso, illeggibile e difficilmente manutenibile.
Linguaggi più moderni, come lo stesso C# di casa Microsoft, hanno abbandonato questa soluzione a favore di un più consono approccio ad interfacce.
Realizziamo qualcosa di straordinario insieme!
Siamo consulenti prima che partner, scrivici per sapere quale soluzione si adatta meglio alle tue esigenze. Potremo trovare insieme la soluzione migliore per dare vita ai tuoi progetti.