Lorsque Rob Pike dit «Go, c'est de la composition», que veut-il dire exactement? [fermé]

12

De moins est exponentiellement plus

Si C ++ et Java concernent les hiérarchies de types et la taxonomie des types, Go concerne la composition.

CMinus
la source

Réponses:

13

Il signifie que lorsque vous utiliseriez quelque chose de l'ordre de:

class A : public B {};

dans quelque chose comme Java ou C ++, dans Go vous utiliseriez (quelque chose d'équivalent à):

class A {
    B b;
};

Oui, cela offre des capacités de type héritage. Développons un peu l'exemple ci-dessus:

struct B {
    int foo() {}
};

struct A { 
    B b;
};

A a;

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

Pour ce faire, cependant, vous utilisez une syntaxe non autorisée en C ++ ou Java - vous laissez l'objet incorporé sans son propre nom, c'est donc plus comme:

struct A {
   B;
};
Jerry Coffin
la source
1
Je suis curieux, je fais ça en C ++ (je préfère la composition). Est-ce que go propose des fonctionnalités qui m'aident à composer quand en Java / C ++ je dois hériter?
Doug
2
@DougT .: Oui, j'ai édité dans un exemple montrant l'idée générale de (une partie de) ce qu'il permet.
Jerry Coffin
2
Je pense que cela manque le point: la différence n'est pas seulement syntaxique impliquant que vous utilisez l'intégration pour construire votre taxonomie. Le fait est que l'absence de substitution de méthode OOP vous empêche de construire votre taxonomie classique et vous devez utiliser la composition à la place.
Denys Séguret
1
@dystroy: Par rapport à Java, vous avez probablement un point. Comparé au C ++, pas tellement - parce que (au moins parmi ceux qui ont un indice) ces taxonomies géantes ont été vues pour la dernière fois il y a près de 20 ans.
Jerry Coffin
1
@dystroy: Vous ne comprenez toujours pas. Une hiérarchie à trois niveaux dans le C ++ moderne est presque inconnue. En C ++, vous les verrez dans la bibliothèque iostreams et la hiérarchie des exceptions - mais presque nulle part ailleurs. Si la bibliothèque iostreams était conçue aujourd'hui, je pense qu'il est sûr de dire que ce ne serait pas non plus de cette façon. Bottom line: vos arguments montrent moins sur C ++ que sur la façon dont vous êtes déconnecté de lui. Étant donné que vous ne l'avez pas utilisé depuis des décennies, cela a du sens. Ce qui n'a pas de sens, c'est d'essayer de dire comment C ++ est utilisé sur la base de cette expérience datée.
Jerry Coffin
8

Cette question / problème est un peu similaire à celui-ci .

Dans Go, vous n'avez pas vraiment de POO.

Si vous voulez "spécialiser" un objet, vous le faites en incorporant, qui est une composition, mais avec quelques goodies qui le rendent partiellement similaire à l'héritage. Vous le faites comme ceci:

type ConnexionMysql struct {
    *sql.DB
}

Dans cet exemple, ConnexionMysql est une sorte de spécialisation de * sql.DB, et vous pouvez appeler sur ConnexionMysql les fonctions définies sur * 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)

Donc, à première vue, vous pourriez penser que cette composition est l'outil pour faire votre taxonomie habituelle.

Mais

si une fonction définie sur * sql.DB appelle d'autres fonctions définies sur * sql.DB, elle n'appellera pas les fonctions redéfinies sur ConnexionMysql même si elles existent.

Avec l'héritage classique, vous faites souvent quelque chose comme ceci:

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

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

Autrement dit, vous définissez doComplexThingsur la super classe comme une organisation sur les appels des spécialisations.

Mais dans Go, cela n'appellerait pas la fonction spécialisée mais la fonction "superclasse".

Donc, si vous voulez avoir un algorithme nécessitant d'appeler certaines fonctions définies sur * sql.DB mais redéfinies sur ConnexionMySQL (ou d'autres spécialisations), vous ne pouvez pas définir cet algorithme en fonction de * sql.DB mais devez le définir ailleurs et cette fonction ne composera que les appels vers la spécialisation fournie.

Vous pouvez le faire comme ceci en utilisant des 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
}

Ceci est très différent de la substitution classique des hiérarchies de classes.

Surtout, vous ne pouvez évidemment pas avoir directement un troisième niveau héritant d'une implémentation de fonction du second.

En pratique, vous finirez par utiliser principalement des interfaces (orthogonales) et laisserez la fonction composer les appels sur une implémentation fournie au lieu d'avoir la "superclasse" de l'implémentation organisant ces appels.

D'après mon expérience, cela conduit à l'absence pratique de hiérarchies plus profondes qu'un niveau.

Trop souvent, dans d'autres langues, vous avez le réflexe, quand vous voyez que le concept A est une spécialisation du concept B, de réifier ce fait en créant une classe B et une classe A comme sous-classe de B. Au lieu de créer votre programme autour de vos données, vous passez du temps à reproduire la taxonomie des objets dans votre code, sur le principe que c'est la réalité.

Dans Go, vous ne pouvez pas définir un algorithme général et le spécialiser. Vous devez définir un algorithme général et vous assurer qu'il est général et qu'il fonctionne avec les implémentations d'interface fournies.

Ayant été horrifié par la complexité croissante de certains arbres de hiérarchie sur lesquels les codeurs faisaient des hacks complexes pour essayer d'accommoder un algorithme dont la logique implique finalement tous les niveaux, je dirais que je suis satisfait de la logique Go plus simple, même si elle oblige vous pensez au lieu de simplement réifier les concepts de votre modèle d'application.

Denys Séguret
la source