Ir a los campos de la interfaz

105

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 PersonProvideren 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?

Matt Mc
fuente
3
"Puedo hacer una interfaz que defina datos en lugar de comportamiento". Yo diría que tiene un comportamiento que devuelve datos.
jmaloney
Voy a escribir una respuesta; Creo que está bien si lo necesita y conoce las consecuencias, pero hay consecuencias y no lo haría todo el tiempo.
twotwotwo
@jmaloney Creo que tienes razón, si quisieras verlo claramente. Pero en general, con las diferentes piezas que he mostrado, la semántica se convierte en "esta función acepta cualquier estructura que tenga un ___ en su composición". Al menos, eso es lo que pretendía.
Matt Mc
1
Este no es material de "respuesta". Llegué a su pregunta buscando en Google "interfaz como propiedad de estructura golang". Encontré un enfoque similar al establecer una estructura que implementa una interfaz como propiedad de otra estructura. Aquí está el patio de recreo, play.golang.org/p/KLzREXk9xo Gracias por darme algunas ideas.
Dale
1
En retrospectiva, y después de 5 años de usar Go, me queda claro que lo anterior no es idiomático. Es un esfuerzo hacia los genéricos. Si se siente tentado a hacer este tipo de cosas, le aconsejo que reconsidere la arquitectura de su sistema. Acepte interfaces y devuelva estructuras, comparta comunicándose y regocíjese.
Matt Mc

Respuestas:

55

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 Personpara almacenar no solo un campo de "nombre", sino el nombre / segundo nombre / apellido / prefijo; si tiene métodos Name() stringy SetName(string), puede mantener Personcontentos 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 por SetFoo()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 una sql.Rows, una []*Person, una []uintde 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 en io.Readerque las conexiones de red y los archivos se parecen.

Una cosa específica: los interfaces 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, todos PersonProviderstienen que importar el paquete donde Personestá 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.Servercon su configuración y promete que un cero bytes.Bufferes 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.

dos
fuente
Una cosa adicional: el enfoque de incrustación se parece un poco más a la herencia, ¿verdad? Obtiene los campos y métodos que tiene la estructura incrustada, y puede usar su interfaz para que cualquier superestructura califique, sin volver a implementar conjuntos de interfaces.
Matt Mc
Sí, se parece mucho a la herencia virtual en otros idiomas. Puede usar la incrustación para implementar una interfaz, ya sea que esté definida en términos de captadores y definidores o un puntero a los datos (o, una tercera opción para el acceso de solo lectura a estructuras pequeñas, una copia de la estructura).
twotwotwo
Tengo que decir que esto me está dando flashbacks a 1999 y aprendiendo a escribir montones de getters y setters repetitivos en Java.
Tom
Es una pena que la propia biblioteca estándar de Go no siempre haga esto. Estoy tratando de simular algunas llamadas a os.Process para pruebas unitarias. No puedo simplemente envolver el objeto de proceso en una interfaz, ya que se accede directamente a la variable miembro Pid y las interfaces Go no admiten variables miembro.
Alex Jansen
1
@ Tom Eso es verdad. Creo getters / setters añadir más flexibilidad que exponer un puntero, pero también no creo que todo el mundo debería captador / todo-setter ify (o que habría que coincida con el estilo típico Ir). Anteriormente tenía algunas palabras señalando eso, pero revisé el principio y el final para enfatizarlo mucho más.
twotwotwo
2

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.

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

Nota Personen la Bobdeclaración. Esto hará que el campo de estructura incluido esté disponible en Bobestructura directamente con algo de azúcar sintáctico.

Igor A. Melekhine
fuente