Dr. Carlo Pescio
Oggetti ed Interfacce

Pubblicato su Computer Programming No. 63


In questo articolo vedremo, sulla base di alcuni esempi concreti, come l’uso mirato delle interfacce possa migliorare la riusabilità e l’estendibilità del design. Prenderemo poi in esame, sempre basandoci su esempi reali, il problema della qualità di un’interfaccia. Arriveremo così ad alcune linee guida per migliorare robustezza, manutenibilità, possibilità di riuso, e così via.


La separazione tra l’interfaccia di un modulo e la sua implementazione è uno dei criteri fondamentali della buona progettazione. Introdotta inizialmente in linguaggi come Modula 2 ed Ada, nel paradigma ad oggetti ha assunto una importanza fondamentale come chiave di volta per la riusabilità e l’estendibilità del design. Non a caso, in Java è stato introdotto un concetto di interfaccia separato da quello di classe, per esplicitarne il ruolo centrale nella progettazione. In questo articolo vedremo due esempi concreti di utilizzo delle interfacce per migliorare la riusabilità e l’estendibilità del software. Daremo anche una caratterizzazione precisa delle interfacce come contratto, e prenderemo in esame, sulla base di esempi reali, il problema della qualità di un’interfaccia. Arriveremo così ad alcune linee guida per migliorare le interfacce, sotto il profilo della robustezza, manutenibilità, possibilità di riuso, e così via.

Cos’é un’interfaccia?
Un’interfaccia è una specifica, un contratto tra ogni classe che la implementa ed il mondo esterno: l’interfaccia vincola le classi che la implementano a supportare le funzioni in essa dichiarate. In questo modo, il compilatore può verificare in modo statico che ogni funzione chiamata su un oggetto sia effettivamente implementata dall’oggetto stesso. Questa verifica, in un linguaggio con checking dinamico come Smalltalk, avviene solo a tempo di esecuzione, quando viene verificato se l’oggetto gestisce o meno il messaggio passato. Eseguire il controllo staticamente, al momento della compilazione, riduce il numero di errori di programmazione che possono essere trovati solo in fase di testing.
Notiamo che l’interfaccia non vincola in alcun modo l’implementazione delle classi. In questo senso, l’uso delle interfacce è la chiave di volta per l’estendibilità del software: oggetti di una qualunque classe C che implementi l’interfaccia I possono essere utilizzati in qualunque contesto che operi su elementi di tipo I. Come vedremo, l’uso accorto delle interfacce è anche una delle chiavi per la riusabilità del software. Ricordiamo (si veda la precedente puntata di Principles&Techniques) che riusabilità ed estendibilità sono fattori distinti, per quanto entrambi importanti.
In alcuni linguaggi, come Java, le interfacce hanno un corrispondente lessicale diretto; in altri linguaggi, come il C++ o Eiffel, non esiste una distinzione tra classe ed interfaccia: semplicemente, un’interfaccia diventa una classe con sole funzioni astratte, non implementate dalla classe stessa. È bene evidenziare che non vi è alcuna differenza sostanziale tra scrivere (in Java)

interface I
  {
  T1 f( T2, ..., Tn ) ;
  // ...
  }

class C : implements I
  {
  // ....
  }

e scrivere invece (in C++)

class I
  {
  public :
    virtual 
    T1 f( T2, ..., Tn ) = 0 ;
    // ...
  } ;

class C : public virtual I
  {
  // ....
  } ;

Notiamo l’uso dell’ereditarietà virtuale in C++: poiché una classe interfaccia non ha dati, e di norma non definisce neppure un costruttore, l’uso dell’ereditarietà virtuale esprime al meglio la relazione IS-A senza controindicazioni di sorta legate all’inizializzazione degli oggetti (vedere [Pes95a] per ulteriori dettagli). Come ho peraltro mostrato nel mio recente intervento al Borland Forum ‘97, in questo caso non abbiamo neanche un impatto sulle prestazioni, in quanto i compilatori trattano il caso di classi base virtuali senza data member in modo particolare.
Per questa ragione, in quanto segue non farò alcuna distinzione tra i linguaggi come Java, dove le interfacce sono first citizen e quelli come il C++, dove le interfacce sono classi particolari: in entrambi i casi possiamo utilizzare i costrutti del linguaggio per implementare senza problemi un design basato su interfacce.
Un’ultima osservazione prima di passare ad alcuni esempi concreti: implementare un’interfaccia è intrinsecamente più sicuro rispetto ad ereditare da una classe che implementa parte delle funzioni, perché nel contesto dell’interfaccia tutte le funzioni virtuali seguono il modello che tempo fa (vedere [Pes95b]) avevo chiamato "di tipo 1". In un futuro articolo tornerò sull’argomento per discutere alcuni sviluppi più recenti; nel frattempo, ricordo che l’articolo citato è disponibile on-line partendo dalla mia home page.

