MokaByte 67 - 8bre 2002 
Un'introduzione al paradigma ad oggetti attraverso lo schema Kernel-Modulo
II parte

di
Matteo Baldoni
Dopo aver introdotto uno degli schemi pił ricorrenti con cui sono organizzate le classi nelle librerie di Java analizzeremo il typechecking statico contrapposto a quello dinamico di linguaggi come Python

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.

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it