Come definire e implementare un'interfaccia Go

Il modello orientato agli oggetti di Go ruota attorno alle interfacce. Personalmente ritengo che le interfacce siano il più importante costrutto linguistico e che tutte le decisioni di progettazione debbano essere focalizzate prima sulle interfacce. 

In questo tutorial imparerai cos'è un'interfaccia, le interfacce di Go su interfacce, come implementare un'interfaccia in Go e infine i limiti delle interfacce rispetto ai contratti.

Che cos'è un'interfaccia Go?

Un'interfaccia Go è un tipo costituito da una raccolta di firme del metodo. Ecco un esempio di un'interfaccia Go:

digitare Serializable interface Serialize () (string, error) Deserialize (s string) error

Il Serializable l'interfaccia ha due metodi. Il Serialize () il metodo non accetta argomenti e restituisce una stringa e un errore, e il Deserializzare () il metodo accetta una stringa e restituisce un errore. Se sei stato in giro per il blocco, il Serializable l'interfaccia è probabilmente familiare a voi da altre lingue, e potete indovinare che il Serialize () metodo restituisce una versione serializzata dell'oggetto target che può essere ricostruita chiamando Deserializzare () e passando il risultato della chiamata originale a Serialize ().

Si noti che non è necessario fornire la parola chiave "func" all'inizio di ogni dichiarazione di metodo. Go già sa che un'interfaccia può contenere solo metodi e non ha bisogno di aiuto da parte tua dicendo che è una "funzione".

Go Best practice per le interfacce

Le interfacce Go sono il modo migliore per costruire la dorsale del tuo programma. Gli oggetti dovrebbero interagire tra loro attraverso le interfacce e non attraverso oggetti concreti. Ciò significa che dovresti costruire un modello a oggetti per il tuo programma che consiste solo di interfacce e tipi di base o oggetti dati (le strutture i cui membri sono tipi di base o altri oggetti dati). Ecco alcune delle migliori pratiche che dovresti perseguire con le interfacce.

Intenzioni chiare

È importante che l'intenzione di ogni metodo e la sequenza di chiamate sia chiara e ben definita sia per i chiamanti che per gli implementatori. Non esiste un supporto a livello di lingua in Go per questo. Ne parlerò più nella sezione "Interface vs. Contract" più avanti.

Iniezione di dipendenza

