structs - instance struct go
Ir a campos de interfaz (1)
Definitivamente es un truco ingenioso, y funciona siempre y cuando estés tranquilo dando acceso a esos campos como parte de tu API. La alternativa que consideraría es mantener la configuración de estructura / interface
incrustable, pero definiendo la interfaz en términos de getters y setters.
Ocultar las propiedades detrás de getters y setters le da cierta flexibilidad adicional para hacer cambios compatibles hacia atrás más tarde. Supongamos que algún día quiere cambiar Person
para almacenar no solo un campo de "nombre" sino primero / medio / último / prefijo; si tiene los métodos Name() string
y SetName(string)
, puede mantener contentos a los usuarios existentes de la interfaz de la Person
al agregar nuevos métodos más detallados. O puede querer marcar un objeto respaldado por una base de datos como "sucio" cuando tiene cambios no guardados; puedes hacerlo cuando las actualizaciones de datos pasan por los métodos SetFoo()
.
Entonces: con getters / setters, puede cambiar los campos de estructura mientras mantiene una API compatible, y agregar lógica alrededor de la propiedad get / sets ya que nadie puede hacer p.Name = "bob"
sin pasar por su código.
Esa flexibilidad es más relevante cuando tu tipo hace algo más complicado. Si tiene una PersonCollection
, podría estar internamente respaldada por un sql.Rows
, a []*Person
, a []uint
de ID de base de datos, o lo que sea. Usando la interfaz correcta, puede evitar que las personas que llaman io.Reader
qué se trata, de la misma manera que io.Reader
hace que las conexiones de red y los archivos se parezcan.
Una cosa específica: las interface
en Go tienen la peculiar propiedad de que puedes implementar una sin importar el paquete que la define; eso puede ayudarlo a evitar importaciones cíclicas . Si su interfaz devuelve una *Person
, en lugar de sólo cadenas o lo que sea, todos los PersonProviders
deben importar el paquete donde se define Person
. Eso puede estar bien o incluso ser inevitable; es solo una consecuencia para saber.
Dicho todo esto, no hay una convención de Go que tenga que ocultar todos sus datos. (Esta es una buena diferencia de, digamos, C ++). El stdlib hace cosas como permitirle inicializar un http.Server
con su configuración y promete que se puede usar un bytes.Buffer
cero bytes.Buffer
. Está bien hacer tus propias cosas así, y, de hecho, no creo que tengas que hacer una abstracción prematura si funciona la versión más concreta y que expone los datos. Solo se trata de ser consciente de las compensaciones.
Estoy familiarizado con el hecho de que, en Go, las interfaces definen la funcionalidad, en lugar de los datos. Pones un conjunto de métodos en una interfaz, pero no puedes especificar ningún campo que se requiera en cualquier elemento que implemente esa interfaz.
Por ejemplo:
// Interface
type Giver interface {
Give() int64
}
// One implementation
type FiveGiver struct {}
func (fg *FiveGiver) Give() int64 {
return 5
}
// Another implementation
type VarGiver struct {
number int64
}
func (vg *VarGiver) Give() int64 {
return vg.number
}
Ahora podemos usar la interfaz y sus implementaciones:
// A function that uses the interface
func GetSomething(aGiver Giver) {
fmt.Println("The Giver gives: ", aGiver.Give())
}
// Bring it all together
func main() {
fg := &FiveGiver{}
vg := &VarGiver{3}
GetSomething(fg)
GetSomething(vg)
}
/*
Resulting output:
5
3
*/
Ahora, lo que no puedes hacer es algo como esto:
type Person interface {
Name string
Age int64
}
type Bob struct implements Person { // Not Go syntax!
...
}
func PrintName(aPerson Person) {
fmt.Println("Person''s name is: ", aPerson.Name)
}
func main() {
b := &Bob{"Bob", 23}
PrintName(b)
}
Sin embargo, después de jugar con interfaces y estructuras incorporadas, descubrí una forma de hacerlo, después de una moda:
type PersonProvider interface {
GetPerson() *Person
}
type Person struct {
Name string
Age int64
}
func (p *Person) GetPerson() *Person {
return p
}
type Bob struct {
FavoriteNumber int64
Person
}
Debido a la estructura integrada, Bob tiene todo lo que tiene la persona. También implementa la interfaz PersonProvider, por lo que podemos pasar a Bob a funciones diseñadas para usar esa interfaz.
func DoBirthday(pp PersonProvider) {
pers := pp.GetPerson()
pers.Age += 1
}
func SayHi(pp PersonProvider) {
fmt.Printf("Hello, %v!/r", pp.GetPerson().Name)
}
func main() {
b := &Bob{
5,
Person{"Bob", 23},
}
DoBirthday(b)
SayHi(b)
fmt.Printf("You''re %v years old now!", b.Age)
}
Aquí hay un Go Playground que muestra el código anterior.
Con este método, puedo crear una interfaz que defina datos en lugar de comportamientos, y que cualquier estructura pueda implementar mediante la incorporación de esos datos. Puede definir funciones que interactúan explícitamente con los datos incrustados y desconocen la naturaleza de la estructura externa. ¡Y todo se comprueba en tiempo de compilación! (La única forma de que puedas PersonProvider
, que yo pueda ver, sería incorporar la interfaz PersonProvider
en Bob
, en lugar de una Person
concreta. Se compilaría y fallaría en tiempo de ejecución).
Ahora, aquí está mi pregunta: ¿es este un buen truco, o debería hacerlo de otra manera?