Cómo usar interfaces en Go



En su tiempo libre del trabajo principal, el autor del material consulta en Go y analiza el código. Naturalmente, en el curso de tal actividad, lee mucho código escrito por otras personas. Recientemente, el autor de este artículo tiene la impresión (sí, la impresión, sin estadísticas) de que los programadores tienen más probabilidades de trabajar con interfaces en el "estilo Java".

Esta publicación contiene recomendaciones del autor sobre el uso óptimo de las interfaces en Go, en función de su experiencia en la escritura de código.

En los ejemplos de este post usaremos dos paquetes animal y circus . Muchas cosas en esta publicación describen el trabajo con código que limita con el uso regular de paquetes.

Como no hacer


Un fenómeno muy común que observo:

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

Este es el llamado uso de interfaces de estilo Java. Se puede caracterizar por los siguientes pasos:

  1. Definir una interfaz.
  2. Defina un tipo que satisfaga el comportamiento de la interfaz.
  3. Definir métodos que satisfagan la implementación de la interfaz.

En resumen, estamos tratando con "tipos de escritura que satisfacen las interfaces". Este código tiene su propio olor distintivo, lo que sugiere los siguientes pensamientos:

  • Solo un tipo satisface la interfaz, sin ninguna intención de expandirla aún más.
  • Las funciones generalmente toman tipos concretos en lugar de tipos de interfaz.

¿Cómo hacer en su lugar


Las interfaces en Go fomentan un enfoque perezoso, lo cual es bueno. En lugar de escribir tipos que satisfagan las interfaces, debe escribir interfaces que cumplan requisitos prácticos reales.

Lo que significa: en lugar de definir Animal en el paquete de animals , defínalo en el punto de uso, es decir, el paquete de 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() } 

Una forma más natural de hacer esto es así:

  1. Definir tipos
  2. Defina la interfaz en el punto de uso.

Este enfoque reduce la dependencia de los componentes del paquete de animals . Reducir las dependencias es la forma correcta de construir software tolerante a fallas.

Ley de la cama


Hay un buen principio para escribir un buen software. Esta es la ley de Postel , que a menudo se formula de la siguiente manera:
"Sea conservador sobre lo que refiere y liberal sobre lo que acepta"
En términos de Go, la ley es:

"Aceptar interfaces, devolver estructuras"

En general, esta es una regla muy buena para diseñar cosas estables y tolerantes a fallas * . La unidad principal de código en Go es una función. Al diseñar funciones y métodos, es útil cumplir con el siguiente patrón:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

Aquí aceptamos todo lo que implementa una interfaz que puede ser cualquier cosa, incluida una vacía. Se sugiere un valor de un tipo específico. Por supuesto, limitar lo que puede ser tiene sentido. Como dice un proverbio Go:

"La interfaz vacía no dice nada", Rob Pike

Por lo tanto, es muy recomendable evitar que las funciones acepten la interface{} .

Ejemplo de aplicación: imitación


Un ejemplo sorprendente de los beneficios de aplicar la ley Postel son los casos de prueba. Digamos que tiene una función que se ve así:

 func Takes(db Database) error 

Si la Database es una interfaz, en el código de prueba simplemente puede proporcionar una imitación de la implementación de la Database de Database sin la necesidad de pasar un objeto DB real.

Cuando la definición de una interfaz por adelantado es aceptable


A decir verdad, la programación es una forma bastante libre de expresar ideas. No hay reglas inquebrantables. Por supuesto, siempre puede definir interfaces de antemano, sin temor a ser arrestado por el código policial. En el contexto de muchos paquetes, si conoce sus funciones y tiene la intención de aceptar una interfaz específica dentro del paquete, hágalo.

La definición de una interfaz generalmente huele a ingeniería excesiva, pero hay situaciones en las que obviamente debería hacer exactamente eso. En particular, me vienen a la mente los siguientes ejemplos:

  • Interfaces selladas
  • Tipos de datos abstractos
  • Interfaces recursivas

A continuación, consideramos brevemente cada uno de ellos.

Interfaces selladas


Las interfaces selladas solo pueden discutirse en el contexto de múltiples paquetes. Una interfaz sellada es una interfaz con métodos no exportados. Esto significa que los usuarios fuera de este paquete no pueden crear tipos que satisfagan esta interfaz. Esto es útil para emular un tipo de variante para buscar exhaustivamente tipos que satisfagan la interfaz.

Si ha definido algo como esto:

 type Fooer interface { Foo() sealed() } 

Solo el paquete definido por Fooer puede usarlo y crear algo de valor a partir de él. Esto le permite crear operadores de interruptor de fuerza bruta para tipos.

La interfaz sellada también permite que las herramientas de análisis recojan fácilmente cualquier coincidencia de patrones sin colisión. El paquete sumtypes de BurntSushi tiene como objetivo resolver este problema.

Tipos de datos abstractos


Otro caso de definir una interfaz por adelantado implica crear tipos de datos abstractos. Pueden ser sellados o no sellados.

Un buen ejemplo de esto es el paquete de sort , que forma parte de la biblioteca estándar. Define una colección ordenable de la siguiente manera

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

Este fragmento de código molestó a mucha gente, porque si desea utilizar el paquete de sort , deberá implementar métodos para la interfaz. A muchos no les gusta la necesidad de agregar tres líneas de código adicionales.

Sin embargo, creo que esta es una forma muy elegante de genéricos en Go. Su uso debe fomentarse con mayor frecuencia.

Las opciones de diseño alternativas y al mismo tiempo elegantes requerirán tipos de orden superiores. En este post no los consideraremos.

Interfaces recursivas


Este es probablemente otro ejemplo de código con un stock, pero a veces es simplemente imposible evitar usarlo. Manipulaciones simples le permiten obtener algo como

 type Fooer interface { Foo() Fooer } 

Un patrón de interfaz recursivo obviamente requerirá su definición por adelantado. La recomendación de definición de interfaz de punto de uso no es aplicable aquí.

Este patrón es útil para crear contextos con trabajo posterior en ellos. El código cargado por el contexto generalmente se encierra dentro del paquete con la exportación de solo los contextos (ala el paquete tensorial ), por lo que en la práctica no veo este caso con tanta frecuencia. Puedo decirte algo más sobre patrones contextuales, pero déjalo para otra publicación.

Conclusión


A pesar de que uno de los encabezados de la publicación dice "Cómo no hacerlo", de ninguna manera estoy tratando de prohibir nada. Más bien, quiero que los lectores piensen más a menudo sobre las condiciones fronterizas, ya que es en tales casos que surgen varias situaciones de emergencia.

El principio de declaración de punto de uso me parece extremadamente útil. Como resultado de su aplicación en la práctica, no encuentro problemas que surgen si lo descuido.

Sin embargo, ocasionalmente también escribo ocasionalmente interfaces de estilo Java. Por lo general, esto sucede si, poco antes, escribí mucho código en Java o Python. El deseo de complicar demasiado y "representar todo en forma de clases" a veces es muy fuerte, especialmente si escribe código Go después de escribir mucho código orientado a objetos.

Por lo tanto, esta publicación también sirve como un recordatorio para uno mismo sobre cómo se ve el camino para escribir código que, posteriormente, no causará dolor de cabeza. Esperando sus comentarios!

imagen

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


All Articles