Comment utiliser les interfaces en Go

Introduction

Écrire du code souple, réutilisable et modulaire est essentiel pour développer des programmes versatiles. Travailler de cette manière assure que le code est plus facile à maintenir en évitant la nécessité de faire la même modification à plusieurs endroits. La manière dont vous le réalisez varie selon la langue. Par exemple, l’héritage est une approche commune utilisée dans les langages tels que Java, C++, C# et plus.

Les développeurs peuvent également atteindre les mêmes objectifs de conception par la composition. La composition est une manière de combiner des objets ou des types de données pour créer des plus complexes. C’est l’approche que Go utilise pour promouvoir la réutilisation du code, la modularité et la flexibilité. Les interfaces dans Go offrent un moyen d’organiser des compositions complexes, et apprendre comment les utiliser permettra de créer du code commun et réutilisable.

Dans cet article, nous apprendrons comment composer des types personnalisés ayant des comportements communs, ce qui nous permettra de réutiliser notre code. Nous apprendrons également comment implémenter des interfaces pour nos propres types personnalisés pour satisfaire aux interfaces définies dans un autre paquet.

Définir un Comportement

Une des implémentations centrales de la composition est l’utilisation d’interfaces. Une interface définit un comportement d’un type. L’une des interfaces les plus couramment utilisées dans la bibliothèque standard de Go est l’interface fmt.Stringer :

type Stringer interface {
    String() string
}

La première ligne de code définit un type appelé Stringer. Elle indique ensuite qu’il s’agit d’une interface. Comme pour la définition de structures, Go utilise des crochets ({}) pour entourer la définition de l’interface. En comparaison à la définition de structures, nous définissons uniquement le comportement de l’interface; c’est-à-dire, « ce que peut faire ce type ».

Dans le cas de l’interface Stringer, le seul comportement est la méthode String(). La méthode n’accepte aucun argument et retourne une chaîne de caractères.

Ensuite, regardons du code qui possède le comportement 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())
}

La première chose que nous faisons est de créer un nouveau type appelé Article. Ce type a un Title (titre) et un Author (auteur) de champ, et ils sont both de la chaîne de caractères de type données :

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

Ensuite, nous définissons une méthode appelée String sur le type Article. La méthode String retournera une chaîne de caractères représentant le type Article :

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

Alors, dans notre main fonction, nous créeons une instance du type Article et l’affectons à la variable appelée a. Nous fournissons les valeurs "Understanding Interfaces in Go" pour le champ Title et "Sammy Shark" pour le champ Author :

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

Ensuite, nous affichons le résultat de la méthode String en appelant fmt.Println et en passant le résultat de l’appel de la méthode a.String() :

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

Après avoir exécuté le programme, vous verrez le sortie suivante :

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

Jusqu’à présent, nous n’avons pas utilisé d’interface, mais nous avons créé un type qui avait un comportement. Ce comportement correspondait à l’interface fmt.Stringer. Dans la prochaine partie, voyons comment nous pouvons utiliser ce comportement pour rendre notre code plus réutilisable.

Définition d’une Interface

Maintenant que nous avons défini notre type avec le comportement souhaité, nous pouvons regarder comment utiliser ce comportement.

Cependant, avant de le faire, voyons ce que nous aurions besoin de faire si nous voulions appeler la méthode String du type Article dans une fonction :

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())
}

Dans ce code, nous ajoutons une nouvelle fonction appelée Print qui prend un Article en argument. Remarquez que tout ce que la fonction Print fait est appeler la méthode String. En raison de cela, nous pourrions plutôt définir une interface pour l’envoyer à la fonction :

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())
}

Ici, nous avons créé une interface appelée Stringer :

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

L’interface Stringer n’a qu’un seul méthode, appelé String(), qui retourne une string. Une méthode est une fonction spéciale qui est limitée à un type spécifique en Go. Contrairement à une fonction, une méthode ne peut être appelée que de l’instance du type sur lequel elle a été définie.

Nous modifions ensuite la signature de la méthode Print pour qu’elle prenne une Stringer, et non un type concret de Article. Comme le compilateur sait que l’interface Stringer définit la méthode String, il ne接受era que les types qui ont également la méthode String.