Interfacce e Riuso
Supponiamo di dover implementare un dialog box che consente di scegliere un elemento da una lista di opzioni, con due bottoni OK e Cancel, e con il doppio click su un elemento della lista equivalente al tasto OK. Il più ovvio design per un simile dialog è quello di fig. 1, dove il dialog box fa da mediatore [GoF94] tra la lista, la sorgente dei dati, ed i tasti.

Ora, quanti dialog di questo tipo contiene una applicazione media? Indubbiamente più di uno. Uno dei programmi che sto tenendo aperti in questo momento è il Visual C++: senza perdere troppo tempo in esplorazioni, File/New, Edit/Go To, View/Toolbars, Insert/Resource, Build/Set Default Configuration, Tools/Remote Connection, Window/Windows, Help/Open Information Title sono tutti basati su questo modello, eventualmente con un banale renaming dei bottoni e/o l’aggiunta di qualche elemento. In teoria, abbiamo quindi trovato uno dei tanti elementi riusabili, sia all’interno di una singola applicazione che in applicazioni diverse; in pratica, questo famoso riuso profetizzato dai padri dell’OOP non si concretizza quasi mai nella misura sperata. Una delle mie attività come consulente è la revisione del design per varie società, al fine di eliminare eventuali problemi, migliorare l’estendibilità o la riusabilità, e così via. Questo mi permette tra l’altro di conoscere bene i problemi e gli errori più frequenti. Proprio sulla base dell’esperienza, posso sicuramente dire che in nove progetti su dieci ad ogni dialog di selezione corrisponde una classe specifica, che reimplementa da zero l’estrazione della descrizione da ogni elemento, l’inserimento nella lista, la gestione del doppio click e la gestione di OK/Cancel.
Del resto, un design come quello di fig. 1 non è assolutamente riusabile: abbiamo legato in modo diretto il mediatore (SelectionDialog) con la sorgente dei dati (Option). Notiamo che uno degli errori più grandi, perpetrato con la complicità di molti autori di testi ed articoli, è di concentrare l’attenzione sulla famigerata "classe riusabile", di norma usando come esempio le classiche classi contenitore. Nei progetti reali, l’unità di riuso è raramente una singola classe: di solito più classi interagiscono per ottenere un fine, e quello che si vuole riusare è il risultato complessivo di quell’interazione. Tornando al nostro esempio, quello che vogliamo fare è riutilizzare, adattandolo ai diversi casi, un insieme di classi che consente di selezionare un elemento in una lista. Se all’interno del design l’accoppiamento tra le classi è troppo forte, se la conoscenza di "quale classe fornisce quale servizio" è hard-coded, non avremo altra possibilità di riuso che il copy&paste, con tutte le conseguenze sulla manutenzione. A questo punto, non solo abbiamo abbandonato la strada della riusabilità e le sue promesse di tempi complessivi di sviluppo inferiori e (soprattutto) di qualità superiore del risultato, ma vedremo anche il nostro collega (o concorrente) che segue l’approccio RAD staccarci di una lunghezza nei tempi di consegna, ottenendo peraltro una qualità complessiva ed una manutenibilità paragonabili, e decisamente inferiori ad quelle di un progetto realmente ad oggetti. Occorre sempre ricordare che il riuso non avviene per caso o in virtù di un linguaggio: è la conseguenza di precise scelte di design.
Torniamo al nostro esempio. Il dialog conosce la classe Option, ma in realtà sfrutta solo un sottoinsieme delle sue funzioni. Semplificando un po’, possiamo pensare che utilizzi solo una funzione GetDescription(); potremmo anche pensare di supportare una rappresentazione non testuale, ma si tratta di una semplice estensione. Una struttura di gran lunga più flessibile è allora quella di fig. 2, dove il dialog box non è legato ad una specifica sorgente dei dati, ma ad una interfaccia Selectable (provate a pensare ad un nome migliore per l’interfaccia; no, ISelectable non è un miglioramento). Ogni classe che (come Option) implementa tale interfaccia può essere usata con il nostro dialog box, al fine di selezionare un elemento in una collezione.

