El gran dilema de la programación orientada a objetos ¿herencia o composición?, es un debate que suele ocurrir constantemente, pero que Go, para bien o para mal (creo que para bien) ha decidido que no ocurra en su lenguaje, y es que en Go la herencia no existe, sólo la composición.
La composición significa utilizar objetos dentro de otros objetos. Y en Go no puede hacerse más sencillo.
Composición por structs incrustados(embedding structs)
Una de las formas de composición que ofrece Go es a través de incrustar un typed struct
dentro de otro.
Podemos tirar del ejemplo clásico de herencia de animales, para ilustrar dicho comportamiento.
Primero que nada vamos a crear un tipo struct animal
como aprendimos en el post sobre structs:
package main
import (
"fmt"
)
func main() {
a := &animal{"Gopher", "Go Gopher"}
a.Name()
a.Species()
}
type animal struct {
name string
species string
}
func (a *animal) Name() {
fmt.Println(a.name)
}
func (a *animal) Species() {
fmt.Printf("%s belongs to %s species\n", a.name, a.species)
}
Como vemos tenemos un tipo struct animal
con los campos name
y species
. Además hemos añadido dos métodos Name()
y Species()
que pintarán los datos de nuestro struct.
El próximo paso lógico es crear un animal que se componga de dicho struct, y para ello usaremos a nuestro animal favorito, el Gopher
type gopher struct {
color string
animal
}
func (g *gopher) Color() {
fmt.Printf("%s is the color: %s\n", g.name, g.color)
}
¿Qué hemos hecho aquí? Lo primero, hemos añadido un color a nuestro nuevo struct de gopher
, sabemos que es muy importante en los gopher su color, además le hemos incrustado anónimamente el atributo animal
, esta construcción es lo que nos informa de que gopher
está compuesto de animal
. Gracias a esto gopher
puede acceder a los atributos y métodos de animal
.
Para ilustrar nuestro ejemplo hemos creado el método Color()
el cual hace uso de uno de los atributos de animal
.
Veamos ahora como hacemos la construcción de nuestro nuevo gopher
package main
import (
"fmt"
)
func main() {
a := animal{"Gopher", "Go Gopher"}
g := &gopher{"blue", a}
g.Name()
g.Species()
g.Color()
}
type animal struct {
name string
species string
}
func (a *animal) Name() {
fmt.Println(a.name)
}
func (a *animal) Species() {
fmt.Printf("%s belongs to %s species\n", a.name, a.species)
}
type gopher struct {
color string
animal
}
func (g *gopher) Color() {
fmt.Printf("%s is the color: %s\n", g.name, g.color)
}
Go playground: https://play.golang.org/p/68ZEIxuvgEu
¿Adivináis cuál sera el output de nuestro código?
¿Mola no? Pero aún podemos hacer más cosas con nuestras composiciones, por ejemplo, además de poder acceder a los métodos del objeto del cual se compone, podemos, sobreescribirlo.
¿Qué pasa si añadimos el siguiente método a nuestro gopher
?
func (g *gopher) Name() {
fmt.Printf("I'm a %s but my real name is %s\n", g.name, "Jack")
}
Output:
Go playground: https://play.golang.org/p/E4ZZNmEV713
Ahora mismo muchos de vosotros estaréis pensando, que esto es muy similar a la herencia, pero tenemos que recordar qué es la composición:
La composición significa utilizar objetos dentro de otros objetos.
Como hemos visto para construir un gopher
hemos necesitado previamente de construir un animal
, en herencia esto no es necesario. Además hay un matiz importante, no podemos sustituir un gopher
por un animal
func main() {
a := animal{"Gopher", "Go Gopher"}
g := &gopher{"blue", a}
g.Name()
g.Species()
g.Color()
New(g)
}
func New(a animal) {
fmt.Println("I'm similar to inheritance, but I'm composition")
}
Si esto fuera una herencia, el código debería funcionar en cambio, si ejecutamos nuestro código obtenemos el siguiente mensaje de error:
Go playground: https://play.golang.org/p/Z0p3mNu7FZh
La composición es realmente potente, y fácil de usar en Go y ahora ya sabéis como utilizarla a nivel de structs, pero aún hay más, veamos como funciona a nivel de interfaces
Composición por interfaces
El uso de interfaces en Go es algo diferente, sabemos que son implícitas, es decir si nuestros tipos la cumplen, estarán implementándola automáticamente. Además se aconseja que cuando van a ser exportadas para ser utilizadas por terceros, sean lo más pequeñas y concretas posibles. Así pues podemos encontrarnos con interfaces muy conocidas como son:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Seguro que si habéis leído más filosofía sobre Go os habréis encontrado con estos ejemplos de la librería oficial, y es que son el ejemplo perfecto de como deben ser las interfaces en Go para poder reaprovechar mejor nuestro código.
Además si seguimos investigando en ese mismo paquete que las contiene, el paquete io
, podremos ver como se utiliza la composición a nivel de interfaces.
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
Así pues si tenemos el siguiente código, estará implementando la interfaz compuesta, ReadWriter
:
type gopher struct {
}
func (g *gopher) Write(p []byte) (n int, err error) {
return 0, nil
}
func (g *gopher) Read(p []byte) (n int, err error) {
return 0, nil
}
Pero no lo dejemos sólo en suposiciones, vamos a comprobar que de verdad implementa dicha interfaz:
package main
import (
"fmt"
"io"
)
func main() {
g := &gopher{}
fetchType(g)
}
func fetchType(i interface{}) {
switch i.(type) {
case io.ReadWriter:
fmt.Println("Implementing ReadWriter interface")
default:
fmt.Println("I'm not implementing ReadWriter interface")
}
}
type gopher struct {
}
func (g *gopher) Write(p []byte) (n int, err error) {
return 0, nil
}
func (g *gopher) Read(p []byte) (n int, err error) {
return 0, nil
}
Go playground: https://play.golang.org/p/pHWQJGdtOFC
Obviamente, no hay que decir que nuestro gopher
también implementa Writer
y Reader
Y hasta aquí llegamos con la composición, en el próximo artículo y último de orientación a objetos hablaremos mucho más sobre las interfaces en Go y como hacer polimorfismo.
Como siempre, podéis dejar vuestras dudas en los comentarios o en nuestro twitter @FriendsofGoTech