Comment définir et implémenter une interface Go

Le modèle orienté objet de Go s'articule autour d'interfaces. Personnellement, je pense que les interfaces sont la construction de langage la plus importante et que toutes les décisions en matière de conception doivent d’abord être centrées sur les interfaces. 

Dans ce tutoriel, vous apprendrez ce qu'est une interface, les interfaces de Go, comment implémenter une interface dans Go, et enfin les limites des interfaces par rapport aux contrats..

Qu'est-ce qu'une interface Go?

Une interface Go est un type qui consiste en un ensemble de signatures de méthodes. Voici un exemple d'interface Go:

type Interface sérialisable Serialize () (chaîne, erreur) Erreur de désérialisation (chaîne)

le Sérialisable interface a deux méthodes. le Sérialiser () méthode ne prend aucun argument et retourne une chaîne et une erreur, et le Désérialiser () La méthode prend une chaîne et renvoie une erreur. Si vous avez fait le tour du pâté de maisons, le Sérialisable L’interface est probablement familière aux autres langues et vous pouvez deviner que le Sérialiser () La méthode retourne une version sérialisée de l'objet cible qui peut être reconstruite en appelant Désérialiser () et en passant le résultat de l'appel initial à Sérialiser ().

Notez qu'il n'est pas nécessaire de fournir le mot clé "func" au début de chaque déclaration de méthode. Go sait déjà qu'une interface ne peut contenir que des méthodes et ne nécessite aucune aide de votre part en lui disant que c'est une "fonction".

Go Interfaces Meilleures Pratiques

Les interfaces Go sont le meilleur moyen de construire l’ossature de votre programme. Les objets doivent interagir les uns avec les autres par le biais d'interfaces et non d'objets concrets. Cela signifie que vous devez créer pour votre programme un modèle objet composé uniquement d'interfaces et de types de base ou d'objets de données (structures dont les membres sont des types de base ou d'autres objets de données). Voici quelques-unes des meilleures pratiques à suivre avec les interfaces.

Intentions claires

Il est important que l'intention derrière chaque méthode et la séquence d'appels soient claires et bien définies à la fois pour les appelants et pour les développeurs. Il n'y a pas de support de niveau de langue dans Go for that. J'en discuterai plus dans la section "Interface vs Contrat" ​​plus tard.

Injection de dépendance

L'injection de dépendance signifie qu'un objet qui interagit avec un autre objet via une interface obtiendra l'interface de l'extérieur en tant que fonction ou argument de méthode et ne créera pas l'objet (ni n'appellera une fonction qui renvoie l'objet concret). Notez que ce principe s’applique aussi aux fonctions autonomes et pas seulement aux objets. Une fonction doit recevoir toutes ses dépendances en tant qu'interfaces. Par exemple:

tapez SomeInterface DoSomethingAesome () func foo (s SomeInterface) s.DoSomethingAwesome () 

Maintenant, vous appelez la fonction foo () avec différentes implémentations de Une interface, et ça marchera avec tous.

Des usines

Évidemment, quelqu'un doit créer les objets concrets. C'est le travail des objets d'usine dédiés. Les usines sont utilisées dans deux situations:

  1. Au début du programme, les fabriques sont utilisées pour créer tous les objets de longue durée dont la durée de vie correspond généralement à la durée de vie du programme..
  2. Pendant l'exécution du programme, divers objets doivent souvent instancier des objets de manière dynamique. Les usines doivent également être utilisées à cette fin.

Il est souvent utile de fournir des interfaces de fabrique dynamiques aux objets pour conserver le modèle d'interaction constitué uniquement d'interface. Dans l'exemple suivant, je définis un Widget interface et un WidgetFactory interface qui retourne un Widget interface de son CreateWidget () méthode. 

le PerformMainLogic () la fonction reçoit un WidgetFactory interface de son appelant. Il est maintenant en mesure de créer dynamiquement un nouveau widget en fonction de ses spécifications de widget et d'invoquer son contenu. Widgetize () méthode sans rien connaître de son type concret (quelle structure implémente l'interface).

type interface de widget Widgetize () type interface de WidgetFactory CreateWidget (chaîne widgetSpec) (widget, erreur) func PerformMainLogic (fabrique WidgetFactory) … widgetSpec: = GetWidgetSpec () widget: = factroy.CreateWidget (widgetSpec) widget.Widgetize ( ) 

Testabilité

La testabilité est l’une des pratiques les plus importantes pour un développement logiciel approprié. Les interfaces Go sont le meilleur mécanisme pour prendre en charge la testabilité dans les programmes Go. Pour tester minutieusement une fonction ou une méthode, vous devez contrôler et / ou mesurer toutes les entrées, les sorties et les effets secondaires de la fonction testée.. 

Pour le code non trivial qui communique directement avec le système de fichiers, l'horloge système, les bases de données, les services distants et l'interface utilisateur, il est très difficile à atteindre. Mais si toutes les interactions passent par des interfaces, il est très facile de se moquer et de gérer les dépendances externes. 

Prenons une fonction qui n’exécute que la fin du mois et exécute du code pour nettoyer les transactions incorrectes. Sans interfaces, il faudrait prendre des mesures extrêmes, telles que modifier l'horloge de l'ordinateur pour simuler la fin du mois. Avec une interface qui fournit l’heure actuelle, il vous suffit de passer une structure sur laquelle vous définissez l’heure souhaitée..