Iniezione di dipendenza significa che un oggetto che interagisce con un altro oggetto attraverso un'interfaccia otterrà l'interfaccia dall'esterno come argomento di una funzione o di un metodo e non creerà l'oggetto (o chiamerà una funzione che restituisce l'oggetto concreto). Nota che questo principio si applica anche alle funzioni autonome e non solo agli oggetti. Una funzione dovrebbe ricevere tutte le sue dipendenze come interfacce. Per esempio:

digitare SomeInterface DoSomethingAesome () func foo (s SomeInterface) s.DoSomethingAwesome () 

Ora chiami funzione foo () con diverse implementazioni di SomeInterface, e funzionerà con tutti loro.

fabbriche

Ovviamente, qualcuno deve creare gli oggetti concreti. Questo è il lavoro di oggetti di fabbrica dedicati. Le fabbriche sono utilizzate in due situazioni:

  1. All'inizio del programma, le fabbriche vengono utilizzate per creare tutti gli oggetti di lunga durata la cui durata corrisponde in genere alla durata del programma.
  2. Durante il runtime del programma, vari oggetti spesso hanno bisogno di istanziare gli oggetti dinamicamente. Le fabbriche dovrebbero essere utilizzate anche per questo scopo.

È spesso utile fornire interfacce di fabbrica dinamiche agli oggetti per sostenere il modello di interazione solo dell'interfaccia. Nell'esempio seguente, definisco a widget interfaccia e a WidgetFactory interfaccia che restituisce a widget interfaccia dal suo CreateWidget () metodo. 

Il PerformMainLogic () la funzione riceve a WidgetFactory interfaccia dal suo chiamante. Ora è in grado di creare dinamicamente un nuovo widget basato sulla sua specifica di widget e invoca il suo Widgetize () metodo senza sapere nulla del suo tipo concreto (quale struttura implementa l'interfaccia).

type Widget interface Widgetize () tipo interfaccia WidgetFactory CreateWidget (widgetSpec stringa) (Widget, errore) func PerformMainLogic (factory WidgetFactory) ... widgetSpec: = Widget GetWidgetSpec (): = factroy.CreateWidget (widgetSpec) widget.Widgetize ( ) 

testabilità

La testabilità è una delle pratiche più importanti per un corretto sviluppo del software. Le interfacce Go sono il miglior meccanismo per supportare la testabilità nei programmi Go. Per testare a fondo una funzione o un metodo, è necessario controllare e / o misurare tutti gli input, le uscite e gli effetti collaterali sulla funzione sotto test. 

Per un codice non banale che comunica direttamente con il file system, l'orologio di sistema, i database, i servizi remoti e l'interfaccia utente, è molto difficile da ottenere. Ma, se tutta l'interazione passa attraverso le interfacce, è molto facile prendere in giro e gestire le dipendenze esterne. 

Si consideri una funzione che viene eseguita solo alla fine del mese e che esegue un codice per eliminare le transazioni non valide. Senza interfacce, dovresti andare a misure estreme come cambiare l'orologio del computer per simulare la fine del mese. Con un'interfaccia che fornisce l'ora corrente, basta passare una struttura a cui si imposta il tempo desiderato.

Invece di importare tempo e chiamando direttamente adesso(), puoi passare un'interfaccia con a Adesso() metodo che in produzione sarà implementato inoltrando a adesso(), ma durante il test verrà implementato da un oggetto che restituisce un tempo fisso per bloccare l'ambiente di test.

Utilizzando un'interfaccia Go

L'utilizzo di un'interfaccia Go è completamente semplice. Basta chiamare i suoi metodi come si chiama qualsiasi altra funzione. La grande differenza è che non puoi essere sicuro di cosa accadrà perché potrebbero esserci implementazioni diverse.

Implementazione di un'interfaccia Go

Le interfacce Go possono essere implementate come metodi sulle strutture. Considera la seguente interfaccia:

digita Shape interface GetPerimeter () int GetArea () int 

Ecco due implementazioni concrete dell'interfaccia Shape:

tipo Square struct side uint func (s * Square) GetPerimeter () uint return s.side * 4 func (s * Square) GetArea () uint return s.side * s.side tipo Rectangle struct width uint height uint func (r * Rectangle) GetPerimeter () uint return (r.width + r.height) * 2 func (r * Rectangle) GetArea () uint return r.width * r.height 

Il quadrato e il rettangolo implementano i calcoli in modo diverso in base ai loro campi e alle loro proprietà geometriche. Il prossimo esempio di codice mostra come popolare una porzione dell'interfaccia Shape con oggetti concreti che implementano l'interfaccia, quindi iterare sulla slice e richiamare la GetArea () metodo di ogni forma per calcolare l'area totale di tutte le forme.

func main () shapes: = [] Shape & Square side: 2, & Rectangle width: 3, height: 5 var totalArea uint per _, shape: = range shapes totalArea + = shape.GetArea ()  fmt.Println ("Area totale:", totaleArea) 

Implementazione di base

In molti linguaggi di programmazione, esiste un concetto di una classe base che può essere utilizzata per implementare la funzionalità condivisa utilizzata da tutte le sottoclassi. Go (giustamente) preferisce la composizione all'eredità. 

È possibile ottenere un effetto simile incorporando una struttura. Definiamo a nascondiglio struct che può memorizzare il valore dei calcoli precedenti. Quando un valore viene recuperato dal caso, stampa anche sullo schermo "cache hit", e quando il valore non è nel caso, stampa "cache miss" e restituisce -1 (i valori validi sono interi non firmati).

type Cache struct cache map [stringa] uint func (c * cache) GetValue (stringa nome) int valore, ok: = c.cache [nome] se ok fmt.Println ("cache hit") return int ( value) else fmt.Println ("cache miss") return -1 func (c * Cache) SetValue (stringa nome, valore uint) c.cache [nome] = valore

Ora incorporo questa cache nelle forme Square e Rectangle. Si noti che l'implementazione di GetPerimeter () e GetArea () ora controlla prima la cache e calcola il valore solo se non è nella cache.

digita struct Cache side uint func (s * Square) GetPerimeter () uint value: = s.GetValue ("perimeter") if value == -1 value = int (s.side * 4) s.SetValue ("perimetro", uint (valore)) restituisce uint (valore) func (s * Square) GetArea () uint valore: = s.GetValue ("area") se valore == -1 valore = int ( lato s * lato) s.SetValue ("area", uint (valore)) restituisce uint (valore) tipo Rectangle struct Cache width uint height uint func (r * Rectangle) GetPerimeter () uint value : = r.GetValue ("perimeter") se value == -1 value = int (r.width + r.height) * 2 r.SetValue ("perimeter", uint (valore)) restituisce uint (valore)  func (r * Rectangle) GetArea () uint valore: = r.GetValue ("area") se value == -1 value = int (r.width * r.height) r.SetValue ("area", uint (valore)) restituisce uint (valore)

Finalmente, il principale() funzione calcola l'area totale due volte per vedere l'effetto della cache.

func main () shapes: = [] Shape & Square Cache cache: make (mappa [stringa] uint), 2, e Rectangle Cache cache: make (mappa [stringa] uint), 3, 5  var totalArea uint per _, shape: = range shapes totalArea + = shape.GetArea () fmt.Println ("Area totale:", totalArea) totalArea = 0 per _, shape: = range shapes totalArea + = shape.GetArea () fmt.Println ("Area totale:", totalArea) 

Ecco l'output:

cache miss cache miss Totale area: 19 cache hit cache hit Area totale: 19

Interface vs. Contract

Le interfacce sono grandiose, ma non garantiscono che le strutture che implementano l'interfaccia soddisfino effettivamente l'intenzione dietro l'interfaccia. Non c'è alcun modo in Go per esprimere questa intenzione. Tutto quello che devi specificare è la firma dei metodi. 

Per andare oltre quel livello di base, hai bisogno di un contratto. Un contratto per un oggetto specifica esattamente cosa fa ogni metodo, quali effetti collaterali vengono eseguiti e qual è lo stato dell'oggetto in ogni momento. Il contratto esiste sempre. L'unica domanda è se è esplicita o implicita. Per quanto riguarda le API esterne, i contratti sono fondamentali.

Conclusione

Il modello di programmazione Go è stato progettato attorno alle interfacce. Puoi programmare in Go senza interfacce, ma perderai i loro numerosi vantaggi. Consiglio vivamente di sfruttare appieno le interfacce nelle avventure di programmazione Go.