Pensate alle conseguenze che un uso sistematico delle interfacce può avere sul potenziale di riuso del vostro codice. Ad esempio, anche in una situazione molto semplice come quella sinora descritta esistono almeno altri due elementi che potremmo generalizzare. Il primo è il tipo di collezione: al dialog non interessano i dettagli implementativi, la collezione potrebbe essere una lista linkata (singola o doppia), un array, una hash table... l’importante è che implementi certe funzionalità base che possono essere astratte in una interfaccia Collection. Ma un elemento ancora più interessante è la listbox. In realtà un dialog di selezione non è necessariamente vincolato ad una rappresentazione a lista. Potremmo usare una combo box, una vista ad albero, un insieme di radio button o di checkmark (comincia qui a delinearsi la possibilità di selezione singola o multipla). Provate a pensare ad un design alternativo che consenta di cambiare, in ogni momento e senza modifiche di sorta al nucleo del dialog di selezione, la rappresentazione degli elementi: questo è un esempio di utilizzo delle interfacce come elemento di estendibilità del design. Provate a compiere un ulteriore passo di astrazione e pensate ad un oggetto "SelectItem" che non necessariamente è implementato come dialog box: ad esempio, il menu popup che ottenete con un right click sulle toolbar di Office o del Visual C++ è un degno sostituto di un dialog box con una lista a selezione multipla. L’uso accorto delle interfacce vi permette di cambiare l’implementazione di tutti questi dettagli senza alcun impatto sul codice client, e di riusare il nucleo delle soluzioni in contesti diversi, sostituendo elementi compatibili con le interfacce.
Una nota finale: possiamo pensare al passaggio dal diagramma di fig. 1 a quello di fig. 2 come all’applicazione di una tecnica di trasformazione più elementare: resterebbe poi da chiarire quando una simile tecnica vada applicata, e quali benefici e costi ne seguono. In un contesto più ampio, analoghe tecniche di trasformazione formano il nucleo del mio approccio al design [Pes97], che io chiamo sistematico per distinguerlo dalle molte metodologie che spiegano il processo di design in termini astratti ma non insegnano a progettare concretamente. Alcuni di voi avranno avuto occasione di frequentare il mio worklab "Systematic Object Oriented Design" al Developer’s Forum, e sapranno con precisione a cosa mi riferisco. Per gli altri, oltre all’articolo di cui sopra (che dà una panoramica dell’approccio) posso preannunciare che nel 1998 appariranno sul Journal of Object Oriented Programming diversi miei articoli sull’argomento. In più di una occasione, proverò anche a trattare alcuni dei temi in Principles&Techniques.

Interfacce ed Estendibilità
Supponiamo di voler creare un programma distribuito, con due o più processi in esecuzione su macchine diverse che comunicano tra loro. In ambiente Windows NT, potremmo pensare di utilizzare le named pipe, un meccanismo di comunicazione molto efficiente. Un design banale è allora quello di fig. 3: diverse classi client (all’interno dello stesso processo) comunicano con l’altro processo attraverso una classe NamedPipe. Naturalmente ho riportato solo parte del design in fig. 3.

Supponiamo che una volta terminato il progetto, sorga l’esigenza di comunicare anche con macchine non NT, e venga individuata come soluzione l’uso dei socket. Ovviamente, "siccome abbiamo usato gli oggetti" sarà facile modificare il programma per supportare anche i socket. Invece non è così. Diverse classi nel design originale parlano direttamente con NamedPipe. Aggiungere una classe Socket senza un elemento di uniformità significa dire addio agli oggetti e tornare a programmare a suon di if/else o di switch/case, con tutte le conseguenze in fase di manutenzione (che succede se in seguito vogliamo comunicare tramite RPC?). Il problema è che il design "banale", come ho sin dall’inizio battezzato quello di fig. 3, non contiene alcun elemento di estendibilità: classi concrete si conoscono e si parlano direttamente. In genere, un elemento di estendibilità è rappresentato proprio da un’interfaccia: notate l’intrinseca estendibilità del design alternativo di fig. 4.

