Comment utiliser les interfaces dans Go



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 } //  Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 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:

  1. Définissez une interface.
  2. Définissez un type qui satisfait le comportement de l'interface.
  3. 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:

  1. Définir les types
  2. 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 { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

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!

image

Source: https://habr.com/ru/post/fr411029/


All Articles