Come Utilizzare le Interfacce in Go

Introduzione

Scrivere codice flessibile, riutilizzabile e modulare è fondamentale per lo sviluppo di programmi versatili. Lavorare in questo modo garantisce che il codice sia più facile da mantenere evitando la necessità di apportare lo stesso cambiamento in diversi posti. Come raggiungere questo obiettivo varia da linguaggio a linguaggio. Per esempio, l’eredità è un approcio comune utilizzato nei linguaggi come Java, C++, C# e così via.

I sviluppatori possono anche raggiungere gli stessi obiettivi di progettazione attraverso la composizione. La composizione è un modo per combinare oggetti o tipi di dati in più complessi. Questo è l’approcio che Go utilizza per promuovere il riutilizzo del codice, la modularità e la flessibilità. Le interfacce in Go forniscono un metodo per organizzare composizioni complesse, e imparare come usarle permetterà di creare codice comune e riutilizzabile.

In questo articolo, impareremo come comporre tipi personalizzati che hanno comportamenti comuni, consentendoci di riutilizzare il nostro codice. Impareremo anche come implementare le interfacce per i nostri tipi personalizzati in modo da soddisfare le interfacce definite da un altro pacchetto.

Definizione di un Comportamento

Una delle implementazioni core della composizione è l’uso di interfacce. Un’interfaccia definisce un comportamento di un tipo. Una delle interfacce più comunemente usate nella libreria standard di Go è l’fmt.Stringer interfaccia:

type Stringer interface {
    String() string
}

La prima riga di codice definisce un type chiamato Stringer. Poi dice che è un’interface.Come definire una struct, Go usa le parentesi graffe ({}) per circondare la definizione dell’interfaccia. In confronto alla definizione delle struct, noi solo definiamo il comportamento dell’interfaccia; ossia, “cosa può fare questo tipo”.

Nel caso dell’interfaccia Stringer, l’unico comportamento è il metodo String(). Il metodo non richiede argomenti e restituisce una stringa.

Poi, guardiamo qualche codice che ha il comportamento dell’fmt.Stringer:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	fmt.Println(a.String())
}

Il primo thing che facciamo è creare un nuovo tipo chiamato Article. Questo tipo ha un Title e un Author campo e entrambi sono del tipo stringa data type:

main.go
...
type Article struct {
	Title string
	Author string
}
...

Successivamente, definiamo un method chiamato String sul tipo Article. Il metodo String restituirà una stringa che rappresenta il tipo Article:

main.go
...
func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

Allora, nella nostra main funzione, creiamo un’istanza del tipo Article e la assegniamo alla variabile chiamata a. Forniamo i valori di "Understanding Interfaces in Go" per il campo Title e "Sammy Shark" per il campo Author:

main.go
...
a := Article{
	Title: "Understanding Interfaces in Go",
	Author: "Sammy Shark",
}
...

Successivamente, stampiamo il risultato del metodo String chiamando fmt.Println e passando il risultato della chiamata del metodo a.String():

main.go
...
fmt.Println(a.String())

Dopo aver eseguito il programma, vedrete l’output seguente:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark.

Fino ad ora, non abbiamo usato un’interfaccia, ma abbiamo creato un tipo che ha un comportamento. quel comportamento corrisponde all’interfaccia fmt.Stringer. Passando ora, vedremo come possiamo usare quel comportamento per rendere il nostro codice più riutilizzabile.

Definizione di un’Interfaccia

Ora che abbiamo definito il nostro tipo con il comportamento desiderato, possiamo osservare come usare quel comportamento.

Però, prima di farlo, considerate ciò che dovremmo fare se volessimo chiamare il metodo String dal tipo Article in una funzione:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)
}

func Print(a Article) {
	fmt.Println(a.String())
}

Nella presente codifica aggiungiamo una nuova funzione chiamata Print che accetta un’Article come argomento. Notare che la funzione Print fa solo chiamare il metodo String. Per questo motivo potremmo definire invece un interfaccia per passarla alla funzione:

main.go
package main

import "fmt"

type Article struct {
	Title string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
	String() string
}

func main() {
	a := Article{
		Title: "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)
}

func Print(s Stringer) {
	fmt.Println(s.String())
}

Abbiamo creato un’interfaccia chiamata Stringer:

main.go
...
type Stringer interface {
	String() string
}
...

L’interfaccia Stringer ha solo un metodo, chiamato String() che restituisce una stringa. Un metodo è una funzione speciale definita nello stesso tipo di dati in Go. Al contrario di una funzione, un metodo può essere chiamato solo dall’istanza del tipo su cui è stato definito.

Riprovediamo quindi l’intestazione della funzione Print ad accettare un Stringer, e non un tipo concreto di Article. Perché il compilatore sa che un’interfaccia Stringer definisce il metodo String, accetterà solo tipi che possiedono anche il metodo String.

Ora possiamo usare la funzione Print con qualsiasi cosa soddisfa l’interfaccia Stringer. Creiamo un altro tipo per mostrarci questo:

main.go
package main

import "fmt"

type Article struct {
	Title  string
	Author string
}

func (a Article) String() string {
	return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
	Title  string
	Author string
	Pages  int
}

func (b Book) String() string {
	return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
	String() string
}

func main() {
	a := Article{
		Title:  "Understanding Interfaces in Go",
		Author: "Sammy Shark",
	}
	Print(a)

	b := Book{
		Title:  "All About Go",
		Author: "Jenny Dolphin",
		Pages:  25,
	}
	Print(b)
}

func Print(s Stringer) {
	fmt.Println(s.String())
}

Aggiorniamo ora il tipo Book. Anche lui ha definito il metodo String. Questo significa che soddisfa anche l’interfaccia Stringer. Per questo motivo, posso invocare anche la nostra funzione Print:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

Finora abbiamo mostrato come usarli solo con un’interfaccia. Tuttavia, un’interfaccia può avere più di uno stile definito. Proseguiremo osservando come possiamo rendere le nostre interfacce più flessibili dichiarando più metodi.

Multipli comportamenti nell’interfaccia

Un principio fondamentale della scrittura del codice Go è quello di scrivere tipi piccoli e concisi che si compongono per creare tipi più complessi. La stessa regola vale per la composizione delle interfacce. Per vedere come costruiamo una interfaccia, prima di tutto definire solo una interfaccia. Definiriamo due tipi, Circle e Square, che entrambi definiscono un metodo chiamato Area. Questo metodo restituirà l’area geometrica rispettivamente dei loro risultati:

main.go
package main

import (
	"fmt"
	"math"
)

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * math.Pow(c.Radius, 2)
}

