In generale, nella programmazione ad oggetti, un’interfaccia è un’entità che descrive un insieme di firme di metodi, le cui implementazioni possono essere poi fornite da più classi che altrimenti non sarebbero necessariamente correlate tra loro. Un’interfaccia è quindi un contratto che deve essere rispettato dalle classi che la implementano.
Le interfacce permettono di ottenere l’eredità multipla e di applicare il concetto di astrazione nella progettazione di classi e moduli software (concetto che abbiamo visto nel post sui principi SOLID).
Ma ora veniamo a LabVIEW. Nel mondo LabVIEW le interfacce sono un concetto abbastanza recente. Sono infatti state introdotte con la versione 2020.
Come riportato nella documentazione, in LabVIEW un’interfaccia può essere vista come una classe senza dati, cioè senza il cluster private data. Un’interfaccia dichiara un ruolo che un oggetto può svolgere senza definire come eseguire tale ruolo. La dichiarazione di tale ruolo avviene definendo un insieme di metodi dinamici vuoti all’interno dell’interfaccia stessa.
Ereditando da un’interfaccia, una classe dichiara che i suoi oggetti svolgono quel ruolo e la classe diventa responsabile di specificare come viene implementato tale ruolo. La classe deve quindi fare l’override di tutti i metodi dinamici definiti nell’interfaccia che implementa.
Anche in LabVIEW le classi possono ereditare da più interfacce permettendo quindi l’ereditarietà multipla. Quando una classe eredita da più interfacce, la classe deve implementare tutti i metodi di tutte le interfacce che implementa.
Classi astratte e interfacce
Innanzitutto definiamo cosa si intende con classe astratta. In generale, una classe astratta è una classe che non può essere istanziata e che deve essere quindi estesa con classi derivate. Una classe astratta definisce metodi e proprietà tutti o in parte anch’essi astratti (vuoti).
Posto che le classi astratte (pure) in LabVIEW non esistono, possiamo sommariamente vedere una classe astratta come una classe base (super classe) con metodi dinamici vuoti e proprietà e dove i metodi dinamici sono marcati con l’opzione Descendants must override. Parlo di classi astratte e non di classi base o superclassi semplicemente per avere un approccio più teorico. Si noti che tutte le considerazioni in questo sotto capitolo possono essere applicate anche a classi base ibride (quindi non puramente astratte).
Fatta questa premessa, vediamo di capire analogia e differenze tra classi astratte e interfacce.
Entrambe rappresentano un contratto che la classe derivata deve rispettare implementando i metodi del tipo base (cioè della classe che estende o dell’interfaccia che implementa). Le classi astratte però definiscono un legame più forte con la classe derivata poiché ne rappresentano il tipo base. Le interfacce invece definiscono un modello generico (un ruolo) che rappresenta un comportamento comune per le classi che implementano tali interfacce. Tali classi possono essere di vario genere e natura. Pensiamo per esempio al metodo Serialize dell’interfaccia Serializable. Tale interfaccia rappresenta la capacità delle classi che la implementano di serializzarsi in una sequenza di byte. Questa interfaccia potrebbe essere implementata da classi completamente scorrelate tra loro: per esempio la classe DMM che modellizza un multimetro digitale, la classe TestReportSettings che contiene le impostazioni per la generazione dei report di una sequenza di test e la classe User che contiene i dati di un utente.
Quando quindi usare una classe astratta e quando un’interfaccia?
L’unica risposta che mi sento di dare è… dipende caso per caso e dipende dalle intenzioni dello sviluppatore. Le due infatti, seppur abbiano diverse analogie, hanno ruoli ben differenti dal punto di vista della progettazione del software. Una classe astratta definisce una sorta di contratto verticale con le classi derivate ed il suo utilizzo limita il livello di astrazione delle componenti software che la utilizzano. Questo perché l’utilizzo di una classe astratta pone un vincolo sulla gerarchia delle classi. Un’interfaccia invece rappresenta più un contratto di tipo orizzontale con gli oggetti che la implementeranno. Questo contratto è più generico rispetto a quello della classe astratta. Grazie a questo però, un’interfaccia crea un livello di astrazione più forte rispetto ad una classe astratta e permette ai vari moduli software di avere dipendenze più generiche. Quindi, se vogliamo optare per un approccio più astratto, generico ed estendibile, opteremo per un’interfaccia. Viceversa, se vogliamo un vincolo più forte sulla gerarchia delle classi (perdendo potenzialmente in astrazione), opteremo per una classe astratta.
Una regola generale che possiamo utilizzare per decidere se usare una classe astratta o un’interfaccia è: usa classi astratte ed ereditarietà se vale l’affermazione “A è una B”. Usa le interfacce se vale l’affermazione “A è capace di…”. Ad esempio, possiamo dire che un rettangolo è un poligono ma non ha senso dire che un rettangolo è capace di essere un poligono. Ad ogni modo, credo che il buon senso sia sempre la regola che vince su tutte le altre. A volte infatti un’interfaccia si adatta molto meglio, anche se la regola sopra dice il contrario.
Best practice per la denominazione delle interfacce
National Instruments fornisce alcune linee guida nella denominazione delle interfacce:
- Utilizzare un aggettivo o un avverbio che descriva la capacità di un oggetto. Ad esempio, denominare un’interfaccia CanMeasureVoltage.lvclass se l’interfaccia rappresenta l’hardware in grado di misurare la tensione. Qualsiasi classe o interfaccia che eredita quell’interfaccia è in grado di misurare la tensione. Un altro esempio potrebbe essere denominare un’interfaccia Serializable se l’interfaccia rappresenta la capacità delle classi che la implementano di serializzarsi in una sequenza di byte (come la serializzazione binaria, XML o JSON).
- Dove non è possibile utilizzare un aggettivo o un avverbio, utilizzare un nome che descriva la categoria di classi che ereditano dall’interfaccia. Ad esempio, denominare un’interfaccia Database.lvclass se l’interfaccia descrive una categoria di classi che hanno il ruolo di salvare dati su database differenti.
- Evitare di usare la lettera maiuscola iniziale “I”. Sebbene la maggior parte dei linguaggi di programmazione testuale denominino le interfacce con una lettera maiuscola iniziale “I” per differenziare le interfacce dalle classi, LabVIEW distingue le interfacce e le classi utilizzando i glifi.
Benefici nell’utilizzo delle interfacce
L’utilizzo delle interfacce dà diversi benefici:
- Un’interfaccia permette di definire un comportamento che può essere implementato da un gruppo di classi non correlate senza costringerle a condividere una gerarchia di classi comune.
- Implementando un’interfaccia, una classe può svolgere un ruolo diverso da quello dettato dalla sua gerarchia di classi.
- Poiché una classe non è limitata al numero di interfacce che può implementare, può partecipare a un’ampia varietà di ruoli.
- Una classe può essere esposta attraverso la sua interfaccia, nascondendo efficacemente i suoi dettagli di implementazione forzando tutte le comunicazioni attraverso i suoi metodi di interfaccia pubblici.
- È possibile modificare facilmente (o meglio estendere) i sistemi esistenti per fornire nuove funzionalità all’interno della loro attuale gerarchia di classi
Il prossimo post illustrerà un esempio pratico sull’uso di interfacce e classi. Spero possa fornire maggiori spunti e strumenti per applicare questi concetti a problemi reali.