Introduzione
La programmazione orientata agli oggetti ha avuto una
notevole diffusione negli ultimi anni, questo è
merito soprattutto del linguaggio Java e della sua vasta
libreria di programmi disponibili. Molte delle classi
presenti in queste librerie sono organizzate secondo
uno schema ricorrente. Tale schema sfrutta completamente
le caratteristiche principali dei linguaggi orientati
agli oggetti, l'astrazione sui dati, il binding dinamico,
il polimorfismo, l'overriding e l'ereditarietà.
Un'analisi di questo schema è utile per acquisire
una maggiore consapevolezza dell'uso dei linguaggi orientati
agli oggetti, a comprendere perchè sia facile
e conveniente realizzare librerie per tali linguaggi.
Lo schema Kernel-Modulo
Uno schema ricorrente dell'organizzazione di classi
nelle librerie di Java è quello rappresentato
nella Figura 1. Una classe (che chiameremo ClasseKernel)
definisce un metodo (metodoKernel) nel cui corpo si
utilizza un oggetto (obj) di un'altra classe (InterfacciaModulo)
invocando un suo metodo (metodoModulo). Molto spesso
la classe dell'oggetto utilizzato, InterfacciaModulo,
è un'interfaccia o una classe astratta implementata
(estesa) da una o più classi (nella figura, ImplUnoModulo,
ImplDueModulo).
Figura 1 - Lo schema "Kernel-Modulo Dinamico"
Questo
è lo schema seguito per la gestione degli eventi
nella libreria Swing e per la realizzazione dei metodi
per l'ordinamento di array e collezioni di oggetti in
genere nella libreria Collection. Ad esempio, nella
gestione dell'evento "pressione di un bottone",
ClasseKernel è rappresentata dalla classe JButton
dove metodoKernel è il metodo notifyAction che
l'interprete di Java invoca automaticamente per notificare
l'occorrenza dell'evento ad ogni oggetto registrato
(obj) come ascoltatore di quel bottone (ActionListener,
cioè il nostro InterfacciaModulo). Questo invoca
a sua volta il metodo actionPerfomed (il nostro metodoModulo).
Quest'ultimo metodo (più in generale l'interfaccia
ActionListener) è poi effettivamente realizzato
in una classe definita dal programmatore (il nostro
ImplUnoModulo) che contiene il codice da eseguire in
relazione alla pressione del bottone.
Chiameremo lo schema rappresentato in Figura 1 "Schema
Kernel-Modulo Dinamico" (KMD in breve) dove la
combinazione delle classi ClasseKernel e InterfacciaModulo
rappresentano il "Kernel" e le classi che
implementano InterfacciaModulo, nell'esempio ImplUnoModulo
e ImplDueModulo, rappresentano i "Moduli".
Il Kernel può essere compilato in modo indipendente
dai Moduli mentre per la compilazione di un Modulo sono
necessarie le informazioni contenuto nella classe InterfacciaModulo.
/*
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() {
...
}
...
}
Lo
schema Kernel-Modulo è utilizzato anche nella
realizzazione di librerie in linguaggi non orientati
agli oggetti, per esempio il linguaggio C. Si consideri
una funzione che richieda una funzione esterna: in C
il prototipo della funzione esterna (nome più
tipo di dato restituito più tipi degli argomenti)
è contenuto in un file header (".h"),
incluso dal programma principale. A un file header corrisponde
un file sorgente (".c") contenente il codice
della funzione esterna. Chiameremo questo schema "Schema
Kernel-Modulo Statico" (KMS in breve). Gli schemi
grafici di schema KMD e KMS sono molto simili (si veda
la Figura 2).
Figura 2 - Lo schema Kernel-Modulo Statico
Astrazione sui dati, binding dinamico, polimorfismo,
overriding ed ereditarietà
A differenza dei linguaggi procedurali, lo schema KMD
nei linguaggi orientati agli oggetti contiene e sfrutta
le caratteristiche principali della programmazione orientata
agli oggetti: l'astrazione sui dati, il binding dinamico,
il polimorfismo, l'overriding e l'ereditarietà.
La combinazione di queste caratteristiche permette di
perseguire l'obiettivo del riuso del software in maniera
più semplice ed efficace rispetto al KMS. Vediamo
come con un esempio.
Figura 3 - A destra un parallelepipedo a sinistra
un cilindro, per entranbi il volume
è calcolato mediante la formula area di base
per altezza.
La
seguente classe definisce gli oggetti di tipo Prisma.
/*
File: Prisma.java */
public class Prisma {
private Poligono base;
private double altezza;
public Prisma(Poligono base, double altezza)
{
this.base = base;
this.altezza = altezza;
}
public double volume(){
// Chiamata al metodo esterno
area
return base.area() * altezza;
}
}
Un
prisma è un oggetto che ha un Poligono come base
ed un double rappresentante l'altezza. Il volume di
un prisma è definito dal metodo volume come area
della base per altezza. Si noti che a questo livello
non indichiamo quale tipo di poligono costituirà
la base del nostro prisma.
Il poligono alla base del prisma è definito tramite
un'interfaccia che richiede che ogni Poligono contenga
un metodo di nome area, utilizzato per calcolare l'area
del poligono stesso.
/*
File: Poligono.java */
interface Poligono {
public double area();
}
Il
nome della classe o dell'interfaccia può essere
utilizzato come tipo nella dichiarazone di nuovi identificatori
o come valore restituito da un metodo. Nell'esempio
ila variabile (campo) base nella classe Prisma è
di tipo Poligono. La classe Prisma e l'interfaccia Poligono
costituiscono il "Kernel".
Se desideriamo costruire un oggetto di tipo Prisma è
necessario fornire la definizione concreta (un'implementazione)
di un Poligono. Tale classe costituisce un "Modulo"
del nostro schema KMD. Ad esempio, la seguente classe
definisce il poligono Rettangolo:
/*
File: Rettangolo.java */
public class Rettangolo implements Poligono {
private double base;
private double altezza;
public Rettangolo(double base, double altezza)
{
this.base = base;
this.altezza = altezza;
}
public double area() {
return base * altezza;
}
}
/* File: UsaPrismi.java, prima versione */
public class UsaPrismi {
public static void main(Sring args[]) {
Prisma parallelepipedo = new
Prisma(new Rettangolo(3, 4), 4);
System.out.println(parallelepipedo.volume());
}
}
Dopo
aver compilato le varie classi, eseguendo la classe
UsaPrismi (java UsaPrismi) avremo come output sul terminale
il numero 48, cioè il volume del prisma con base
un rettangolo (di base 3 per 4) e altezza 4 (in effetti,
un parallelepipedo). A differenza dello schema KMS un
Modulo non è il semplice file sorgente per i
prototipi dei metodi definiti dall'interfaccia nel Kernel,
bensì il tipo introdotto dalla classe Modulo
è in relazione di sottotipo (o tipo più
specifico) con il tipo definito dall'interfaccia. Nell'esempio
significa che il tipo Rettangolo è sottotipo
di Poligono e quindi ogni oggetto di tipo Rettangolo
è anche di tipo Poligono. Questo significa che
il campo base di tipo Poligono nella classe Prisma può
contenere anche un oggetto di tipo Rettangolo (più
precisamente un riferimento ad un oggetto Rettangolo).
Questa proprietà è nota come polimorfismo.
Si supponga ora di voler definire un cilindro, cioè
un prisma con base un cerchio, in Java sarà sufficiente
introdurre la seguente definizione di classe e modificare
il main di UsaPrismi nel seguente modo:
/*
File: Cerchio.java */
public class Cerchio implements Poligono {
private double raggio;
public Cerchio(double raggio) {
this.raggio = raggio;
}
public double area() {
return raggio * raggio * Math.PI;
}
}
/*
File: UsaPrismi.java, seconda versione*/
public class UsaPrismi {
public static void main(Sring args[]) {
Prisma parallelepipedo = new
Prisma(new Rettangolo(3, 4), 4);
Prisma cilindro = new Prisma(new
Cerchio(2), 4);
// Nella seguente verrà
utilizzato il metodo area
// di Rettangolo
System.out.println(parallelepipedo.volume());
// Nella seguente verrà
utilizzato il metodo area
// di Cerchio!
System.out.println(cilindro.volume());
}
}
Ad
una nuova esecuzione della classe UsaPrismi l'output
sarà il precedente valore 48 (il volume del parallelepipedo)
ed il valore 50.24, cioè il volume del cilindro
. La cosa interessante è che per calcolare questi
valori si è utilizzato lo stesso metodo volume
presente nella classe Prisma contenente la stessa identica
chiamata al metodo esterno area. In altre parole, il
metodo area descritto nell'interfaccia Poligono è
polimorfo e può essere definito per più
di una classe, assumendo all'occorrenza (come nell'esempio
sopra) diverse implementazioni in ciascuna di esse (overriding
o sovrascrittura).
Figura 4 - L'astrazione sui dati, il binding
dinamico, il polimorfismo, l'overriding e l'ereditarietà
nello schema KMD
L'effettivo
codice del metodo area che viene eseguito viene determinato
solo a tempo di esecuzione, cioè nel momento
in cui potrà essere valutato l'effettivo tipo
dell'oggetto puntato dalla variabile base in Prisma.
La differenza fondamentale tra lo schema KMD e KMS è
quindi il momento in cui si effettua il legame tra il
nome del metodo esterno (o funzione esterna) utilizzato
all'interno del Kernel e l'effettivo codice da eseguire.
Tale legame, nel caso del linguaggio C, è realizzato
dal compilatore, ovvero il linker nel caso che sia il
Kernel che il Modulo siano già stati compilati
separatamente. Nel caso dei linguaggi orientati agli
oggetti la soluzione adottata è invece di ritardare
il più possibile, in pratica a tempo di esecuzione,
la risoluzione del legame tra il nome del metodo e il
suo codice. Il vantaggio è che mentre nello schema
KMC ad ogni header corrisponde un solo modulo, nello
schema KMD ad ogni interfaccia o classe astratta possono
corrispondere più implementazioni usate anche
contemporaneamente, ottenendo una maggiore flessibilità.
Se vi sono più definizioni per lo stesso metodo
esterno, sarà possibile scegliere quale codice
effettivamente eseguire in base al tipo di oggetto su
cui è invocato (nell'esempio, il metodo per il
calcolo l'area del rettangolo o cerchio) senza la necessità
di un esplicito test nella classe Kernel. Si noti che
è anche possibile aggiungere classi Modulo senza
alcuna necessità di ricompilare le classi Kernel
(nel nostro esempio abbiamo aggiunto la definizione
della classe Cerchio). Tale meccanismo prende il nome
di binding dinamico e si contrappone al binding statico
dei linguaggi procedurali tipo C.
Infine supponiamo di voler introdurre nel nostro esempio
il poligono quadrato. Un quadrato è a tutti gli
effetti un rettangolo la cui base e altezza hanno la
stessa lunghezza. In Java e in più generale nei
linguaggi orientati agli oggetti è possibile
definire classi come estensioni di altre classi senza
la necessità di riscrivere il codice in comune
ma semplicemente ereditandolo. Nell'esempio il metodo
per il calcolo dell'area per la classe Quadrato è
ereditato dalla classe Rettangolo. Possiamo così
scrivere una terza versione del main in UsaPrismi definendo
un terzo prisma, un cubo.
/*
File: Quadrato.java */
public class Quadrato extends Rettangolo {
public Quadrato(double lato) {
super(lato, lato);
}
}
/*
File: UsaPrismi.java, terza versione*/
public class UsaPrismi {
public static void main(Sring args[]) {
Prisma parallelepipedo = new
Prisma(new Rettangolo(3, 4), 4);
Prisma cilindro = new Prisma(new
Cerchio(2), 4);
Prisma cubo = new Prisma(new
Quadrato(4), 4);
System.out.println(parallelepipedo.volume());
System.out.println(cilindro.volume());
System.out.println(cubo.volume());
}
}
Conclusioni
In questo articolo abbiamo presentato lo schema Kernel-Modulo
Dinamico confrontandolo con lo schema Kernel-Modulo
Statico ed introducendo le principali caratteristiche
dei linguaggi orientati agli oggetti, l'astrazione sui
dati, il binding dinamico, il polimorfismo, l'overriding
e l'ereditarietà. Abbiamo inoltre visto come
tutte queste caratteristiche consentano un piu` semplice
riuso del software e una maggiore flessibilità
nella programmazione. Nel prossimop articolo discuteremo
del controllo dei tipi statico come "documentazione
interna del programma".
Bibliografia
[1] Bruce Eckel - "Thinking in Java", Edizione
Italiana, Apogeo, 2002. http://www.mindview.net/Books
[2] Cay S. Horstmann - "Concetti di informatica
e fondamenti di Java 2", Apogeo, 2000.
[3] Cay S. Horstmann e Gary Cornell - "Java 2:
i fondamenti", McGraw-Hill, 2001.
[4] Cay S. Horstmann e Gary Cornell - "Java 2:
tecniche avanzate", McGraw-Hill, 2000 .
[5] Meilir Page-Jones - "Progettazione a oggetti
con UML", Apogeo, 2002.
[6] Dave Schmidt - "Programming Principles in Java:
Architectures and Interfaces", http://www.cis.ksu.edu/~schmidt/CIS200
Matteo Baldoni è
ricercatore in informatica presso il Dipartimento di
Informatica dell'Università degli Studi di Torino.
|