Au lieu d'importer temps et appelant directement c'est l'heure(), vous pouvez passer une interface avec un À présent() méthode qui dans la production sera mis en œuvre en envoyant à c'est l'heure(), mais pendant le test sera implémenté par un objet qui retourne une heure fixe pour geler l'environnement de test.

Utiliser une interface Go

Utiliser une interface Go est complètement simple. Vous appelez simplement ses méthodes comme vous appelez n'importe quelle autre fonction. La grande différence est que vous ne pouvez pas être sûr de ce qui va arriver car il peut y avoir différentes implémentations.

Implémentation d'une interface Go

Les interfaces Go peuvent être implémentées en tant que méthodes sur des structures. Considérez l'interface suivante:

type Interface de forme GetPerimeter () int GetArea () int 

Voici deux implémentations concrètes de l'interface Shape:

type Square struct side uint func (s * Square) GetPerimeter () uint return s.side * 4 func (s * Square) GetArea () uint return s.side * s.side type Rectangle struct width uint hauteur uint func (r * Rectangle) GetPerimeter () uint retour (r.width + r.height) * 2 func (r * Rectangle) GetArea () uint return r.width * r.height 

Le carré et le rectangle implémentent les calculs différemment en fonction de leurs champs et de leurs propriétés géométriques. L'exemple de code suivant montre comment renseigner une tranche de l'interface Shape à l'aide d'objets concrets implémentant l'interface, puis effectuer une itération sur la tranche et l'invoquer. GetArea () méthode de chaque forme pour calculer la surface totale de toutes les formes.

func main () shape: = [] Shape & Square side: 2, & Rectangle width: 3, height: 5 var totalArea uint pour _, shape: = formes de la plage totalArea + = shape.GetArea ()  fmt.Println ("Surface totale:", totalArea) 

Implémentation de base

Dans de nombreux langages de programmation, il existe un concept de classe de base qui peut être utilisé pour implémenter une fonctionnalité partagée utilisée par toutes les sous-classes. Go (à juste titre) préfère la composition à l'héritage. 

Vous pouvez obtenir un effet similaire en incorporant une structure. Définissons un Cache struct qui peut stocker la valeur des calculs précédents. Lorsqu'une valeur est extraite de la casse, elle est également imprimée sur l'écran "résultat de cache" et si la valeur n'est pas dans la casse, elle affiche "cache cache" et renvoie -1 (les valeurs valides sont des entiers non signés)..

type Cache struct cache map [chaîne] uint func (c * Cache) GetValue (chaîne de nom) int valeur, ok: = c.cache [nom] si ok fmt.Println ("cache hit") renvoie int ( valeur) else fmt.Println ("cache miss") return -1 func (c * Cache) SetValue (nom chaîne, valeur uint) c.cache [nom] = valeur

Maintenant, je vais incorporer cette mémoire cache dans les formes carrée et rectangle. Notez que la mise en œuvre de GetPerimeter () et GetArea () vérifie maintenant d'abord le cache et calcule la valeur uniquement si elle n'est pas dans le cache.

type Square struct Cache uint func (s * Square) GetPerimeter () uint valeur: = s.GetValue ("périmètre") si valeur == -1 valeur = int (s.side * 4) s.SetValue ("périmètre", uint (valeur)) retour uint (valeur) func (s * carré) GetArea () uint valeur: = s.GetValue ("zone") si valeur == -1 valeur = int ( s.side * s.side) s.SetValue ("area", uint (value)) return uint (value) type Rectangle struct Largeur du cache uint hauteur uint func (r * Rectangle) GetPerimeter () uint valeur : = r.GetValue ("périmètre") si valeur == -1 valeur = int (r.width + r.height) * 2 r.SetValue ("périmètre", uint (valeur)) retourne uint (valeur)  func (r * Rectangle) GetArea () uint valeur: = r.GetValue ("area") si value == -1 valeur = int (r.width * r.height) r.SetValue ("area", uint (valeur)) retour uint (valeur)

Finalement, le principale() la fonction calcule la surface totale deux fois pour voir l'effet de cache.

func main () shape: = [] Shape & Square Cache cache: make (map [chaîne] uint), 2, & Rectangle Cache cache: make (carte [chaîne] uint), 3, 5  var totalArea uint pour _, shape: = formes de la plage totalArea + = shape.GetArea () fmt.Println ("Surface totale:", totalArea) totalArea = 0 pour _, shape: = formes de la plage totalArea + = shape.GetArea () fmt.Println ("Surface totale:", totalArea) 

Voici la sortie:

cache miss cache miss zone totale: 19 cache hit cache cache superficie totale: 19

Interface vs contrat

Les interfaces sont géniales, mais elles ne garantissent pas que les structures implémentant l'interface remplissent réellement l'intention qui se cache derrière l'interface. Go n’a aucun moyen d’exprimer cette intention. Tout ce que vous spécifiez est la signature des méthodes. 

Pour aller au-delà de ce niveau de base, vous avez besoin d'un contrat. Un contrat pour un objet spécifie exactement ce que fait chaque méthode, quels effets secondaires sont effectués et quel est l'état de l'objet à chaque instant. Le contrat existe toujours. La seule question est de savoir si c'est explicite ou implicite. En ce qui concerne les API externes, les contrats sont essentiels.

Conclusion

Le modèle de programmation Go a été conçu autour d'interfaces. Vous pouvez programmer en Go sans interfaces, mais vous perdriez leurs nombreux avantages. Je vous recommande vivement de tirer pleinement parti des interfaces dans vos aventures de programmation de Go.