No hace mucho tiempo, un colega retuiteó una excelente publicación
Cómo usar las interfaces Go . Discute algunos errores al usar las interfaces en Go, y también da algunas recomendaciones sobre cómo seguir usándolos.
En el artículo mencionado anteriormente, el autor cita la interfaz del paquete de
clasificación de la biblioteca estándar como un ejemplo de un tipo de datos abstracto. Sin embargo, me parece que tal ejemplo no revela muy bien la idea cuando se trata de aplicaciones reales. Especialmente sobre aplicaciones que implementan la lógica de un campo de negocios o resuelven problemas del mundo real.
Además, cuando se usan interfaces en Go, a menudo hay un debate sobre la sobreingeniería. Y también sucede que, después de leer este tipo de recomendaciones, las personas no solo dejan de abusar de las interfaces, sino que también intentan abandonarlas por completo, privándose de usar uno de los conceptos de programación más fuertes en principio (y uno de los puntos fuertes de Go in particular). Sobre el tema de los errores típicos en Go, por cierto, hay un
buen informe de Stive Francia de Docker. Allí, en particular, las interfaces se mencionan varias veces.
En general, estoy de acuerdo con el autor del artículo. Sin embargo, me pareció que el tema del uso de interfaces como tipos de datos abstractos se reveló de manera bastante superficial, por lo que me gustaría desarrollarlo un poco y reflexionar sobre este tema con usted.
Consulte el original
Al comienzo del artículo, el autor da un pequeño ejemplo de código, con la ayuda del cual señala errores al usar interfaces que los desarrolladores suelen hacer. Aquí está el código.
package animal type Animal interface { Speaks() string }
package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() }
El autor llama a este enfoque
"uso de interfaz estilo Java" . Cuando declaramos una interfaz, implementamos el único tipo y métodos que satisfarán esta interfaz. Estoy de acuerdo con el autor, el enfoque es regular, el código más idiomático en el artículo original es el siguiente:
package animal
package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
Aquí, en general, todo es claro y comprensible. La idea básica:
"Primero declare los tipos, y solo luego declare las interfaces en el punto de uso" . Esto es correcto Pero ahora desarrollemos una pequeña idea con respecto a cómo puede usar interfaces como tipos de datos abstractos. El autor, por cierto, señala que en tal situación no hay nada de malo en declarar la interfaz
"por adelantado" . Trabajaremos con el mismo código.
Juguemos con abstracciones
Entonces tenemos un circo y hay animales. Dentro del circo hay un método bastante abstracto
llamado `Perform` , que toma la interfaz`
Speaker` y hace que la mascota haga sonidos. Por ejemplo, hará que el perro ladre del ejemplo anterior. Crea un domador de animales. Como no es tonto aquí, generalmente también podemos hacer que haga sonidos. Nuestra interfaz es bastante abstracta. :)
package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" }
Hasta ahora todo bien. Vamos más lejos ¿Enseñamos a nuestro domador a dar órdenes a las mascotas? Hasta ahora, tendremos un comando de
voz . :)
package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" }
package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d)
Mmmm, ¿no es interesante? ¿Parece que nuestro colega no está contento de que se haya convertido en una mascota en este contexto? : D ¿Qué hacer?
El orador parece que una abstracción no es muy adecuada aquí. Crearemos una más adecuada (o mejor dicho, devolveremos de alguna manera la primera versión del
"ejemplo incorrecto" ), después de lo cual cambiaremos la notación del método.
package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { }
Esto no cambia nada, dices, el código aún se ejecutará, porque ambas interfaces implementan un método, y usted tendrá razón en general.
Sin embargo, este ejemplo captura una idea importante. Cuando hablamos de tipos de datos abstractos, el contexto es crucial. La introducción de una nueva interfaz, al menos, hizo que el código en un orden de magnitud sea más obvio y legible.
Por cierto, una de las formas de obligar al domador a no ejecutar el comando de
"voz" es simplemente agregar un método que no debería tener. Agreguemos dicho método, dará información sobre si la mascota es entrenable.
package circus type Animal interface { Speaker IsTrained() bool }
Ahora el domador no puede deslizarse en lugar de una mascota.
Ampliar comportamiento
Forzaremos a nuestras mascotas, para variar, a ejecutar otros comandos, además, agreguemos un gato.
package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" }
package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" }
Genial, ahora podemos dar diferentes órdenes a nuestros animales, y ellos los llevarán a cabo. En un grado u otro ...: D
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d)
Nuestros gatos domésticos no son particularmente susceptibles a la formación. Por lo tanto, ayudaremos al domador y nos aseguraremos de que no sufra con ellos.
package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") }
Eso esta mejor. A diferencia de la interfaz
Animal inicial, que duplica
Speaker , ahora tenemos la interfaz
`Animal` (que es esencialmente un tipo de datos abstracto) que implementa un comportamiento bastante significativo.
Discutamos los tamaños de interfaz
Ahora reflexionemos sobre un problema como el uso de interfaces amplias.
Esta es una situación en la que utilizamos interfaces con una gran cantidad de métodos. En este caso, la recomendación es algo como esto:
"Las funciones deben aceptar interfaces que contengan los métodos que necesitan" .
En general, estoy de acuerdo en que las interfaces deben ser pequeñas, pero en este caso, el contexto nuevamente importa. Volvamos a nuestro código y enseñemos a nuestro domador a
"alabar" a su mascota.
En respuesta a los elogios, la mascota emitirá una voz.
package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() }
Parece que todo está bien, utilizamos la interfaz mínima necesaria. No hay nada superfluo. Pero aquí nuevamente el problema. Maldita sea, ahora podemos
"alabar" al otro entrenador y él
"dará una voz" . : D Grab it? .. El contexto siempre importa.
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d)
¿Por qué soy yo? En este caso, la mejor solución sería utilizar una interfaz más amplia (que representa el tipo de datos abstractos
"mascota" ). Como queremos aprender a alabar a una mascota, no a ninguna criatura que pueda hacer sonidos.
package circus
Mucho mejor Podemos alabar a la mascota, pero no podemos alabar al domador. El código nuevamente se volvió más simple y más obvio.
Ahora un poco sobre la Ley de la Cama
El último punto al que me gustaría referirme es la recomendación de que deberíamos aceptar un tipo abstracto y devolver una estructura específica. En el artículo original, esta mención se da en la sección que describe la llamada
Ley de Postel .
El autor cita la propia ley:.
“Sé conservador con lo que haces, sé liberal con lo que aceptas”
Y lo interpreta en relación con el lenguaje Go
"Ir": "Aceptar interfaces, devolver estructuras"
func funcName(a INTERFACETYPE) CONCRETETYPE
Sabes, en general, estoy de acuerdo, esta es una buena práctica. Sin embargo, quiero enfatizar nuevamente. No lo tomes literalmente. El diablo está en los detalles. Como siempre, el contexto es importante.
No siempre una función debe devolver un tipo específico. Es decir Si necesita un tipo abstracto, devuélvalo. No es necesario intentar reescribir el código evitando la abstracción.
Aquí hay un pequeño ejemplo. Un elefante apareció en un circo
"africano" cercano, y usted pidió a los propietarios del circo que prestaran un elefante a un nuevo espectáculo. Para ti, en este caso es importante, solo que el elefante puede ejecutar los mismos comandos que otras mascotas. El tamaño de un elefante o la presencia de una trompa en este contexto no importa.
package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} }
package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e)
Como puede ver, dado que no nos importan los parámetros específicos de un elefante que lo distingue de otras mascotas, bien podemos usar la abstracción, y devolver la interfaz en este caso será bastante apropiado.
Para resumir
El contexto es crucial cuando se trata de abstracciones. No descuides las abstracciones y tenles miedo, al igual que no debes abusar de ellas. No debe tomar recomendaciones como reglas. Hay enfoques que han sido probados por el tiempo; hay enfoques que aún no se han probado. Espero haber podido profundizar un poco más sobre el tema del uso de interfaces como tipos de datos abstractos y alejarme de los ejemplos habituales de la biblioteca estándar.
Por supuesto, para algunas personas esta publicación puede parecer demasiado obvia, y los ejemplos son extraídos del dedo. Para otros, mis pensamientos pueden ser controvertidos y los argumentos poco convincentes. Sin embargo, alguien puede estar inspirado y comenzar a pensar un poco más profundo no solo sobre el código, sino también sobre la esencia de las cosas, así como las abstracciones en general.
Lo principal, amigos, es que constantemente se desarrolla y recibe el verdadero placer del trabajo. Bueno para todos!
PS. El código de muestra y la versión final se pueden encontrar
en GitHub .