Compito dell’interfaccia IPC (inter-process communication) è di fornire una visione astratta delle funzionalità richieste ad un canale di comunicazione tra processi. Ogni classe che implementa l’interfaccia IPC potrà essere usata per comunicare tra processi diversi senza alcuna modifica alle classi client. Provate a pensare ad una gerarchia di interfacce IPC: ad esempio, le anonymous pipe di NT sono utilizzabili solo tra processi locali. O ancora, la gerarchia potrebbe distinguere tra classi di comunicazione portabili come i socket ed altre platform-specific, come le named pipe. Verificate che valga il principio di sostituibilità di Liskov tra interfacce derivate ed interfacce base.
Notiamo che le classi concrete Socket, Named Pipe, e così via, potrebbero anche risiedere in moduli esterni, ad esempio DLL nel caso del C++. Il nome della DLL potrebbe essere specificato anche a run-time, ad esempio attraverso un file di setup. In Java, potremmo invece usare Class.forName( className ) per creare un oggetto di classe className, che nuovamente è una stringa che può assumere valori diversi a run-time. In questo modo si realizza un programma facilmente estendibile in ogni momento, senza alcuna ricompilazione del modulo base. In alcuni casi (molto rari) è anche possibile per il programma principale generare del codice sorgente (ad esempio per svolgere un compito molto lungo in modo ottimizzato), invocare un compilatore a linea di comando, ed in seguito caricare la DLL così ottenuta, portando la dinamicità del sistema ai suoi limiti. Nessun livello di programmazione generica (si veda lo scorso numero) è in grado di portare agli stessi risultati di semplicità di estensione ed allo stesso tempo di flessibilità: con tanti saluti a chi dice che gli oggetti non funzionano.
In generale, ogni volta che una classe utilizza un’altra classe concreta dovreste fermarvi e pensare: sono interessato ad aggiungere un elemento di estendibilità in questo punto? Molte volte la risposta sarà no - sarebbe troppo oneroso prevedere degli slot di estensione in ogni punto del programma. Ma in un numero rilevante di casi la risposta sarà affermativa, ed il modo corretto di introdurre un elemento di estendibilità è di interporre un’interfaccia astratta ben progettata tra i client e la classe concreta (anche in questo caso, si tratta di una semplice tecnica di trasformazione del design). Introdurre un’interfaccia aiuta anche a comprendere meglio le reali esigenze della classe: sposta l’attenzione sul comportamento atteso, più che su chi fornisce quel comportamento.

