Articolo scritto da Alessio Saltarin il 31/07/2008
Pagina 1 di 2
Il polimorfismo è una tecnica di programmazione che consente l'utilizzo di parti di codice sorgente che, pur rimanendo inalterate, generano a run-time comportamenti diversi.
Creare codice polimorfico ha un significato preciso nella programmazione orientata agli oggetti: significa creare una tassonomia di classi che implementano tutte una stessa interfaccia.
Perciò se, ad esempio, la mia interfaccia definisce un metodo "calcolaArea", ogni classe che implementerà questa interfaccia avrà sicuramente un metodo "calcolaArea": questo ci permette di scrivere metodi polimorfici, metodi cioè in grado di cambiare il proprio algoritmo di esecuzione a seconda del tipo di oggetto che viene passato come argomento.
Il polimorfismo nei linguaggi OOP tradizionali
In Java - ma lo stesso discorso vale per il C++ e per qualsiasi altro linguaggio Object Oriented (OO) compilato, vedremo poi invece il caso di Ruby - ad esempio:
interface IFormaGeometrica
{
void calcolaArea();
}
public class Triangolo implements IFormaGeometrica
{
@Override
public int calcolaArea()
{
return (this.base*this.altezza)/2;
}
}
In questo caso definiamo l'interfaccia
IFormaGeometrica che stabilisce che ogni oggetto che "è" una FormaGeometrica avrà un metodo
calcolaArea- ad esempio la classe Triangolo, che è una FormaGeometrica, ha una sua implementazione di calcolaArea; il che ci permette di scrivere un programma in grado di calcolare le aree di qualsiasi forma geometrica, sia che questa sia presente oggi nel codice, sia che verrà implementata in futuro, senza cambiare il codice sorgente originale.
Infatti, se scrivo una classe Calcolatore:
public final class Calcolatore
{
public static void main(String[] args)
{
Collection<IFormaGeometrica> forme =
new ArrayList<IFormaGeometrica>();
forme.add(new Triangolo());
forme.add(new Quadrato());
forme.add(new Pentagono());
for (IFormaGeometrica g : forme)
{
System.out.println(g.calcolaArea());
}
}
}
questa può prendere come input una qualsiasi collezione di forme geometriche, a patto che ogni oggetto presente nella collezione implementi l'interfaccia
IFormaGeometrica, e che cioè abbia in sostanza un metodo
calcolaArea. Questo esempio in Java è puramente accademico: in realtà, probabilmente nel costruttore di ogni singola classe, dovremo prevedere in input le misure dei lati della forma geometrica, l'apotema e così via.
L'obiettivo è raggiunto: abbiamo scritto una classe in grado di stampare a video l'area di qualsiasi forma geometrica. Ripeto: sia che nel codice questa sia già stata implementata (come nel caso di Triangolo), sia che venga implementata in futuro.
Non solo: se l'implementazione del calcolo dell'area del Triangolo contenesse un bug, io posso modificare la classe
Triangolo senza dover riscrivere la classe
Calcolatore. Magari quest'ultima è stata deployata su un server che necessita di riavvio ad ogni modifica: in questo caso non dover cambiare il codice è un vantaggio notevole. Ma a parte questo, è sempre vantaggioso limitare le parti di codice che "cambiano" perchè ogni modifica porta con sè potenziali bug. Viceversa, è una buona norma di programmazione sapere sempre con certezza quali sono le parti di codice che rimangono invariate.
Quello che accade "dietro le scene" è che il compilatore si assicura che ogni oggetto all'interno del codice polimorfico del metodo
main della classe
Calcolatore implementi l'interfaccia. In questo modo, si dice usando un'espressione tratta dalla metodologia di
design by contract, "il contratto è rispettato".
Se non lo fosse, se cioè in "forme" avessimo un oggetto di una classe che non implementa
IFormaGeometrica avremmo un errore a
compile time, ovvero non arriveremmo nemmeno a far girare il programma, perchè il compilatore si accorgerebbe dell'errore.
Il polimorfismo in Ruby
Ma cosa accade in Ruby? E' possibile in Ruby, che non è un linguaggio compilato ma interpretato, scrivere metodi polimorfici?
Sì, è certamente possibile, ma esiste una profonda differenza "filosofica" rispetto ai linguaggi OO e compilati, che
potremmo descrivere così. In Java e nella programmazione OO classica, per stabilire che un oggetto appartiene ad una determinata tipologia di oggetti (cioè implementa una certa interfaccia) occorre esplicitamente far derivare l'oggetto da una classe padre: in sostanza occorre usare le tecniche dell'ereditarietà (ereditando da una classe, da una classe astratta o da un'interfaccia).
Sarebbe come dire: per stabilire se questa che ho davanti è un'anatra, prendo il suo DNA e lo studio in laboratorio per vedere se è proprio quello di un'anatra.
In Ruby invece si utilizza il "test dell'anatra" (duck test) inventato da James Riley (vedi
http://en.wikipedia.org/wiki/Duck_typing):
se cammina come un'anatra e starnazza come un'anatra, allora è un'anatra.
(il che è incidentalmente ciò che fa ognuno di noi quando vede un'anatra).
Cosa vuol dire questo? Vuol dire che in Ruby, e più in generale nei linguaggi di scripting orientati agli oggetti, come anche Python e Perl, non occorre specificare l'interfaccia, nè esplicitare relazioni di ereditarietà tra le classi.
Semplicemente l'interprete "si fida" del fatto che il programmatore, qualora passi al metodo polimorfico un oggetto che "dovrebbe avere" un certo metodo, in realtà ce l'abbia.