Cuando Rob Pike dice "Ir es sobre composición", ¿qué quiere decir exactamente? [cerrado]

Respuestas:

13

Él quiere decir que donde usarías algo del orden de:

class A : public B {};

en algo como Java o C ++, en Go usarías (algo equivalente a):

class A {
    B b;
};

Sí, esto proporciona capacidades de herencia. Expandamos un poco el ejemplo anterior:

struct B {
    int foo() {}
};

struct A { 
    B b;
};

A a;

a.foo();  // not allowed in C++ or Java, but allowed in Go.

Sin embargo, para hacer esto, usa una sintaxis que no está permitida en C ++ o Java; deja el objeto incrustado sin un nombre propio, por lo que es más parecido a:

struct A {
   B;
};
Jerry Coffin
fuente
1
Tengo curiosidad, hago eso en C ++ (prefiero composición). ¿Proporciona características que me ayudan a componer cuando en Java / C ++ tendría que heredar?
Doug T.
2
@DougT .: Sí, he editado en un ejemplo que muestra la idea general de (parte de) lo que permite.
Jerry Coffin
2
Creo que esto pierde el punto: la diferencia no es solo sintáctica, lo que implica que utilizas la incrustación para construir tu taxonomía. El hecho es que la falta de anulación del método OOP le impide construir su taxonomía clásica y debe usar composición en su lugar.
Denys Séguret
1
@dystroy: en comparación con Java, probablemente tenga un punto. En comparación con C ++, no tanto, porque (al menos entre aquellos con una pista) esas taxonomías gigantes se vieron por última vez hace aproximadamente 20 años.
Jerry Coffin
1
@distroy: Todavía no entiendes. Una jerarquía de tres niveles en C ++ moderno es casi desconocida. En C ++ verá los de la biblioteca iostreams y la jerarquía de excepciones, pero cerca de ningún otro lado. Si la biblioteca iostreams se estuviera diseñando hoy, creo que es seguro decir que tampoco sería así. En pocas palabras: sus argumentos muestran menos sobre C ++ que sobre lo desconectado que está con él. Dado que no lo ha usado en décadas, eso tiene sentido. Lo que no tiene sentido es tratar de decir cómo se usa C ++ en función de esa experiencia anticuada.
Jerry Coffin
8

Esta pregunta / problema es similar a este .

En Go, realmente no tienes OOP.

Si desea "especializar" un objeto, hágalo incrustando, que es una composición, pero con algunas ventajas que lo hacen parcialmente similar a la herencia. Lo haces así:

type ConnexionMysql struct {
    *sql.DB
}

En este ejemplo, ConnexionMysql es un tipo de especialización de * sql.DB, y puede llamar a ConnexionMysql las funciones definidas en * sql.DB:

type BaseMysql struct {
    user     string
    password string
    database string
}

func (store *BaseMysql) DB() (ConnexionMysql, error) {
    db, err := sql.Open("mymysql", store.database+"/"+store.user+"/"+store.password)
    return ConnexionMysql{db}, err
}

func (con ConnexionMysql) EtatBraldun(idBraldun uint) (*EtatBraldun, error) {
    row := con.QueryRow("select pv, pvmax, pa, tour, dla, faim from compte where id=?", idBraldun)
    // stuff
    return nil, err
}

// somewhere else:
con, err := ms.bd.DB()
defer con.Close()
// ...
somethings, err = con.EtatBraldun(id)

Entonces, a primera vista, podría pensar que esta composición es la herramienta para hacer su taxonomía habitual.

Pero

si una función definida en * sql.DB llama a otras funciones definidas en * sql.DB, no llamará a las funciones redefinidas en ConnexionMysql, incluso si existen.

Con la herencia clásica, a menudo haces algo como esto:

func (db *sql.DB) doComplexThing() {
   db.doSimpleThing()
   db.doAnotherSimpleThing()
}

func (db *sql.DB) doSimpleThing() {
   // standard implementation, that we expect to override
}

Es decir, usted define doComplexThingen la superclase como una organización en llamadas de especializaciones.

Pero en Go, esto no llamaría a la función especializada sino a la función de "superclase".

Entonces, si desea tener un algoritmo que necesite llamar a algunas funciones definidas en * sql.DB pero redefinidas en ConnexionMySQL (u otras especializaciones), no puede definir este algoritmo como una función de * sql.DB, pero debe definirlo en otro lugar y esta función solo compondrá las llamadas a la especialización proporcionada.

Podrías hacerlo así usando interfaces:

type interface SimpleThingDoer {
   doSimpleThing()
   doAnotherSimpleThing()
}

func doComplexThing(db SimpleThingDoer) {
   db.doSimpleThing()
   db.doAnotherSimpleThing()
}

func (db *sql.DB) doSimpleThing() {
   // standard implementation, that we expect to override
}

func (db ConnexionMySQL) doSimpleThing() {
   // other implemenation
}

Esto es bastante diferente de la anulación clásica de las jerarquías de clases.

Especialmente, obviamente no puede tener directamente un tercer nivel heredando una implementación de función del segundo.

En la práctica, terminará utilizando principalmente interfaces (ortogonales) y dejará que la función componga las llamadas en una implementación proporcionada en lugar de que la "superclase" de la implementación organice esas llamadas.

En mi experiencia, esto lleva a la ausencia práctica de jerarquías más profundas que un nivel.

Con demasiada frecuencia, en otros idiomas, tiene el reflejo, cuando ve que el concepto A es una especialización del concepto B, para reificar este hecho creando una clase B y una clase A como una subclase de B. En lugar de crear su programa alrededor de sus datos, pasa tiempo reproduciendo la taxonomía de objetos en su código, en el principio de que esto es realidad.

En Go no puede definir un algoritmo general y especializarlo. Debe definir un algoritmo general y asegurarse de que sea general y funcione con las implementaciones de interfaz proporcionadas.

Habiendo quedado horrorizado por la creciente complejidad de algunos árboles de jerarquía en los que los codificadores estaban haciendo trucos complejos para tratar de acomodar un algoritmo cuya lógica finalmente implica todos los niveles, diría que estoy contento con la lógica Go más simple, incluso si obliga pensar en lugar de simplemente reificar los conceptos de su modelo de aplicación.

Denys Séguret
fuente