Interfacce e Contratti
Abbiamo visto due esempi concreti di utilizzo delle interfacce durante il design, per migliorarne l’estendibilità e la riusabilità. Potete peraltro trovare qualche altro semplice esempio in [CM97]. Mantenendo fede al nome della rubrica, è ora il momento di fare un passo indietro e riconsiderare le interfacce in un’ottica più ampia, per capire sino a che punto sono sicure e vantaggiose, e sino a dove sono correttamente supportate dai diversi linguaggi.
Se pensiamo ad un’interfaccia come ad un contratto, Java ed il C++ non ci consentono di essere molto precisi circa i termini del contratto stesso. Tutto quello che possiamo fare è definire la signature delle funzioni che appartengono all’interfaccia: non possiamo però dire nulla circa il comportamento che ci aspettiamo da ogni funzione. Questa parte del contratto viene lasciata a livello informale, alla famosa "documentazione" che ben pochi scrivono con il dovuto grado di precisione e che praticamente nessuno mantiene sincronizzata con il codice.
Eppure, ben pochi utilizzerebbero un circuito integrato senza una precisa specifica comportamentale. Perché il software rappresenta, ancora una volta, un’eccezione? In gran parte, perché dare una specifica del comportamento senza fare riferimento ad una implementazione è molto difficile. In moltissimi casi, decisamente al di là delle capacità del programmatore medio. Esiste una vasta letteratura sui metodi formali di specifica, ma la loro applicazione a progetti non "giocattolo", fuori dall’accademia, è sempre stata estremamente limitata. Del resto, se dare una specifica formale di un’interfaccia mi costa (in tempo e denaro) più dello sviluppo, testing e debugging delle classi che implementano tale interfaccia, è realmente conveniente investire in una specifica? La ricerca della perfezione non sempre è vantaggiosa per i produttori, e apparentemente nel mondo del software commerciale è addirittura deleteria. Bertrand Meyer ha recentemente suggerito che, per componenti pensati per un riuso su larga scala, la spesa necessaria alla definizione di una specifica formale possa essere quantomeno ammortizzata. Guardando al passato, non credo che molti accoglieranno la proposta.
Esistono, ovviamente, delle vie di mezzo. Proprio Meyer (la cui intervista, vi ricordo, appare su Computer Programming No. 61) ha introdotto in Eiffel un supporto piuttosto completo per il cosiddetto Design by Contract [Mey97], che è in qualche modo una semplificazione dei metodi formali di specifica. In Eiffel, è possibile definire come parte dell’interfaccia di una classe le precondizioni di ogni funzione (ovvero la parte del contratto che ogni cliente deve rispettare per poter chiamare la funzione), le postcondizioni di ogni funzione (ovvero, la parte del contratto che la funzione si impegna a rispettare, ammesso che valgano le precondizioni), e l’invariante di classe (ovvero le proprietà che possiamo assumere valide in ogni momento circa lo stato degli oggetti). Se uno di questi elementi del contratto viene violato, viene sollevata un’eccezione. Eiffel permette agli oggetti di violare temporaneamente l’invariante: è normale che durante l’esecuzione di una funzione di aggiornamento, lo stato interno diventi inconsistente. Tuttavia, l’invariante deve essere rispettata all’entrata ed all’uscita di ogni chiamata top-level ad una funzione. Inoltre, le precondizioni e le postcondizioni vengono ereditate dalle classi derivate, in modo da rispettare il principio di sostituibilità di Liskov. Ricordo (si veda il mio articolo precedente in Principles&Techniques per maggiori dettagli) che tale principio afferma che un oggetto di classe derivata deve essere sostituibile in tutti i contesti in cui può essere usato un oggetto di classe base. Ciò significa che le precondizioni di una classe derivata possono essere solo più permissive di quelle della classe base, e che le postcondizioni possono essere solo più restrittive. Ovvero, che il contratto di una classe derivata può solo "chiedere di meno" e "garantire di più" ai clienti, in modo da non scontentarli. Finezze? Può darsi. Per citare un esempio noto in bibliografia, [Coo92] mostra come un uso troppo liberale dell'ereditarietà, non legato alla conformità ad una interfaccia né ad una specifica comportamentale, abbia introdotto non poche inconsistenze nella gerarchia di classi contenitore di Smalltalk.
Purtroppo, simulare il notevole supporto al Design by Contract presente in Eiffel in linguaggi come Java o in C++ è più complicato di quanto vorremmo. Da un lato, Eiffel permette di far riferimento al "valore precedente" dell’oggetto, operazione già piuttosto complessa da realizzare in C++ o Java. Inoltre in Eiffel le precondizioni, postcondizioni ed invariante fanno parte dell’interfaccia, mentre noi potremmo solo metterle come parte dell’implementazione, vanificando i nostri scopi di estendibilità controllata. Ereditare le pre/postcondizioni sarebbe poi compito del programmatore. Controllare l’invariante sarebbe nuovamente compito del programmatore, che dovrebbe anche verificare se si trova in una chiamata innestata o top-level. Insomma, un compito molto gravoso che il più delle volte si ripercuote in un uso delle asserzioni (pre/post condizioni ed invariante sono tutte forme di asserzione) di gran lunga inferiore al possibile, ancora di più in Java dove non esiste un meccanismo per eliminare ogni controllo in versione release. Tra l’altro, da una comunicazione privata con James Gosling (il progettista di Java) sono venuto a sapere che in una delle prime versioni del linguaggio aveva incluso un certo supporto per il Design by Contract, ma in seguito lo ha eliminato a causa di alcuni problemi (non mi ha voluto dire quali). La mia opinione, già espressa nell’intervista a Meyer, è che troppi programmatori si sarebbero spaventati di fronte a quello che sembra un inutile apparato formale. Anzi, sicuramente molti sarebbero contrari a specificare meglio i termini del "contratto software", per poter prosperare al meglio nell’incertezza: come nei contratti reali, il cliente non può tornare indietro a lamentarsi di ciò che non garantiamo. Del resto, gli stessi IDL del DCOM e di CORBA, che dovrebbero fare da infrastruttura per i componenti distribuiti, non permettono di esprimere altro che la signature delle funzioni. La direzi one delle interfacce sembra quindi essere tracciata, ad un livello che seppure notevole (come abbiamo visto negli esempi precedenti) è decisamente inferiore alle loro reali potenzialità. Esiste comunque qualche tentativo analogo al Design by Contract per rendere i metodi formali più appetibili ai "veri programmatori", legando un linguaggio di specifica ad un linguaggio di uso comune: ad esempio, Larch C++ è un sistema sviluppato all’università dell’Iowa che permette di dare una specifica formale del comportamento delle classi C++. Se volete approfondire il tema, potete trovare sia lo strumento che un’abbondante documentazione all’URL http://www.cs.iastate.edu/~leavens/larchc++.html.
In conclusione, l’uso delle interfacce è sicuramente un elemento centrale della progettazione ad oggetti, che concretizza il famoso principio (riportato peraltro anche in un testo non principle-driven come [GoF94]) "program to an interface, not an implementation". Tuttavia richiede una corretta documentazione del reale contratto dietro l’interfaccia, dal momento che la sola signature non è assolutamente in grado di esprimere il comportamento atteso dalle implementazioni conformi. Scrivere e mantenere "viva" tale documentazione è il prezzo che dobbiamo pagare per le maggiori possibilità di riuso ed estendibilità che ne conseguono.

