Introduzione
Nel precedente articolo abbiamo introdotto le caratteristiche
principali dei linguaggi orientati agli oggetti, ed
in particolare di Java, attraverso lo schema "Kernel-Modulo".
Molte delle classi presenti nelle librerie di Java sono
organizzate secondo questo schema, ad esempio le classi
per la gestione degli eventi nella libreria Swing, le
classi per la realizzazione dei metodi per l'ordinamento
di array e collezioni di oggetti in genere nella libreria
Collection, le classi Observer e Observable. In questo
articolo continuiamo l'analisi di questo schema utilizzandolo
per comprendere meglio le caratteristiche del paradigma
ad oggetti e del linguaggio Java in particolare. Tale
studio ci permetterà di capire l'utilità
del controllo dei tipi statico di Java, contrapposto
al typechecking dinamico di linguaggi di scripting orientati
agli oggetti come Python, come "documentazione
interna del programma".
Lo schema Kernel-Modulo Dinamico
Lo schema "Kernel-Modulo Dinamico" (KMD in
breve), rappresentato in Figura 1, è caratterizzato
da una classe, che chiameremo ClasseKernel che definisce
un metodo, metodoKernel, nel cui corpo è utilizzato
un oggetto (obj) di un'altra classe, che chiamiameremo
InterfacciaModulo, invocando un suo metodo, metodoModulo.
Normalmente la classe InterfacciaModulo è un'interfaccia
implementata da una o più classi, nella figura
ImplUnoModulo e ImplDueModulo. La combinazione delle
classi ClasseKernel e InterfacciaModulo rappresentano
il "Kernel" mentre le classi che implementano
InterfacciaModulo, nell'esempio ImplUnoModulo e ImplDueModulo,
rappresentano i "Moduli". L'utilizzo della
tecnica del binding dinamico risolverà a tempo
di esecuzione il legame tra il nome del metodo esterno,
metodoModulo, e il suo codice selezionandolo il base
all'effettivo tipo dell'oggetto obj su cui è
invocato e non al suo tipo dichiarato a tempo di compilazione.
Questo permette di associare ad una interfaccia più
implementazioni usate anche contemporaneamente, ottenendo
una maggiore flessibilità.
Figura 1 - Lo schema "Kernel-Modulo Dinamico"
/*
File: ClasseKernel.java */
public class ClasseKernel {
private obj: InterfacciaModulo;
public void metodoKernel() {
...
obj.metodoModulo();
...
}
...
}
/*
File: InterfacciaModulo.java */
public interfacce InterfacciaModulo {
public void metodoModulo();
}
/*
File: ImplUnoModulo.java */
public class ImplUnoModulo implements InterfacciaModulo
{
...
public void metodoModulo() {
...
}
...
}
Type
checking statico come documentazione interna al programma
La componente Kernel nello schema KMD può essere
compilata separatamente dalla componente Modulo. Solitamente
la componente Kernel è fornita come libreria
ed il programmatore completa lo schema fornendo il proprio
o i propri Moduli. A differenza di altri linguaggi orientati
agli oggetti, come Python, il controllo dei tipi in
Java è eseguito a tempo di compilazione (typechecking
statico). Quindi durante la compilazione del Kernel
il compilatore si preoccupa di verificare che l'oggetto
obj di tipo InterfacciaModulo utilizzato nella classe
ClasseKernel disponga effettivamente del metodo metodoModulo
invocato su di esso. Nel caso che InterfacciaModulo
sia un'interfaccia o una classe astratta, e quindi metodoModulo
in tale classe è una semplice dichiarazione di
segnatura (o prototipo), Java si riserverà di
controllare durante la compilazione di una componente
Modulo che metodoModulo venga effettivamente implementato
in modo da garantire a tempo di esecuzione l'esistenza
del codice da associare al nome del metodo. In altre
parole , in Java spesso si ricorre al meccanismo di
ereditarietà per stabilire un'interfaccia comune
piuttosto che per ereditare effettivamente del codice,
InterfacciaModulo nello schema KDM ne è un esempio.
L'uso di tali interfacce provvede una forma di documentazione
"interna al programma", i metodi che devono
essere realizzati da una classe per potersi definire
una componente Modulo per un certo Kernel sono quelli
specificati in InterfacciaModulo. La corretta compilazione
della componente Kernel garantisce che tutti i riferimenti
a metodi esterni siano stati "documentati"
nell'interfaccia che fa parte del Kernel mentre la corretta
compilazione di una componente Modulo garantisce che
quest'ultima fornisca un'implementazione per ogni metodo
esterno utilizzato nel Kernel.
In altri linguaggi orientati agli oggetti, come ad esempio
Python, il controllo dei tipi è effettuato a
tempo di esecuzione e così pure il controllo
dell'esistenza di un metodo associato ad un certo oggetto
(typechecking dinamico). Per questo motivo i progettisti
di Python non hanno sentito la necessità di introdurre
un costrutto del linguaggio per definire interfacce
o classi astratte. Nel caso di Python lo schema Kernel-Modulo
si presenta come in Figura 2, cioè priva dell'interfaccia
InterfacciaModulo.
Figura
2 - Lo schema "Kernel-Modulo Dinamico"
in Python
#
File: ClasseKernel.py
class ClasseKernel:
...
def metodoKernel():
...
(self.obj).metodoModulo()
...
...
#File:
ImplUnoModulo.py
class ImplUnoModulo:
...
def metodoModulo():
...
...
Le
classi Modulo non condividono più un'interfaccia
comune ed in generale non è garantita alcuna
relazione tra esse. L'unica relazione che dovrebbe accumunarle
è l'implementazione del metodo esterno utilizzato
in ClasseKernel che però è descritto,
documentato, esclusivamente all'interno del codice stesso
della classe. L'unico modo per aiutare i realizzatori
delle classi Modulo è di fornire una documentazione
che accompagni il codice del Kernel, una "documentazione
esterna al programma" che descriva le segnature
dei metodi richiesti dal Kernel. Ovviamente all'atto
dell'esecuzione non c'è nessuna garanzia che
l'implementazione di un certo Modulo rispetti le richieste
del Kernel e quindi nessuna garanzia che a tempo di
esecuzione non si verifichino errori. Il controllo statico
dei tipi permette invece di eliminare in fase di compilazione
questa classe di errori, assai frequenti in fase si
sviluppo.
L'altra faccia della medaglia: downcasting
Nel precedente articolo abbiamo sottolineato la natura
polimorfa delle variabili definite in Java: una variabile
di un certo tipo può contenere i riferimenti
ad oggetti dello stesso tipo o di un sottotipo (ma mai
di un sovratipo). Si consideri lo schema KMD supponendo
che la classe Modulo ImplUnoModulo disponga oltre all'implementazione
del metodo esterno metodoModulo utilizzato in ClasseKernel
anche del metodo altroMetodo il cui prototipo non è
presente in InterfacciaModulo (si veda la Figura 3).
Figura 3 - Lo schema "Kernel-Modulo Dinamico
Dalla
nostra precedente introduzione abbiamo chiarito il fatto
che in Java viene verificata la validità di una
invocazione di metodo su di un oggetto durante la fase
di compilazione. Questo fa si che non conoscendo effettivamente
il tipo di oggetto a cui una certa variabile farà
riferimento durante l'esecuzione, l'unico criterio accettabile
per verificare la correttezza della chiamata sarà
quello di basarsi sul tipo della variabile stessa. Consideriamo
il seguente frammento di codice di un metodo della ClasseKernel.
/*
File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public unMetodoInKernel() {
...
// quello qui di seguito è un upcast
obj = new ImplUnoModulo();
// la seguente istruzione genera un errore a tempo di
compilazione
obj.altroMetodo();
// quello qui di seguito è un downcast
((ImplUnoModulo)obj).altroMedoto();
...
}
...
}
Nell'esempio
l'istruzione obj.altroMetodo() causerà un errore
di compilazione poichè la variabile obj ha tipo
InterfacciaModulo e non esiste nessun metodo di nome
altroMetodo per tali tipo di oggetti (sebbene sia facile
capire dal codice del programma che durante l'esecuzione
tale variabile farà sicuramente riferimento ad
un oggetto di tipo ImplUnoModulo). In questo caso sarà
necessario "rassicurare" il compilatore che
a tempo di esecuzione quella data variabile conterrà
un oggetto di tipo ImplUnoModulo effettuando un donwcast
nel seguente modo ((ImplUnoModulo)obj).altroMetodo().
In altri termini il downcast può essere considerato
come un modo per ovviare taluni limiti del typechecking
statico che, ovviamente, non può conoscere cosa
avverrà a tempo di esecuzione.
Analizziamo un altro possibile frammento di codice di
un altro possibile metodo della ClasseKernel e supponiamo
che InterfacciaModulo sia una classe anzichè
una interfaccia e quindi possibile creare istanze, oggetti,
di essa (si noti che in questo caso la classe ImplUnoModulo
estende la classe InterfacciaModulo anzichè implementarla).
/*
File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public unAltroMetodoInKernel() {
...
// Un array di elementi di tipo InterfacciaModulo che
a tempo di
// esecuzione saranno effettivamente di tipo ImplUnoModulo
InterfacciaModulo[] array = new ImplUnoModulo[10];
// Staticamente il tipo di array[0] è InterfacciaModulo
ma
// dinamicamente avrà di tipo ImplUnoModulo e
la seguente
// istruzione solleverà un'eccezione a run-time!!
array[0] = new InterfacciaModulo();
}
...
}
In
questo caso viene dichiarata una variabile array di
tipo array di elementi di tipo InterfacciaModulo ma
sarà inizializzata, a tempo di esecuzione, con
un oggetto array di elementi (dieci) di tipo ImplUnoModulo.
Questo è possibile poichè in Java gli
array sono oggetti e un array di oggetti di tipo ImplUnoModulo
è anche un array di oggetti di tipo InterfacciaModulo
(ImplUnoModulo estende InterfacciaModulo). L'istruzione
di assegnamento al primo elemento di array, array[0]
= new InterfacciaModulo() a tempo di compilazione non
desta nessun problema poichè il compilatore assume
che ogni elemento dell'array stesso sia una variabile
di tipo InterfacciaModulo. Purtroppo però, in
fase di esecuzione, verrà sollevata un'eccezione
di tipo ArrayStoreException
perchè quella che si credeva, a tempo di compilazione,
una variabile di tipo InterfacciaModulo è in
realtà una variabile di tipo ImplUnoModulo non
adatta quindi a contenere riferimenti ad oggetti di
sovraclassi. Questo tipo di errore non è evitabile
con un cast di qualche tipo e quindi evidenzia come
non tutti gli errori di tipo possano essere catturati
o ovviati dal typecheching statico.
Figura
4 - Il problema degli array
Conclusioni
In questo articolo abbiamo visto i vantaggi del typechecking
statico di Java rispetto a quello dinamico di linguaggi
tipo Python ma anche alcuni esempi in cui l'effetto
del typechecking statico risulta controintuitivo. Nel
prossimo articolo tratteremo più in profontità
overriding e overloading.
Bibliografia
[1] Bruce Eckel - "Thinking in Java", Edizione
Italiana, Apogeo, 2002. http://www.mindview.net/Books
[2] Bruce Eckel- "Thinking in Python", http://www.mindview.net/Books,
2001. In preparazione.
[2] Cay S. Horstmann e Gary Cornell - "Java 2:
i fondamenti", McGraw-Hill, 2001.
[3] Cay S. Horstmann e Gary Cornell - "Java 2:
tecniche avanzate", McGraw-Hill, 2000 .
[4] Mark Pilgrim. "Dive Into Python". http://diveintopython.org
[5] Python. http://www.python.org
Matteo Baldoni è
ricercatore in informatica presso il Dipartimento di
Informatica dell'Università degli Studi di Torino.
|