MokaByte 66 - 7mbre 2002 
Un'introduzione al paradigma ad oggetti attraverso lo schema Kernel-Modulo
I parte
di
Matteo Baldoni
Attraverso lo studio di uno degli schemi più ricorrenti con cui sono organizzate le classi nelle librerie di Java è possibile capire perchè il paradigma object-oriented abbia avuto ampia diffusione e coglierne l'essenza: l'astrazione sui dati, il binding dinamico, il polimorfismo, l'overriding e l'ereditarietà.

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.

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