Go - from Google


Interfacce     

questo è un capitolo parziale

Altro argomento molto importante che ci lega a quel mondo "a oggetti" che in Go è così sfuggente. In questo linguaggio un'interfaccia è composto da un insieme di metodi che definiscono il comportamento di un certo tipo. Ogni tipo in realtà ha una sua interfaccia che ne definisce i metodi. Si può anche dire, come scritto su alcuni blog sull'argomento, che un'interfaccia è una sorta di contratto tra una implementazione attesa e la sua effettiva realizzazione.
Come in altri linguaggi le interfacce sono del tutto astratte per cui non è possibile alcuna forma di discendenza diretta da esse similmente a quanto avviene in molti altri linguaggi. In realtà le interfacce sono il naturale elemento che permette a Go di avere molte delle caratteristiche tipiche dei linguaggi più ancorati al paradigma a oggetti: come detto nell'introduzione Go non è un linguaggio tipicamente a oggetti ma tramite le interfacce, come vedremo in questo paragrafo e anche altrove, non si fa mancare niente. Questo capitolo è in base a quanto detto, molto importante soprattutto in congiunzione con quello relativo alle strutture.
Formalmente la dichiarazione è molto semplice:

type nometipointerfaccia interface {
metodo-1 (parametri) tipo di ritorno
metodo-2 (parametri) tipo di ritorno
.......
metodo-n (parametri) tipo di ritorno
}

Come al solito si noti il posizionamento delle parentesi graffe che è rigidamente quello indicato, more solitu. Per convenzione il nome del tipo interfaccia termina con il suffisso "er" quindi tipo Printer, Writer, Counter o quello che è. In alternativa si può usare il suffisso "able" (es. Ricoverable) laddove "er" suona male. Dal momento che il mondo della programmazione parla inglese direi che non è il caso di farsi altre domande e accettare questa convenzione senza nulla obiettare. Ovviamente, come detto, è solo una convenzione, poi potete anche chiamare anche "pippo" la vostra interfaccia, il compilatore non ve ne farà una colpa.

Valgono alcune regole, o meglio considerazioni, generali:
  • Un tipo non deve necessariamente esplicitare quali interfacce implementa
  • Più tipi possono implementare la stessa interfaccia
  • Un tipo che implementa un'interfaccia può ovviamente avere altre funzioni al suo interno
  • Un tipo può implementare più interfacce
  • Un'interfaccia può contenere un riferimento ad ogni tipo che la implementa.

Per capire meglio come lavorano le interfacce seguiremo come traccia un articolo molto chiaro e ben fatto che potrete trovare, nella sua forma originale, qui (non ho l'abitudine di riportare articoli altrui ma sfido chiunque a trovare una introduzione migliore alle interfacce. Anche il  testo A way to Go riprende questo tipo di esempio).

Cominciamo quindi l'esempio base:


  Esempio 11.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Rectangle struct {
length, width int
}

func (r Rectangle) Area() int {
return r.length * r.width
}

func main() {
r := Rectangle{length:5, width:3}
fmt.Println("Rectangle details are: ",r)
fmt.Println("Rectangle's area is: ", r.Area())
}

Alla riga 5 viene definita una semplice struttura, un rettangolo, per il quale forniamo un metodoe per il calcolo della relativa area, riga 9 e che richiamiamo con dei parametri assegnati alla riga 14. Tutto molto semplice. L'esempio nasce dal fatto che un rettangolo è una superficie, non differente concettualmente da altre e tutte dotate di un'area. Quindi il passo successivo è quello di esprimere detta generalità.

  Esempio 11.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type Shaper interface {
Area() int
}

type Rectangle struct {
length, width int
}

func (r Rectangle) Area() int {
return r.length * r.width
}

func main() {
r := Rectangle{length:5, width:3}
s := Shaper(r)
fmt.Println("Area of the Shape r is: ", s.Area())
}

la riga 5 introduce la nostra interfaccia. Essa propone un metodo, Area(), che restituisce un intero. Il metodo manca dell'implementazione. Alla riga 9 ricompapre la nostra struct Rectangle, identica a quella dell'esempio 11.1. Insomma è tutto simile all'esempio precedente. La differenza sostanziale, oltre alla presenza della di interfaccia già vista, si ha alla riga 19 dove viene definita una variabile di tipo Shaper legata ad un tipo rettangolo tramite la definizione alla riga 18. La riga 19 equivale alla classica istruzione Java:

public class Rectangle implements Shaper
oppure in C#
class Rectangle : Shaper

risultando, a detta degli autori, più compatta (a parer mio la sintassi perfetta è quella di Java).
Il metodo Area() è attaccato al tipo Rectangle che quindi mantiene il contratto implementativo verso l'interfaccia. Sul fatto che questo contratto sia il fulcro di tutto lo possiamo vedere modificando l'esempio come segue:

package main

import "fmt"

type Shaper interface {
Area() int
}

type Rectangle struct {
length, width int
}

type Square struct {
lato int
}

func (r Rectangle) Area() int {
return r.length * r.width
}

func main() {
r := Rectangle{length:5, width:3}
s := Shaper(r)
fmt.Println("Area of the Shape r is: ", s.Area())
q := Square{lato:3}
s1 := Shaper(q)
}

cercando di compilare il codice qui sopra mostrato riceviamo un chiaro messaggio d'errore:

cannot convert q (type Square) to type Shaper:
Square does not implement Shaper (missing Area method)


questo perchè il tipo Square non è "puntato" dal metodo Area() che punta invece Rectangle. Affinchè anche Square sia accettato dall'interfaccia dobbiamo "puntargli contro" un metodo Area(), proprio come per Rectangle, e quindi l'esempio precedente può venire esteso come segue:

  Esempio 11.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

type Shaper interface {
Area() int
}

type Rectangle struct {
length, width int
}

type Square struct {
lato int
}

func (r Rectangle) Area() int {
return r.length * r.width
}

func (s Square) Area() int {
return s.lato * s.lato
}

func main() {
r := Rectangle{length:5, width:3}
s := Shaper(r)
fmt.Println("Area of the Shape r is: ", s.Area())
q := Square{lato:3}
s1 := Shaper(q)
fmt.Println("Area of the Square s1 is: ", s1.Area())
}

Se consideraimo la cosa a livello di interfaccia non possiamo non riscontrare un comportamento polimorfico. La cosa (anche se riconosco che il concetto non è così immediato in questo caso) diventa magari meglio comprensibile modificando la funzione main dell'esempio 11.3 come segue:

func main() {
r := Rectangle{length:5, width:3}
q := Square{lato:3}
var intf01 Shaper
intf01 = r
fmt.Println("Area of the Shape r is: ", intf01.Area())
intf01 = q
fmt.Println("Area of the Square s1 is: ", intf01.Area())
}

Ovvero definiamo una variabile che ha come tipo l'interfaccia Shaper (in C# e Java una cosa così è meglio non farla...) e ad essa possiamo attribuire comportamenti diversi dal momento che Area() può essere implementata liberamente.