|
Le Interfacce
L'ereditarietà
è un mezzo di classificazione importante, che
tuttavia presenta dei limiti in determinate circostanze.
Nel mondo reale, quando si pensa alla classificazione
degli esseri viventi, salta subito agli occhi il caso
dell'Ornitorinco, un animale che abbraccia in modo trasversale
la classificazione: è un mammifero ma depone
le uova, e possiede caratteristiche morfologiche comuni
alla marmotta e alla papera.

Figura 1 - L'Ornitorinco, un animale che rifiuta
facili classificazioni
Nel
software queste situazioni di parentela trasversali
sono estremamente comuni. Si provi a progettare un software
per la gestione di un negozio di libri. Il punto di
partenza più naturale è la definizione
di una classe Libro:

Figura
2 - Una classe che racchiude le informazione di
un libro
La
classe Libro appena definita è caratterizzata
da un titolo, un prezzo ed un autore. Se si desidera
estendere l'attività alla classificazione di
riviste, si può passare ad una classificazione
di questo genere:

Figura 3 - Una gerarchia che mette in relazione
libri e riviste
Le
riviste hanno alcuni attributi in comune con il libro,
come il titolo e il prezzo, ma hanno anche degli attributi
differenti come il tipo (quotidiano, mensile
).
Per rappresentare in modo corretto la parentela, è
necessario introdurre una superclasse astratta comune
alle due categorie.
Se
poi si desidera estendere la casistica in modo da includere
anche la vendita di CD ROM, ci si trova di fronte ad
un primo problema di classificazione: anche se da un
punto di vista fisico il CD ROM non ha niente in comune
con un libro (è fatto di plastica invece che
di carta, richiede un particolare strumento per poter
essere fruito e così via), è anche vero
che il CD ROM ha diversi attributi in comune ad un libro:
un autore, una data di pubblicazione, un argomento un
titolo
Si può allora espandere la classificazione
in modo da tenere in considerazione le distinzioni appena
illustrate:

Figura 4 - Una gerarchia più complessa per
il problema di esempio
Nel
momento in cui emerge la necessità di estendere
il programma in modo da trattare anche il caso di articoli
di cancelleria, sorge il problema di come classificare
questi ultimi in relazione agli oggetti editoriali appena
individuati. L'unica caratteristica in comune tra libri
ed articoli di cancelleria è il fatto di avere
un prezzo

Figura 5 - Il prezzo rappresenta una caratteristica
trasversale ad ogni possibile classificazione
Il
prezzo è una proprietà degli oggetti che
non può rientrare in una normale gerarchia: è
un chiaro esempio di proprietà trasversale. Un'altra
caratteristica trasversale è l'ordinabilità:
alcuni oggetti, tipo libri e riviste, sono ordinabili
per titolo o per autore, mentre altri, come le agende,
chiaramente no.