Qualità delle interfacce
Progettare un’interfaccia non è affatto semplice. Di rado mi capita di vedere uno sviluppatore realmente soddisfatto di una libreria di classi commerciale, indice che l’interfaccia che le classi espongono lascia parecchio a desiderare. Del resto, è anche difficile dare una buona definizione della qualità di una interfaccia. Tra le tante metriche che vengono proposte per l’OOD, ben poche si applicano a questo caso, ed a mio avviso non catturano quello che lo sviluppatore considera l’elemento fondamentale: la semplicità di utilizzo di una classe. Prima ancora della completezza delle funzionalità offerte (che al limite possono essere estese) la semplicità di uso determina la percezione della qualità da parte del programmatore.
Proviamo allora a tornare all’interfaccia come contratto. Come clienti, un buon contratto è quello che ci chiede poco e ci garantisce molto. Questo è l’elemento determinante nella percezione di "semplicità d’uso". Se l’interfaccia di una classe richiede che f1 sia chiamata prima di f2, che f3 non sia mai chiamata prima di f4, che dopo aver chiamato f5 non si chiami mai f6, che alcune funzioni vengano chiamate solo se l’oggetto si trova in un certo stato, se insomma le precondizioni sono troppo restrittive ed affidate alla documentazione o peggio all’esplorazione dei sorgenti (chi ha usato MFC sa di cosa sto parlando), difficilmente chi la usa sarà soddisfatto. Questo richiama uno dei criteri che avevo proposto in [Pes95c]: non dovrebbero esserci forti sequenzialità e dipendenze temporali tra le funzioni proprie di un oggetto. Per quanto il mio criterio si riferisse all’analisi, si applica altrettanto bene alle interfacce. Purtroppo, è anche un criterio difficilmente formalizzabile, soprattutto in assenza di una specifica comportamentale come parte dell’interfaccia. Il rispetto di questo principio resta quindi lasciato all’esperienza ed alla capacità creativa del progettista. Esistono però altri criteri un po’ più oggettivi e facili da verificare. Il primo è sicuramente il numero di parametri di ogni funzione. Una funzione con molti parametri è sempre sospetta.
Talvolta, la funzione è estremamente specializzata, quindi ha bisogno di molti parametri per poter completare "in un colpo solo" una serie di sotto-funzioni complesse. Vale sicuramente la pena di spendere un po’ di tempo pensando ad una sua decomposizione in funzioni più elementari, o cercando di raggruppare più parametri in una sola astrazione significativa. In alcuni casi, possiamo applicare la legge di inversione di Meyer [Mey97] (che in realtà andrebbe chiamata tecnica di inversione): è possibile che i troppi parametri siano dovuti ad una errata allocazione di responsabilità. Se spostiamo la funzione altrove (ad esempio su uno dei parametri, o su una classe che rappresenta un gruppo di essi) possiamo probabilmente fornire un’interfaccia più semplice a chi utilizza le nostre classi. Vediamo un semplice esempio, modellato intorno ad MFC, e riferito per chiarezza ad una classe concreta anziché ad una interfaccia astratta. In MFC, le funzioni di manipolazione dei menu, come la seguente:

BOOL CWnd::HiliteMenuItem( 
CMenu* pMenu, 
UINT nIDHiliteItem, 
UINT nHilite ) ;

