Dans ses temps libres de l'œuvre principale, l'auteur du matériel consulte Go et analyse le code. Naturellement, au cours d'une telle activité, il lit beaucoup de code écrit par d'autres personnes. Récemment, l'auteur de cet article a l'impression (oui, l'impression, pas de statistiques) que les programmeurs sont plus susceptibles de travailler avec des interfaces de «style Java».
Cet article contient des recommandations de l'auteur sur l'utilisation optimale des interfaces dans Go, basées sur son expérience dans l'écriture de code.Dans les exemples de ce billet, nous utiliserons deux packages
animal
et
circus
. Beaucoup de choses dans ce post décrivent le travail avec du code à la limite de l'utilisation régulière des packages.
Comment ne pas faire
Un phénomène très courant que j'observe:
package animals type Animal interface { Speaks() string }
package circus import "animals" func Perform(a animal.Animal) string { return a.Speaks() }
C'est ce qu'on appelle l'utilisation d'interfaces de style Java. Il peut être caractérisé par les étapes suivantes:
- Définissez une interface.
- Définissez un type qui satisfait le comportement de l'interface.
- Définissez des méthodes qui satisfont l'implémentation de l'interface.
En résumé, nous avons affaire à des «types d'écriture qui satisfont les interfaces». Ce code a sa propre
odeur distincte, suggérant les pensées suivantes:
- Un seul type satisfait l'interface, sans aucune intention de l'étendre davantage.
- Les fonctions prennent généralement des types concrets au lieu de types d'interface.
Comment faire à la place
Les interfaces dans Go encouragent une approche paresseuse, ce qui est bien. Au lieu d'écrire des types qui satisfont aux interfaces, vous devez écrire des interfaces qui répondent à de réelles exigences pratiques.
Ce qui signifie: au lieu de définir
Animal
dans le package
animals
, définissez-le au point d'utilisation, c'est-à-dire le package
circus
* .
package animals type Dog struct{} func (a Dog) Speaks() string { return "woof" }
package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
Une façon plus naturelle de procéder est la suivante:
- Définir les types
- Définissez l'interface au point d'utilisation.
Cette approche réduit la dépendance à l'égard des composants de l'emballage des
animals
. Réduire les dépendances est le bon moyen de créer un logiciel tolérant aux pannes.
La loi du lit
Il existe un bon principe pour écrire un bon logiciel. C'est la
loi de Postel , qui est souvent formulée comme suit:
"Soyez conservateur sur ce que vous référez et libéral sur ce que vous acceptez"
En termes de Go, la loi est:
"Accepter les interfaces, retourner les structures"
Dans l'ensemble, c'est une très bonne règle pour concevoir des choses stables et tolérantes aux pannes
* . L'unité principale de code dans Go est une fonction. Lors de la conception de fonctions et de méthodes, il est utile de respecter le modèle suivant:
func funcName(a INTERFACETYPE) CONCRETETYPE
Ici, nous acceptons tout ce qui implémente une interface qui peut être n'importe quoi, y compris une interface vide. Une valeur d'un type spécifique est suggérée. Bien sûr, limiter ce qui peut être logique est logique. Comme le dit un proverbe Go:
"L'interface vide ne dit rien", Rob Pike
Par conséquent, il est fortement conseillé d'empêcher les fonctions d'accepter l'
interface{}
.
Exemple d'application: imitation
Les cas types constituent un exemple frappant des avantages de l'application de la loi Postel. Disons que vous avez une fonction qui ressemble à ceci:
func Takes(db Database) error
Si la
Database
est une interface, alors dans le code de test, vous pouvez simplement fournir une imitation de l'implémentation de la
Database
sans avoir à passer un véritable objet DB.
Lorsque la définition d'une interface à l'avance est acceptable
Pour vous dire la vérité, la programmation est un moyen assez libre d'exprimer des idées. Il n'y a pas de règles inébranlables. Bien sûr, vous pouvez toujours définir des interfaces à l'avance, sans craindre d'être arrêté par la police du code. Dans le contexte de nombreux packages, si vous connaissez vos fonctions et avez l'intention d'accepter une interface spécifique au sein du package, faites-le.
Définir une interface dégage généralement une ingénierie excessive, mais il y a des situations dans lesquelles vous devriez évidemment faire exactement cela. En particulier, les exemples suivants me viennent à l'esprit:
- Interfaces scellées
- Types de données abstraits
- Interfaces récursives
Ensuite, nous examinons brièvement chacun d'eux.
Interfaces scellées
Les interfaces scellées ne peuvent être discutées que dans le contexte de plusieurs packages. Une interface scellée est une interface avec des méthodes non exportées. Cela signifie que les utilisateurs en dehors de ce package ne peuvent pas créer de types qui satisfont cette interface. Ceci est utile pour émuler un type variant afin de rechercher de manière exhaustive les types satisfaisant l'interface.
Si vous avez défini quelque chose comme ceci:
type Fooer interface { Foo() sealed() }
Seul le package défini par
Fooer
peut l'utiliser et en créer quelque chose de valeur. Cela vous permet de créer des opérateurs de commutateur à force brute pour les types.
L'interface scellée permet également aux outils d'analyse de détecter facilement toutes les correspondances de motif de non-collision.
Le package sumtypes de BurntSushi vise à résoudre ce problème.
Types de données abstraits
Un autre cas de définition préalable d'une interface implique la création de types de données abstraits. Ils peuvent être scellés ou non scellés.
Un bon exemple de ceci est le package de
sort
, qui fait partie de la bibliothèque standard. Il définit une collection triable comme suit
type Interface interface {
Ce morceau de code a bouleversé beaucoup de gens, car si vous voulez utiliser le package de
sort
, vous devrez implémenter des méthodes pour l'interface. Beaucoup n'aiment pas la nécessité d'ajouter trois lignes de code supplémentaires.
Cependant, je trouve que c'est une forme très élégante de génériques dans Go. Son utilisation devrait être plus souvent encouragée.
Des options de conception alternatives et en même temps élégantes nécessiteront des types d'ordre supérieur. Dans cet article, nous ne les examinerons pas.
Interfaces récursives
C'est probablement un autre exemple de code avec un stock, mais il y a des moments où il est tout simplement impossible d'éviter de l'utiliser. Des manipulations simples vous permettent d'obtenir quelque chose comme
type Fooer interface { Foo() Fooer }
Un modèle d'interface récursif nécessitera évidemment sa définition à l'avance. La recommandation de définition d'interface de point d'utilisation n'est pas applicable ici.
Ce modèle est utile pour créer des contextes contenant des travaux ultérieurs. Le code chargé par le contexte s'enferme généralement à l'intérieur du package avec l'exportation uniquement des contextes (ala le package
tensor ), donc, dans la pratique, je rencontre ce cas pas si souvent. Je peux vous dire autre chose sur les modèles contextuels, mais laissez-le pour un autre post.
Conclusion
Malgré le fait que l’une des rubriques de l’article soit «Comment ne pas le faire», je n’essaie en aucun cas d’interdire quoi que ce soit. Je veux plutôt inciter les lecteurs à réfléchir plus souvent aux conditions frontalières, car c'est dans de tels cas que diverses situations d'urgence surviennent.
Je trouve le principe de la déclaration du point d'utilisation extrêmement utile. Du fait de son application dans la pratique, je ne rencontre pas de problèmes qui se posent si je le néglige.
Cependant, j'écris aussi occasionnellement des interfaces de style Java. En règle générale, cela se produit si, peu de temps avant, j'écrivais beaucoup de code en Java ou en Python. Le désir de trop compliquer et de «tout représenter sous forme de classes» est parfois très fort, surtout si vous écrivez du code Go après avoir écrit beaucoup de code orienté objet.
Ainsi, ce message sert également de rappel à soi-même à quoi ressemble le chemin pour écrire du code qui ne causera pas de maux de tête par la suite. En attendant vos commentaires!
