MokaByte 75 - Giugno 2003 
Corso di programmazione Java
XV parte: Interfacce
di
Andrea Gini
Dopo aver visto i fondamenti della programmazione ad Oggetti ed aver messo in luce l'ereditarietà, è ora di studiare le interfacce Java, un costrutto che permette di implementare una forma di ereditarietà multipla che viene ampiamente utilizzata nella programmazione OO.

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.

 

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