sono tutte funzioni membro di CWnd, e prendono praticamente tutte come parametro un menu, la specifica di un elemento, ed un flag che dice se mettere o togliere un qualche attributo, definito dal nome della funzione. Per specificare un elemento di menu dobbiamo usare l’operatore or bitwise, componendo i valori MF_BYCOMMAND o MF_BYPOSITION con un valore che indica il comando o la posizione all’interno del menu.
Di norma, ciò che vogliamo fare è prendere un certo elemento, esaminarne e modificarne un insieme di attributi. Sarebbe molto più comodo avere una classe che rappresenta l’astrazione MenuItem, come la seguente:

class MenuItem
  {
  public :
    enum How 
    {BY_COMMAND, BY_POSITION } ;
    MenuItem( Menu* menu, int item, How spec ) ;
    Hilite( bool on ) ;
    Check( bool on ) ;
    Enable( bool on ) ;
    CString GetString() ;
    UINT GetState() ;
    // ...
  } ;

Dopo la costruzione (che richiede tre parametri solo perché ho voluto evitare l’operatore bitwise per rendere più robusta l’interfaccia) ogni funzione su MenuItem ha pochi o nessun parametro (le varie funzioni con parametro bool potrebbero essere sdoppiate in due funzioni senza parametri). Iterare sugli elementi di un menu diventerebbe più semplice, e così via. Tutto questo, semplicemente spostando la responsabilità di una serie di funzioni da CWnd (che obiettivamente ha poco a che vedere con esse) ad una classe che meglio si può assumere tali responsabilità. Ovviamente, questa struttura andrebbe contro uno dei principi di progetto di MFC, ovvero rispecchiare il più possibile la struttura del SDK; questo porta però inevitabilmente ad una pessima struttura ad oggetti.
In ogni caso, anche i progettisti di MFC hanno occasionalmente rimediato ad alcune mancanze del SDK: ad esempio tutte le funzioni come Rectangle o Ellipse, che a livello SDK vogliono quattro parametri x1,y1,x2,y2, in MFC sono presenti anche in una versione overloaded con un solo parametro CRect: la funzionalità non è stata spostata, ma più parametri sono stati riuniti in un’unica astrazione. Naturalmente, dal mio punto di vista sarebbe più elegante avere una classe Rectangle ed una funzione Rectangle::Draw( DC ), ma nuovamente, questo va contro la filosofia di MFC.
Un altro caso molto frequente di funzione con molti parametri è la "funzione universale", la cui lista di parametri è pensata come unione di tutte quelle utili alle diverse implementazioni. Stiamo quindi cercando di rendere "generale" qualcosa nel modo sbagliato, ovvero come unione di proprietà anziché come intersezione delle proprietà comuni. In questo caso, la lunga lista di parametri è sintomo di un abuso dell’interfaccia: stiamo raggruppando astrazioni diverse sotto un unico nome; forse, semplicemente sotto un’unica funzione. Il rimedio migliore è quello di separare le diverse funzioni, e se è necessario anche le diverse interfacce.
Riprendiamo ora l’esempio precedente (HiliteMenuItem); notiamo che la sua interfaccia è decisamente poco robusta: possiamo dimenticare l’or bitwise, usare valori insensati, eccetera. La mia classe alternativa non solo separa i due parametri, ma usa un tipo enumerato per evitare un errore classico, ovvero l’inversione di due parametri nella chiamata di funzione. Una funzione con diversi parametri di tipo compatibile in sequenza diretta è sempre candidata ad errori nel punto di chiamata, errori che spesso sono anche complicati da trovare perché la chiamata "sembra corretta". Alcuni linguaggi, come Ada, permettono di specificare il nome del parametro formale a fianco di ogni parametro attuale, proprio per evitare errori legati alla posizione in chiamate di funzioni con molti parametri (ed anche per gestire meglio i parametri con valori di default). In linguaggi come C++ o Java, dobbiamo sfruttare il type system al meglio per prevenire simili errori.
Un altro buon criterio è di fornire funzionalità ad un adeguato livello di astrazione, se necessario stratificando il design - interfacce di basso livello con micro-funzionalità ed interfacce di più alto livello che offrono funzionalità più complete. Restando su Windows ed MFC, per tracciare una linea dobbiamo necessariamente usare l’accoppiata MoveTo-LineTo, in quanto non esiste un’unica funzione (né una corrispondente classe) Line. LineTo può essere più efficiente in diversi casi, ma Line sarebbe decisamente più pratica in molti altri.
Veniamo infine ad un ultimo, importante criterio di qualità dell’interfaccia (anche se, come sempre, ci sarebbe molto altro da dire). Un’interfaccia lega (accoppia) sia le classi che la implementano che le classi client con i tipi dei parametri di tutte le funzioni che compongono l’interfaccia stessa. Sarebbe auspicabile che questi parametri fossero "leggeri", ovvero che non introducessero accoppiamenti indesiderati. L’interfaccia ideale ha come parametri delle sue funzioni o tipi predefiniti o altre interfacce, e mai classi concrete: altrimenti, l’utilità dell’interfaccia come strumento di disaccoppiamento viene in qualche modo limitata.

