Soy culpable, me gusta poner títulos rimbombantes. En este caso no me siento tan mal porque el título ahorrará a más de uno la necesidad de hacer click por pura curiosidad 😉
Lo que pretendo en este es combinar los conceptos de los dos últimos posts (composición e interfaces), y presentar algunas implicaciones de esa unión.
Recordando las interfaces
Para los despistados, vamos a definir una interfaz y un tipo que cumpla el interfaz.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
type motor struct { | |
ruido string | |
} | |
func (n motor) hacerRuido() string { | |
return fmt.Sprintf("Este motor hace: %s", n.ruido) | |
} | |
type cosaRuidosa interface { | |
hacerRuido() string | |
} | |
func main() { | |
var ruidoso cosaRuidosa | |
ruidoso = motor{"¡Brrroum!"} | |
fmt.Println(ruidoso.hacerRuido()) | |
} |
La salida es:
Este motor hace: ¡Brrroum!
Lo más importante de este ejemplo es comprobar que NO hay ninguna referencia en ningún sitio a que el tipo naveEspacial
sea una cosaRuidosa
.
No hace falta decir que la interfaz cosaRuidosa
tendrá un tipo que lo implemente, ni decir que el tipo motor
implementa esa interfaz. No hay ninguna fórmula equivalente a implements
de Java; simplemente no es necesario.
En Go, basta con que un tipo cumpla un interfaz. No hace falta decírselo a nadie; el compilador es suficientemente listo como para darse cuenta.
Independencia entre interfaces, tipos y paquetes
El principal beneficio de la satisfacción implícita de interfaces (que no haya que poner implements
en cada clase que implementa una interfaz) es que se eliminan dependencias.
En Java, si queremos decir que una clase cumple un interfaz, es necesario modificar el código de la clase. Esto crea dos complicaciones:
- Se crea una dependencia entre el interfaz y la clase: si se modifica la interfaz, habrá que modificar la clase forzosamente.
- Las interfaces que implementa una clase tienen que definirse antes que las clases que las implementan. Este tema puede parecer trivial, pero veremos situaciones en las que nos viene muy bien definir el interfaz a posteriori.
Añadiendo la composición a la ecuación
Vamos a añadir un tipo compuesto al ejemplo anterior y veremos qué pasa. Hay un cambio sutil en la definición del motor. Antes utilizábamos una variable de tipo ruidoso
y ahora utilizamos concretamente un motor
. Espero que este detalle no despiste a nadie.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
type motor struct { | |
ruido string | |
} | |
func (n motor) hacerRuido() string { | |
return fmt.Sprintf("Este motor hace: %s", n.ruido) | |
} | |
type cosaRuidosa interface { | |
hacerRuido() string | |
} | |
type naveEspacial struct { | |
numLasers int | |
capcidadPasajeros int | |
motor | |
} | |
func main() { | |
var miMotor = motor{"¡Brrroum!"} | |
var miNave = naveEspacial{4, 2, miMotor} | |
fmt.Println(miNave.hacerRuido()) | |
} |
La salida es la misma que antes:
Este motor hace: ¡Brrroum!
Atención a lo siguiente:
- Como
naveEspacial
está compuesto pormotor
, tiene todas las funciones asociadas amotor
- Como
naveEspacial
tiene todas las funciones asociadas amotor
, cumple todos los interfaces que cumplemotor
De este modo, logramos cumplir todas las interfaces de los tipos que usamos en la composición.
Definiendo interfaces a posteriori
Uno de los usos más habituales (en mi caso) del desacoplamiento entre interfaces y tipos es la definición de interfaces para hacer testing.
Como no hemos entrado en los temas de testing, y no quiero introducir más de un concepto cada vez, voy a utilizar un fichero estándar con un main
para ilustrar este caso.
Vamos a «lanzar una prueba» sobre nuestro tipo motor
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
type motor struct { | |
ruido string | |
} | |
func (m motor) hacerRuido() string { | |
return fmt.Sprintf("Este motor hace: %s", m.ruido) | |
} | |
type ruidoso interface { | |
hacerRuido() string | |
} | |
type mockMotor string | |
func (mm mockMotor) hacerRuido() string { | |
return string(mm) | |
} | |
func main() { | |
var miMotor = motor{"¡Brrroum!"} | |
var ruidoResultante = mockMotor("Este motor hace: ¡Brrroum!") | |
fmt.Println("Probando ruido del motor") | |
if ejercitarRuido(miMotor) == ejercitarRuido(ruidoResultante) { | |
fmt.Println("Todo OK") | |
} else { | |
fmt.Println("Este motor no suena bien") | |
} | |
} | |
func ejercitarRuido(r ruidoso) string { | |
return r.hacerRuido() | |
} |
Por supuesto, la prueba es correcta:
Probando ruido del motor Todo OK
De este ejemplo quiero que prestemos atención a lo siguiente:
- Hemos definido el interfaz
ruidoso
, el tipomockMotor
y la funciónejercitarRuido
después de definirmotor
. En un caso más real, lo definiríamos en otro fichero - La función
ejercitarRuido
no sabe si lo que recibe es un mock (una implementación simplificada) o la implementación real
Lo que nos ha posibilitado trabajar de esta manera ha sido la definición a posteriori de un interfaz que contenga las operaciones de la clase que queremos probar.
Y hasta aquí la unión de composición e interfaces. Espero que no haya nada extraño, ya que todos los conceptos se han visto antes. Si lo ha habido, dad uso a la sección de comentarios 😉