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 sea necesario en cualquier cosa 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 incrustadas, descubrí una forma de hacer esto, de alguna manera:
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 incrustada, Bob tiene todo lo que tiene Person. También implementa la interfaz PersonProvider, por lo que podemos pasar a Bob a funciones que están 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 demuestra el código anterior.
Con este método, puedo crear una interfaz que defina datos en lugar de comportamiento, y que puede ser implementada por cualquier estructura simplemente incorporando esos datos. Puede definir funciones que interactúan explícitamente con esos datos incrustados y desconocen la naturaleza de la estructura externa. ¡Y todo se comprueba en tiempo de compilación! (Puedo ver que la única forma en que podría estropearlo sería incrustar la interfaz PersonProvider
en Bob
, en lugar de una concreta Person
. 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 manera diferente?
Respuestas:
Definitivamente es un buen truco. Sin embargo, exponer punteros aún hace que el acceso directo a los datos esté disponible, por lo que solo le brinda una flexibilidad adicional limitada para cambios futuros. Además, las convenciones de Go no requieren que siempre coloque una abstracción delante de sus atributos de datos .
Tomando esas cosas juntas, me inclinaría hacia un extremo u otro para un caso de uso dado: ya sea a) simplemente haga un atributo público (usando incrustación si corresponde) y pasar tipos concretos ob) si parece que exponer los datos lo haría causar problemas más tarde, exponga un getter / setter para una abstracción más robusta.
Vas a sopesar esto por atributo. Por ejemplo, si algunos datos son específicos de la implementación o espera cambiar las representaciones por alguna otra razón, probablemente no desee exponer el atributo directamente, mientras que otros atributos de datos pueden ser lo suficientemente estables como para que hacerlos públicos sea una ganancia neta.
Ocultar propiedades detrás de captadores y definidores le brinda cierta flexibilidad adicional para realizar cambios compatibles con versiones anteriores más adelante. Digamos que algún día quiere cambiar
Person
para almacenar no solo un campo de "nombre", sino el nombre / segundo nombre / apellido / prefijo; si tiene métodosName() string
ySetName(string)
, puede mantenerPerson
contentos a los usuarios existentes de la interfaz mientras agrega nuevos métodos más detallados. O quizás desee poder marcar un objeto respaldado por una base de datos como "sucio" cuando tiene cambios no guardados; puede hacerlo cuando todas las actualizaciones de datos pasan porSetFoo()
métodos.Entonces: con getters / setters, puede cambiar los campos de estructura mientras mantiene una API compatible y agregar lógica alrededor de la propiedad get / set, ya que nadie puede hacerlo
p.Name = "bob"
sin pasar por su código.Esa flexibilidad es más relevante cuando el tipo es complicado (y la base de código es grande). Si tiene una
PersonCollection
, podría estar respaldada internamente por unasql.Rows
, una[]*Person
, una[]uint
de ID de base de datos o lo que sea. Con la interfaz correcta, puede evitar que las personas que llaman se preocupen de cuál es, la forma enio.Reader
que las conexiones de red y los archivos se parecen.Una cosa específica: los
interface
s en Go tienen la peculiar propiedad de que puedes implementar uno sin importar el paquete que lo define; que puede ayudarlo a evitar importaciones cíclicas . Si su interfaz devuelve un*Person
, en lugar de solo cadenas o lo que sea, todosPersonProviders
tienen que importar el paquete dondePerson
está definido. Eso puede estar bien o incluso ser inevitable; es solo una consecuencia que hay que conocer.Pero, de nuevo, la comunidad de Go no tiene una fuerte convención contra la exposición de miembros de datos en la API pública de su tipo . Se deja a su criterio si es razonable utilizar el acceso público a un atributo como parte de su API en un caso determinado, en lugar de desalentar cualquier exposición porque posiblemente podría complicar o prevenir un cambio de implementación más adelante.
Entonces, por ejemplo, stdlib hace cosas como permitirle inicializar un
http.Server
con su configuración y promete que un cerobytes.Buffer
es utilizable. Está bien hacer tus propias cosas de esa manera y, de hecho, no creo que debas abstraer las cosas de manera preventiva si la versión más concreta que expone datos parece funcionar. Se trata solo de estar al tanto de las compensaciones.fuente
Si entiendo correctamente, desea completar los campos de una estructura en otro. Mi opinión es no utilizar interfaces para ampliar. Puede hacerlo fácilmente con el siguiente enfoque.
https://play.golang.org/p/aBJ5fq3uXtt
Nota
Person
en laBob
declaración. Esto hará que el campo de estructura incluido esté disponible enBob
estructura directamente con algo de azúcar sintáctico.fuente