Tutto qui?
Naturalmente... no. Vi sarebbero molti altri criteri di qualità da discutere: ad esempio, il concetto di iniziativa in un’interfaccia [Koe97] meriterebbe un intero articolo. Inoltre le interfacce si evolvono, anche perché non sempre si riesce ad ottenere una buona interfaccia al primo colpo. Talvolta si scopre anche di aver "dimenticato" un’interfaccia astratta come base di diverse classi concrete. Gestire adeguatamente l’evoluzione delle interfacce, in presenza dei numerosi vincoli dei progetti reali (impossibilità a modificare alcune parti del sorgente, necessità di restare compatibili con altro software, magari anche a livello binario, eccetera) non è facile e può anche richiedere alcune modifiche ai linguaggi: ad esempio, la conformità strutturale rispetto ad un’interfaccia, anziché la conformità "per nome". Non solo, che dire dei componenti software? Nel COM, ad esempio, le interfacce hanno un ruolo fondamentale e l’ereditarietà di implementazione scompare, lasciando il posto ad una tecnica di riuso diversa chiamata "aggregation" (nome decisamente poco adatto in quanto non si tratta di una semplice composizione per aggregazione).
Infine, in quanto sopra è rimasta una zona d’ombra: se le classi client conoscono solo l’interfaccia, chi crea gli oggetti di classe concreta? Ricordiamo che il nome della classe concreta deve essere noto per crearne un’istanza (oggetto). La risposta breve, ma che per molte ragioni non è completamente soddisfacente, è "dovete usare uno dei pattern creazionali del [GoF94]". In realtà, anche in questo caso si possono applicare diverse tecniche di trasformazione che hanno il pregio di non richiedere doti di paragnosta (provate a leggere la sezione "Applicability" del pattern Abstract Factory e capirete che non è semplice sapere in anticipo se ci troveremo in una delle situazioni descritte - o meglio ancora, non è semplice dire quando non ci troveremo in una di esse).
Restate sintonizzati: tutti questi temi verranno approfonditi in una delle future puntate di Principles&Techniques.

Reader's Map
Molti visitatori che hanno letto
questo articolo hanno letto anche:

Bibliografia:
[CM97] Peter Coad, Mark Mayfield, "Designing with Interfaces for Java", Software Development, April 1997.
[Coo92] William R. Cook, "Interfaces and Specifications for the Smalltalk-80 Collection Classes", ACM OOPSLA’92 Proceedings, 1992.
[Koe97] Andrew Koenig, "Turning an interface inside out", Journal of Object Oriented Programming, Vol. 10 No. 2, May 1997.
[GoF94] Gamma ed altri, "Design Patterns", Addison-Wesley, 1994.
[Mey97] Bertrand Meyer, "Object Oriented Software Construction, second edition", Prentice Hall, 1997.
[Pes95a] Carlo Pescio, "C++ Manuale di Stile", Edizioni Infomedia, 1995.
[Pes95b] Carlo Pescio, "Il problema della "fragile base class" in C++", Computer Programming No. 41, Novembre 1995.
[Pes95c] Carlo Pescio, "Lezioni di Object Oriented Technology", Computer Programming No. 35, Aprile 1995.
[Pes97] Carlo Pescio, "Systematic Object Oriented Design", IEEE Computer, Vol. 30 No. 9, September 1997.

Biografia
Carlo Pescio (pescio@eptacom.net) svolge attività di consulenza in ambito internazionale nel campo delle tecnologie Object Oriented. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. È incaricato della valutazione dei progetti dal Direttorato Generale della Comunità Europea come Esperto nei settori di Telematica e Biomedicina. È laureato in Scienze dell'Informazione ed è membro dell'ACM, dell'IEEE e della New York Academy of Sciences.