type Square struct {
	Width  float64
	Height float64
}

func (s Square) Area() float64 {
	return s.Width * s.Height
}

type Sizer interface {
	Area() float64
}

func main() {
	c := Circle{Radius: 10}
	s := Square{Height: 10, Width: 5}

	l := Less(c, s)
	fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}

Dato che ogni tipo definisce il metodo Area, possiamo creare un’interfaccia che definisca questo comportamento. Definiamo quindi l’interfaccia Sizer come segue:

main.go
...
type Sizer interface {
	Area() float64
}
...

Poi definiamo una funzione chiamata Less che accetta due argomenti Sizer e restituisce il più piccolo:

main.go
...
func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}
...

Notiamo che non accettiamo solo i parametri come tipi Sizer, ma anche il risultato come Sizer! Ci significa che non restituzioniamo più un Square o un Circle, ma piuttosto l’interfaccia di Sizer.

Infine stampiamo quale ha avuto l’area più piccola:

Output
{Width:5 Height:10} is the smallest

Poi, aggiungeremo un altro comportamento a ciascun tipo. Questa volta aggiungeremo il metodo String() che restituisce una stringa. questo soddisfarà l’interfaccia fmt.Stringer:

main.go
package main

import (
	"fmt"
	"math"
)

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
	Width  float64
	Height float64
}

func (s Square) Area() float64 {
	return s.Width * s.Height
}

func (s Square) String() string {
	return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
	Area() float64
}

type Shaper interface {
	Sizer
	fmt.Stringer
}

func main() {
	c := Circle{Radius: 10}
	PrintArea(c)

	s := Square{Height: 10, Width: 5}
	PrintArea(s)

	l := Less(c, s)
	fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
	if s1.Area() < s2.Area() {
		return s1
	}
	return s2
}

func PrintArea(s Shaper) {
	fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Poiché sia il tipo Circle che il tipo Square implementano sia il metodo Area che il metodo String, ora possiamo creare un’altra interfaccia per descrivere quell’insieme più ampio di comportamenti. Per fare questo, creeremo un’interfaccia chiamata Shaper. Composeremo questa dell’interfaccia Sizer e dell’interfaccia fmt.Stringer:

main.go
...
type Shaper interface {
	Sizer
	fmt.Stringer
}
...

Nota: è considerato idiomatico cercare di chiamare la tua interfaccia finendo con er, come in fmt.Stringer, io.Writer, ecc. questo è il motivo per cui abbiamo chiamato la nostra interfaccia Shaper e non Shape.

Ora possiamo creare una funzione chiamata PrintArea che riceve come argomento un Shaper. questo significa che possiamo chiamare entrambi i metodi sull’argomento passato per entrambi i metodi Area e String:

main.go
...
func PrintArea(s Shaper) {
	fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Se eseguiamo il programma, ricevermo il seguente output:

Output
area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

Abbiamo ora visto come possiamo creare interfacce più piccole e costruirle ininterfacce più grandi come necessario. Anche se avremmo potuto cominciare con l’interfaccia più grande e passarla a tutte le nostre funzioni, è considerata la migliore pratica inviare solo l’interfaccia più piccola a una funzione che è necessaria. questotypicamente risulta in codice più chiaro, poiché tutto ciò che accetta una specifica interfaccia più piccola intende solo lavorare con quel comportamento definito.

Per esempio, se passassimo Shaper alla funzione Less, potremmo supporre che si va ad invocare sia il metodo Area che String. Tuttavia, poiché solo intendiamo invocare il metodo Area, fa sembrare la funzione Less più chiara, dato che sappiamo che possiamo invocarlo solo su un argomento passato a essa.

Conclusione

Abbiamo visto come creare interfacce più piccole e costruirlle per ottenere interfacce più grandi consente di condividere solo quello che è necessario a una funzione o metodo. Abbiamo anche imparato che possiamo componer le nostre interfacce da altre interfacce, incluse quelle definite da pacchetti differenti, non solo da quelli del nostro pacchetto.

Se vuoi apprendere più sul linguaggio di programmazione Go, controlla tutta la serie Come scrivere codice in Go.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go