Figura 6 - L'ordinabilità è un'altra
caratteristica trasversale
rispetto alla gerarchia dell'esempio
In
conclusione, nel contesto di un programma per la gestione
degli articoli di una cartolibreria sono permessi vari
livelli di astrazione: in fase di vendita l'unico attributo
che conta realmente è il prezzo; durante l'organizzazione
della merce sugli scaffali conta invece l'ordinabilità;
in fase di consultazione infine contano tutti gli attributi
che ogni singolo oggetto è in grado di esprimere.
Uso delle Interfacce per definire il comportamento
L'uso delle interfacce genera una certa perplessità
in chi ha l'abitudine di associare gli oggetti software
ad una determinata implementazione. Quale può
essere l'utilità di un sistema di classificazione
di entità software basato solamente sulle firme
dei metodi? La risposta è che l'interfaccia denota
un protocollo adatto a descrivere proprietà comuni
a diverse categorie di oggetti, astraendo delle numerose
possibilità di implementare il comportamento
stesso.
Si
pensi ad un caso concreto: se durante la preparazione
di un dolce la ricetta suggerisce di mischiare gli ingredienti
all'interno di un contenitore, si può ricorrere
ad una marmitta da cucina, realizzata in plastica e
con una forma tale da rendere il lavoro di mescolatura
particolarmente facile; d'altra parte, in mancanza di
una marmitta, è possibile usare qualunque altro
tipo di contenitore, compresa una pentola per pastasciutta.
Nonostante la pentola non sia stata progettata per questo
uso, essa ha in comune con la marmitta la proprietà
di poter contenere dei fluidi, e per questa ragione
potrà essere usata per portare a termine correttamente
l'operazione.
Si pensi anche ad una lavastoviglie: essa opera su oggetti
lavabili, non necessariamente su stoviglie, pentole
o posate. Chiunque abbia dei bambini per casa, avrà
utilizzato più volte la lavastoviglie per ripulire
giocattoli in plastica: una situazione permessa dal
fatto che anche questi ultimi risultano essere lavabili.
Le
interfacce riducono la dipendenza da classi concrete:
un'interfaccia rappresenta un contratto fra la classe
che la implementa e i suoi client. I termini del contratto
sono definiti dalle firme dei metodi dichiarati nell'interfaccia,
metodi che ogni classe concreta si impegna ad implementare.
Si provi a definire un'interfaccia che sia rappresentativa
di tutti gli oggetti Lavabili:
public
interface Lavabile {
public void bagna();
public void insapona();
public void asciuga();
}
Un
ipotetico oggetto Lavatrice potrà a questo punto
prevedere un metodo lava() capace di operare su qualunque
oggetto lavabile:
public
class Lavatrice {
public void lava(Lavabile l) {
l.bagna();
l.insapona();
l.asciuga();
}
}
Gli esempi presi dal mondo reale possono dare un'idea
di alcune situazioni in cui il meccanismo dell'interfaccia
rivela la sua utilità: visto il largo uso che
ne viene fatto nelle classi di sistema di Java, sarà
possibile approfondire con la pratica la sensibilità
necessaria a capire quali sono le precise circostanze
in cui ricorrere alle interfacce per la risoluzione
di problemi concreti.
Sintassi
Per dichiarare un'interfaccia si ricorre ad una sintassi
simile a quella usata per le classi, con alcune importanti
differenze:
public
interface NomeInterfaccia {
public
static final tipo nomeAttributo;
public
tipo nomeMetodo(tipo par1, tipo par2,
.);
}
I
metodi devono per forza essere pubblici e non prevedono
un blocco di codice. È possibile definire attributi
solo di tipo static e final, ossia costanti.
Per
dichiarare una classe che implementa un'interfaccia,
bisogna utilizzare la parola chiave implements nel modo
la seguente:
public
class ClassB extends ClassA implements Interface1, Interface2,
... {
...
corpo della classe A
...
}
dove
Interface1, Interface2, Interface3,
sono le interfacce
da implementare. E' permesso implementare più
di un'interfaccia. Si noti che le interfacce possono
formare a loro volta gerarchie, nelle quali è
permesso introdurre l'ereditarietà multipla:
public
interface MyInterface extends Interface1,Interface2,...
{
...
}
Se
due interfacce contengono due metodi con la stessa firma
e con lo stesso valore di ritorno, la classe concreta
dovrà implementare il metodo solo una volta e
il compilatore non segnalerà alcun errore. Se
i metodi hanno invece lo stesso nome ma firme diverse,
la classe concreta dovrà dare un'implementazione
per ciascuno dei metodi. Se infine le interfacce dichiarano
metodi con lo stesso nome ma con valore di ritorno differente
(ad esempio int getResult() e long getResult()), il
compilatore segnalerà un errore, dal momento
che il linguaggio Java non permette di dichiarare in
una stessa classe metodi la cui firma differisca solo
per il tipo del valore di ritorno.
Infine,
se due interfacce legate da un qualche grado di parentela
dichiarano una costante utilizzando lo stesso nome,
sarà sempre possibile accedere all'una o all'altra
usando l'identificatore di interfaccia:
Interfaccia1.costante;
Interfaccia2.costante;
Si
noti che in questo caso le costanti omonime possono
anche essere di tipo diverso.
Un esempio concreto
Per non restare troppo nell'astratto, ecco ad un esempio
di reale utilità.
Le
API Java definiscono l'interfaccia Comparable
public
interface Comparable {
public int compareTo(Object o);
}
Tale
interfaccia viene implementata da un gran numero di
classi molto diverse tra loro: BigDecimal, BigInteger,
Byte, ByteBuffer, Character, CharBuffer, Charset, CollationKey,
Date, Double, DoubleBuffer, File, Float, FloatBuffer,
IntBuffer, Integer, Long, LongBuffer, ObjectStreamField,
Short, ShortBuffer, String ed URI.
Le
classi appena elencate hanno in comune tra di loro solamente
il fatto di essere ordinabili. Dal momento che implementano
tutti l'interfaccia Comparable, è possibile scrivere
un metodo che permetta di ordinare array di un oggetti
di qualunque tipo tra quelli elencati:
public
class ComparableSorter {
public static Comparable[] sort(Comparable[]
list) {
for(int i = 0 ; i < list.length
; i++ ) {
int minIndex = i;
for(int j = i ;
j < list.length ; j++) {
if ( list[j].compareTo(list[minIndex])
< 0 )
minIndex
= j;
}
Comparable tmp =
list[i];
list[i] = list[minIndex];
list[minIndex] =
tmp;
}
return list;
}
}
Il
metodo ordina() non è interessato a quale sia
il tipo concreto degli oggetti che gli vengono passati:
l'unico requisito a cui è interessato è
che essi implementino l'interfaccia Comparable, in modo
da permettere l'esecuzione dell'algoritmo di ordinamento.
Dal punto di vista del metodo ordina(), un vettore di
Integer è uguale ad un vettore di String: il
suo comportamento non è influenzato da questa
differenza. Questo metodo funziona su tutti gli oggetti
che implementano l'interfaccia Comparable, persino su
oggetti che al momento non esistono, ma che verranno
creati nei prossimi anni.
Chi
realizza le classi concrete ha la responsabilità
di stabilire un criterio di confronto e di incorporarlo
nel metodo compareTo(): la logica di ordinamento presente
nel metodo ordina() trascende il particolare criterio
adottato per l'oggetto concreto.
Tipi e Polimorfismo
Il polimorfismo è un'importante proprietà
dei linguaggi ad oggetti: essa attesta la possibilità
di utilizzare un oggetto al posto di un altro, laddove
esista una parentela tra i due. Grazie alle interfacce
è possibile esprimere ad un livello di dettaglio
molto profondo l'appartenenza a determinate categorie,
e creare procedure in grado di operare in modo trasversale
su un gran numero di oggetti accomunati solo da una
certa proprietà.
Come
già constatato in precedenza, una classe ha come
tipo quello della propria classe e di tutte le sue superclassi.
L'interfaccia denota a sua volta un tipo: pertanto una
classe ha tanti tipi quante sono le interfacce implementate.
Java è un linguaggio Strong Typed: il legame
tra un oggetto e i suoi tipi è un aspetto fondamentale
ed inderogabile, al contrario di linguaggi come il C
o il C++ dove il legame tra tipo ed oggetto è
lasco, e vengono permesse operazioni anche di casting
prive di senso. In Java è obbligatorio definire
esplicitamente il tipo di una variabile; inoltre il
casting tra oggetti funziona solamente se il tipo dell'oggetto
coincide con quanto richiesto dall'operatore di casting.
Una buona norma di programmazione è quella di
manipolare gli oggetti utilizzando una variabile del
tipo che possiede i requisiti meno stringenti in relazione
al contesto. In questo modo si garantisce il massimo
grado di riutilizzo ad ogni singolo elemento del sistema.
Il Patter Factory
Una interfaccia permette di definire entità software
astraendo dai dettagli implementativi, in modo da eliminare
le dipendenze da classi concrete e porre l'attenzione
sul ruolo degli oggetti nell'architettura che si vuole
sviluppare. In fase di creazione occorre comunque specificare
il nome di una classe concreta, una circostanza che
ripresenta il problema della dipendenza dal contesto:
Interfaccia
c = new OggettoConcreto();
Questa
dipendenza può essere rimossa ricorrendo ad un
espediente di programmazione piuttosto interessante.
Si immagini di dover progettare un sistema per la manipolazione
di documenti; ovviamente esso dovrà definire
un'interfaccia Document che dichiari le modalità
di interazione comuni a tutti i documenti presenti nel
sistema:
public
interface Document {
public void open();
public void close();
public String read();
public void write(String s) ;
}
Si
può quindi procedere con la definizione di alcune
implementazioni concrete di Document:
public
class TechnicalDocument implements Document {
public void open() { ... }
public void close() { ... }
public String read() { ... }
public void write(String s) { ... }
}
public class CommercialDocument implements Document
{
public void open() { ... }
public void close() { ... }
public String read() { ... }
public void write(String s) { ... }
}
Se
tuttavia si desidera che il sistema non presenti dipendenze
dai documenti concreti, è necessario ripulire
il codice da espressioni del tipo:
Document
doc = new TechnicalDocument();
Per
raggiungere questo scopo, è sufficiente definire
un'interfaccia factory (parola inglese che significa
"fabbrica"), il cui compito è quello
di creare oggetti di tipo Document:
public
interface DocumentFactory {
public Document createDocument();
}
Per
premettere la creazione dei diversi tipi di documento,
è necessario dichiarare una classe factory per
ogni tipo di documento:
public
class TechnicalDocumentFactory implements DocumentFactory
{
public Document createDocument() {
Document doc = new TechnicalDocument();
...
return doc;
}
}
public class CommercialDocumentFactory implements DocumentFactory
{
public Document createDocument() {
Document doc = new CommercialDocument();
...
return doc;
}
}
A
questo punto, laddove sia necessario creare oggetti
concreti, si potrà ricorrere alle classi factory,
come nel metodo seguente che copia il contenuto di un
documento all'interno di un documento differente:
public
Document copyDocument(Document oldDocument,
DocumentFactory factory) {
Document newDocument = factory.createDocument();
oldDocument.open();
newDocument.open();
String content = oldDocument.read();
newDocument.write(content);
oldDocument.close();
newDocument.close();
return newDocument ;
}
La
procedura copyDocument() riesce a creare un oggetto
concreto senza creare alcun tipo di dipendenza verso
classi concrete. Essa pertanto potrà essere utilizzata
con qualunque oggetto conforme alle interfacce Document
e DocumentFactory, anche se progettati in un momento
successivo alla definizione delle interfacce.

