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, 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".
Figura
1 - Lo schema "Kernel-Modulo Dinamico".
(clicca sull'immagne per ingrandirla)
Typeckecking statico: sovraccarico (overloading) e ridefinizione
(overriding)
In Java il controllo dei tipi è effettuato a
tempo di compilazione (tychecking statico), questo significa
che il compilatore si preoccupa di verificare che un
certo oggetto disponga effettivamente del metodo invocato
su di esso. Il controllo avviene sulla base del tipo
della variabile che conterrà il riferimento all'oggetto
e non sul tipo effettivo dell'oggetto stesso (eventualmente
più specifico, cioè un sottotipo) che
sarà noto solo a tempo di esecuzione. Se l'oggetto
puntato dalla variabile durante l'esecuzione ha tipo
più specifico rispetto al tipo della variaile
stessa e per esso è disponibile una ridefinizione
del metodo associato durante la compilazione, il meccanismo
del binding dinamico assicura che quest'ultimo sia quello
utilizzato. In altre parole il binding dinamico permette
di scegliere tra le possibili ridefinizioni di un metodo
quella da applicare. Il punto chiave per comprendere
e riuscire a sfruttare bene dal punto di vista pratico
questo meccanismo è capire cosa si intende esattamente
per ridefinizione. In Java, a differenza di altri linguaggi
orientati agli oggetti, un metodo di una sottoclasse
ridefinisce un metodo di una sua sopraclasse se ha il
suo stesso nome e identica lista di tipi di parametri.
Per approfondire questo tema, consideriamo una prima
semplice variante nello schema KMD precedentemente illustrato.
Supponiamo che il metodo esterno metodoModulo utilizzato
in ClasseKernel abbia un parametro formale c di tipo
ClasseUno e che la classe Modulo ImplUnoModulo disponga,
oltre che dell'implementazione del metodo esterno metodoModulo
con parametro di tipo ClasseUno, anche di un altro metodo
avente sempre nome metodoModulo ma con un parametro
di tipo ClasseDue e il cui prototipo non è presente
in InterfacciaModulo. Tecnicamente si dice che il metodo
metodoModulo in ImplUnoModulo è sovraccarico
(overloaded), si veda la Figura 3.
Figura
2 - Lo schema "Kernel-Modulo Dinamico",
una semplice variante
(clicca sull'immagne per ingrandirla)
Supponiamo,
inoltre, che ClasseDue sia un'estensione di ClasseUno
(ClasseDue è sottotipo di ClasseUno) e consideriamo
il seguente frammento di codice di un metodo della ClasseKernel.
/*
File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseUno c;
...
obj.metodoModulo(c);
...
}
...
}
Consideriamo
l'istruzione obj.metodoModulo(c) dell'esempio. In fase
di compilazione il typechecker verificherà che
essendo obj di tipo InterfacciaModulo e c di tipo ClasseUno,
esista una definizione del metodo metodoModulo che si
applica al caso, cosa che per l'esempio è vera
in quanto esiste proprio un metodo con quel nome e con
parametro formale di tipo ClasseUno nella definizione
di InterfacciaModulo. Osserviamo che nel nostro caso
esistono ben due implementazione dell'interfaccia in
questione, quindi vi sono due differenti implementazione
di metodoModulo applicato a ClasseUno. A tempo di esecuzione
il meccanismo di binding dinamico farà si che
il codice associato alla chiamata sia quello del metodo
ridefinito nella calsse del tipo dell'oggetto puntato
da obj, ad esempio in ImplUnoModulo.
Ma cosa succede se il parametro attuale della chiamata
avesse tipo ClasseDue?
/*
File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseDue c;
...
obj.metodoModulo(c);
...
}
...
}
Sarebbe
ragionevole aspettarsi che durante l'esecuzione, venga
associato alla chiamata il codice del metodo metodoModulo
avente parametro formale di tipo ClasseDue. Questo però
non accade e il metodo selezionato sarà quello
dell'esempio precedente. Il motivo è semplice,
questo secondo metodo nella classe ImplUnoModulo non
è una ridefinizione del prototipo specificato
in InterfacciaModulo e quindi il binding dinamico non
la considera. L'unica maniera per effettuare l'associazione
desiderata sarebbe effettuare un esplicito downcast
durante la chiamata ((ImplUnoModulo)obj).metodoModulo(c).
In questo modo fin dalla fase di compilazione il metodo
con parametro formale di tipo ClasseDue risulterebbe
associato alla nostra chiamata. Un'alternatica a questa
soluzione "sporca" (che tra l'altro non è
consigliabile come soluzione visto che costringe alla
modifica diretta del codice del Kernel!) consiste nell'introduzione
della segnatura di questo nuovo metodo nell'interfaccia
InterfacciaModulo; questo risolverebbe il problema senza
la necessità di un downcast specifico al tipo
della classe Modulo utilizzata.
In questa nuova variante i metodi esterni utilizzabili
nella ClasseKernel e definiti in InterfacciaModulo sono
due, entrambi hanno lo stesso nome ma tipi di parametri
diversi (overloading). I due metodi definiti in InterfacciaModulo
differiscono tra di loro dalla presenza di un parametro
di tipo ClasseUno per il primo e un parametro di tipo
ClasseDue per il sencondo, dove la classe ClasseDue
estende la classe ClasseUno. Ovviamente, in questo caso,
ogni classe Modulo, come nell'esempio ImplUnoModulo
e ImplDueModulo, dovranno necessariamente implementare
entrambi i metodi che ridefiniranno i prototipi in InterfacciaModulo
(overriding). Si noti che, seguendo quanto detto prima
sul concetto di ridefinizione in Java, ognuno dei metodo
nel Modulo ridefinisce il corrispondente prototipo in
InterfacciaModulo, come mostrato in Figura 3.
Figura 3 - Lo schema "Kernel-Modulo Dinamico",
un'altra variante.
(clicca sull'immagne per ingrandirla)
Consideriamo
la seguente ulteriore variante del frammento del codice
di un metodo della ClasseKernel. In questo caso la variabile
che conterrà il parametro attuale è di
tipo ClasseUno ma a tempo di esecuzione sicuramente
farà riferimento ad un oggetto di tipo ClasseDue.
/*
File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseUno c;
...
c = new ClasseDue();
...
obj.metodoModulo(c);
...
}
...
}
Quale
codice viene associato a tempo di esecuzione alla chiamata
dell'istruzione obj.metodoModulo(c)? Il fatto che abbiamo
indicato che l'oggetto effettivamente puntato dal parametro
attuale c a tempo di esecuzione sia un oggetto di tipo
ClasseDue non cambia il ragionamento presentato precedentemente.
Durante la compilazione il compilatore determina il
tipo del parametro attuale c della chiamata (ClasseUno)
e il tipo della variabile obj (InterfacciaModulo). Quindi
verifica che in InterfacciaModulo esista un metodo di
nome metodoModulo con un parametro di tipo ClasseUno
e questo viene trovato. A tempo di esecuzione, per il
meccanismo del binding dinamico, si associa il codice
contenuto in una possibile ridefinizione del metodo
determinato a tempo di compilazione presente nella classe
che definisce tipo reale dell'oggetto puntato da obj.
Poichè per ridefinizione si intende un metodo
con stesso nome e stessa lista di tipi di parametri
il metodo associato a tempo di esecuzione sarà
il metodo metodoModulo della classe ImplUnoModulo con
parametro formale un oggetto di tipo ClasseUno anche
se ituitivamente avremmo pensato a quello con parametro
formale un oggetto di tipo ClasseDue. In pratica per
capire quale metodo verrà usato bisogna considerare
il tipo, a tempo di esecuzione, dell'oggetto su cui
il metodo è invocato; quindi occorre cercare
nella classe così determinata un metodo avente
nome identico a quello invocato e lista dei tipi degli
argomenti corrispondente alla lista dei tipi determinati
a tempo di compilazione per quella chiamata.
Riassumendo, il binding dinamico determina il codice
da associare ad una chiamata di un metodo tenendo conto
del tipo dell'oggetto su cui è invocato il metodo
stesso durante l'esecuzione e del tipo statico associato
ai suoi parametri attuali durante la compilazione. Vi
è quindi un differente trattamento per quello
che è considerato il parametro implicito (l'oggetto
a cui si invia il messaggio) e i parametri espliciti
di un metodo. Tale differenza di trattamento può
spesso causare problemi, infatti è come se avessimo
una chiamata di una funzione metodoModulo(obj, c), con
riferimento all'esempio, in cui il tipo di obj è
determinato dinamicamente e quello di c staticamente!
In Python, un linguaggio di programmazione di scripting
orientato agli oggetti, ad esempio, la scelta di non
introdurre tipi espliciti e non permettere l'overloading
porta a non avere tali problemi; tuttavia al contempo
si perdono tutti i vantaggi, costituiti dall'eliminazione
di tutta una serie di errori a tempo di compilazione
nonchè della "documentazione interna al
programma" illustrati in questi articoli.
Conclusioni
Questo articolo conclude una serie finalizzata ad introdurre
alcuni concetti fondamentali della programmazione ad
oggetti in Java attraverso l'analisi di uno schema di
uso frequente che abbiamo chiamato schema Kernel-Modulo.
Bibliografia
[1] Bruce Eckel - "Thinking in Java", Edizione
Italiana, Apogeo, 2002. http://www.mindview.net/Books
[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] Meilir Page-Jones - "Progettazione a oggetti
con UML", Apogeo, 2002.
Matteo
Baldoni è ricercatore in informatica presso il
Dipartimento di Informatica dell'Università degli
Studi di Torino.
|