A présent, nous pouvons utiliser la méthode Print avec n’importe quoi qui satisfait l’interface Stringer. Faisons maintenant une autre classe pour montrer cela :

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())
}

Nous ajoutons maintenant un second type appelé Book. Il a également une méthode String définie. Cela signifie qu’il satisfait également l’interface Stringer. En raison de cela, nous pouvons également l’envoyer à notre fonction 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.

Nous avons démontré jusqu’à présent comment utiliser simplement une seule interface. Cependant, une interface peut avoir plus d’un comportement défini. Prochaine étape, nous allons voir comment rendre nos interfaces plus flexibles en déclarant plus de méthodes.

Behaviors Multiple in an Interface

Un des principes fondamentaux de l’écriture du code Go consiste à écrire des types petits et concis et les composer pour créer des types plus complexes. La même règle s’applique lorsque vous composez des interfaces. Pour voir comment construisons une interface, nous commencerons par définir seulement une interface. Nous définissons deux formes, un Circle et un Square, qui définissent tous deux une méthode appelée Area. Cette méthode retournera la surface géométrique de leurs formes respectives :

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
}

Parce que chaque type déclare la méthode Area, nous pouvons créer une interface qui définit ce comportement. Nous créons alors l’interface suivante Sizer :

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

Nous définissons ensuite une fonction appelée Less qui prend deux Sizer en paramètres et renvoie le plus petit d’entre eux :

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

Noticez que nous acceptons non seulement les deux arguments comme le type Sizer, mais nous retournons également Sizer en résultat. Cela signifie que nous ne retournons pas un Square ou un Circle, mais plutôt le comportement de Sizer.

Enfin, nous imprimons ce qui a eu la plus petite surface :

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

Ajoutons maintenant un autre comportement à chaque type. Cette fois, nous ajoutons la méthode String() qui retourne une chaîne de caractères. Cela satisfait l’interface 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())
}

Comme les types Circle et Square implémentent à la fois les méthodes Area et String, nous pouvons maintenant créer une autre interface pour décrire ce jeu plus large de comportements. Pour ce faire, nous créerons une interface nommée Shaper. Nous composerons celle-ci des interfaces Sizer et fmt.Stringer :

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

Remarque : Il est considéré comme idiomatique de nommer votre interface en terminant par er, comme dans fmt.Stringer, io.Writer, etc. C’est pourquoi nous avons nommé notre interface Shaper et non pas Shape.

Maintenant, nous pouvons créer une fonction appelée PrintArea qui prend un Shaper en argument. Cela signifie que nous pouvons appeler les deux méthodes sur la valeur passée en argument pour les méthodes Area et String :

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

Si nous exécutons le programme, nous obtiendrons l’output suivant :

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

Nous avons maintenant vu comment nous pouvons créer des interfaces plus petites et les construire ensembles pour former des plus grandes selon nos besoins. Bien que nous aurions pu commencer avec l’interface plus large et l’envoyer à toutes nos fonctions, il est considéré comme une meilleure pratique d’envoyer seulement l’interface la plus petite à une fonction qui est nécessaire. Cela conduit généralement à un code plus clair, car tout ce qui accepte une interface spécifique plus petite n’a qu’une seule intention : travailler avec ce comportement défini.

Par exemple, si nous passions Shaper à la fonction Less, nous pouvons supposer que cela va appeler les méthodes Area et String. Cependant, puisque nous ne voulons que appeler la méthode Area de toute l’argument passé à elle, il rend la fonction Less plus claire car nous savons que nous ne pouvons appeler que la méthode Area de tout argument passé à elle.

Conclusion

Nous avons vu comment créer des interfaces plus petites et les construire pour obtenir des interfaces plus grandes permet d’utiliser seulement ce qui est nécessaire à une fonction ou à une méthode. Nous avons également appris que nous pouvons composer nos interfaces avec d’autres interfaces, y compris celles définies dans d’autres paquets, pas juste dans nos propres paquets.

Si vous voulez en savoir plus sur le langage de programmation Go, consultez la série complète Comment coder en Go.

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