Figura 7 - Grazie al Pattern Factory il sistema
di gestione di documenti DocumentHandler
non prevede dipendenze dalle realizzazioni concrete
dell'interfaccia Document
(clicca sull'immagine per ingrandire)
L'espediente
appena illustrato prende il nome di Pattern Factory,
e viene usato in decine di situazioni differenti durante
lo sviluppo di sistemi con linguaggi ad oggetti. Nel
corso degli ultimi anni sono stati codificati numerosi
Pattern, che sono entrati a far parte delle best practices
della comunità degli sviluppatori. Lo studio
dei Pattern è un passaggio obbligatorio per chi
desideri raggiungere il controllo totale sulle possibilità
offerte dai linguaggi ad oggetti: esiste un'ampia letteratura
sull'argomento, alla quale si raccomanda di attingere
a partire dai testi suggeriti in bibliografia.
Conclusioni
Questo mese abbiamo analizzato il costrutto delle interfacce,
che permette di implementare una forma di ereditarietà
multipla in Java. Il mese prossimo parleremo di Eccezioni,
l'ultimo importante costrutto del linguaggio Java.
Bibliografia
e riferimenti
Design Patterns - Elements of Reusable Object-Oriented
Software
E. Gamma, R. Helm, R.Johnson, J. Vlissides
Addison Wesley - 1995
Un libro che ha introdotto per la prima volta nel mondo
della programmazione Object Oriented il rivoluzionario
concetto di Pattern. Da leggere, rileggere e consultare.
Refactoring
: Improving the Design of Existing Code
Martin Fowler, Kent Beck (Contributor), John Brant (Contributor),
William Opdyke, don Roberts
Ed. Addison-Wesley Object Technology Series - 1999
Questo libro introduce una settantina di tecniche di
revisione del codice Java che permettono di rendere
i programmi più robusti, chiari e facili da mantenere.
Un libro da tenere sempre a portata di mano.
|