Go est-il objet ?

vendredi 31 mars 2023 · 5 minutes · 954 mots

Au gré d’une discussion sur un discord de programmation, une affirmation m’a rendu perplexe : Go ne serait pas un langage objet.

Je savais que Go n’avait pas de mécanisme d’héritage. Mais, cela suffisait-il pour le disqualifier en tant que langage objet ?

Permettez-moi donc de vous faire part de ma réflexion sur le sujet.

Mais qu’est-ce qu’un langage objet, d’abord ?

Wikipédia définit un objet comme une représentation d’un concept, une idée ou une entité du monde physique 1, avec des propriétés et des comportements.

Par exemple, on pourrait représenter une voiture comme une marque, une couleur et un carburant. Ça, ce serait pour ses propriétés.

Pour son comportement, on peut dire qu’une voiture peut démarrer, accélérer et freiner.

Dans une même entité, on trouve des données pour stocker les états (propriétés), et des fonctions ou méthodes pour interagir avec l’objet (comportement).

Ainsi, la définition d’un langage objet serait un langage qui permette de regrouper des données et des fonctions dans une même structure ?

Pas si simple, car au gré de l’arrivée de nouveaux langages, ou l’évolution de certains, la définition a évolué. Aujourd’hui, on peut définir un langage objet selon les critères suivants :

  • L’encapsulation : l’objet a un état (données) et un comportement (méthodes). On est clairement dans la définition de l’objet vue plus haut.

  • L’abstraction : l’objet peut “cacher” son fonctionnement interne et exposer une interface permettant aux autres objets d’interagir avec.

  • L’héritage : l’objet peut adopter le comportement d’un autre objet sans qu’il soit nécessaire de redéfinir ce comportement.

  • Le polymorphisme : soit un même comportement change selon l’objet sur lequel il s’applique, soit un comportement hérité est redéfini.

Go coche-t-il toutes les cases d’un langage objet ?

L’encapsulation

struct est une collection de données en Go. Voici comment je définirai un nouveau type Point comme un objet composite contenant sa position :

type Point struct {
	X, Y float64
}

Il est possible de définir des méthodes sur des types grâce au receiver. On ajoute le type qui reçoit la méthode avant le nom de la méthode. Par exemple :

func (p Point) Dist() float64 {
	return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

Les ingrédients pour l’encapsulation sont tous réunis : un moyen de regrouper des données et des comportements.

L’abstraction

La visibilité des données par l’extérieur 2 se définit par la casse du nom de la variable : une minuscule pour une variable privée et une majuscule pour une variable publique. Par exemple :

type Voiture struct {
    Couleur string   // visible à l'extérieur
    code    string   // non visible à l'extérieur
}

Le même principe est appliqué pour les méthodes qui, si leur nom commence par une minuscule, ne pourront pas être utilisées à l’extérieur. Inversement, si leur nom commence par une lettre capitale, elles seront visibles.

Go nomme ce mécanisme l’export des noms et permet de cacher ou exposer des fonctionnalités, comme définit par l’abstraction.

L’héritage

J’ai dit plus haut que je savais que Go n’avait pas d’héritage. Mais cela ne veut pas forcément dire que Go ne permet pas d’obtenir la même chose que l’héritage. En effet, Go permet l’incorporation de type.

Par exemple, reprenons l’exemple de la voiture et définissons un objet “Véhicule” avec une méthode “Rouler” :

type Vehicule struct {
    Nom string
}
func (v Vehicule) Rouler() {
    println("je roule")
}

Tout ce qui fait un véhicule peut être incorporé dans un objet “Voiture” comme suit :

type Voiture struct {
    Vehicule
}

Et si on devait utiliser tout cela :

var tuture Voiture 

tuture.Vehicule.Rouler()    // "je roule"
tuture.Rouler()             // "je roule"

Exemple complet sur le playground

Les 2 appels à la méthode “Rouler” sont valables et on a bien là un moyen de faire hériter des comportements d’un objet à un autre.

Le polymorphisme

Dans le précédent exemple, les 2 appels à la méthode “Rouler” donnaient le même résultat parce qu’ils appelaient la même méthode “Rouler” définie au niveau de l’objet “Véhicule”. On peut redéfinir la méthode au niveau de “Voiture” comme suit :

func (v Voiture) Rouler() {
    println("je roule comme une voiture")
}

Dès lors, les appels à “Rouler” donneraient différents résultats :

var tuture Voiture 

tuture.Vehicule.Rouler()    // "je roule"
tuture.Rouler()             // "je roule comme une voiture"

Une méthode héritée d’un objet parent peut donc être modifiée au niveau de l’objet fils.

Mais on peut aller plus loin. En effet, Go possède aussi la notion d’interface qui permet de définir des types par leur comportement. Concrètement, une interface est une collection de méthodes.

type Vehicule interface {
    Rouler()
}

type Voiture struct {}
func (v Voiture) Rouler() {
    println("je roule comme une voiture")
}

type Car struct {}
func (c Car) Rouler() {
    println("je roule comme un car")
}

func ControleTechnique(v Vehicule) {
    v.Rouler()
}

On pourrait alors appeler ControleTechnique soit avec un type Voiture soit un type Car comme dans l’exemple suivant :

var tuture Voiture
var poticar Car

ControleTechnique(tuture)   // "je roule comme une voiture"
ControleTechnique(poticar)  // "je roule comme un car"

Exemple complet sur le playground

Avec la redéfinition des méthodes héritées et les interfaces, Go implémente le polymorphisme à plusieurs niveaux.

Conclusion

Go est bien un langage objet. Mais ce qui compte bien plus que de cocher les cases de tel ou tel paradigme, c’est de donner les moyens aux codeuses et codeurs de faire leur boulot le mieux possible. Les concepteurs de Go ont repris des autres langages, ce qu’il leur semblait le plus pertinent pour rendre le code lisible, performant et maintenable.

Pour ma part, Go reste un langage extrêmement productif dans mon quotidien de développeur.


  1. Page Wikipédia sur les langages orientés objet ↩︎

  2. La notion d’extérieur s’entend au sens de package qui est le mécanisme de portée des variables et des fonctions de Go ↩︎

thinking go