Aller au contenu

Programmation orientée objet ?

Contrairement à Java ou à C++, Go ne se dit pas être un langage « Orienté Objet ». Il permet cependant un style de programmation moderne inspirée des concepts de la programmation OOP. Certains disent que Go est un langage post-OOP.

Encapsulation

Encapsulation (Programmation) - Wikipedia

En programmation, l’encapsulation désigne le regroupement de données avec un ensemble de routines qui en permettent la lecture et la manipulation. Ce principe est souvent accompagné du masquage de ces données brutes1 afin de s’assurer que l’utilisateur ne contourne pas l’interface qui lui est destinée.

Go nous offre les struct et les méthodes liées à ces structures pour implémenter l’encapsulation. On peut aussi masquer les détails avec des attributs ou des méthodes privées (en choisissant un identifiant qui commence par une minuscule). On peut donc dire que Go supporte l’encapsulation des données.

Héritage

L’héritage est une caractéristique clé de la programmation OOP, mais Go ne supporte pas l’héritage comme Java ou C++. En revanche, Go remplace l’héritage par la composition.

On illustre habituellement l’héritage dans les langages OOP avec la modélisation des formes géométriques (un rectangle est un polygone, un polygone est une figure géométrique, un cercle est une figure géométrique…), des êtres vivants (un humain est un mammifère, un mammifère est un être vivant …) ou les véhicules (un camion est un véhicule, une voiture est un véhicule…). Considérons ce dernier et regardons comment structurer l’information en Go.

Le schéma de nos données est le suivant :

Le type de base Vehicle est modélisé avec une structure simple :

type Vehicle struct {
    Weight float32
}

et on lui associe une méthode Start :

func (v *Vehicle) Start() {
    fmt.Println("Starting Vehicle")
}

Pour le type Car nous disons qu’une voiture est composée d’un véhicule et nous donnons le nom Base à ce type de base :

type Car struct {
    Base  Vehicle
    Model string
}

On peut redéfinir la méthode Start pour ce nouveau type Car :

func (c *Car) Start() {
    fmt.Println("Starting " + c.Model)
}

Pour le vélo, nous utilisons aussi la composition, mais cette fois, nous la faisons de manière anonyme, c’est-à-dire que nous ne donnons pas de nom au type de base :

type Bicycle struct {
    Vehicle
    WheelSize float32
}

et nous définissons la méthode Fold qui permet de plier le vélo (oui, c’est bien un vélo pliable)

func (b *Bicycle) Fold() {
    fmt.Println("Folding Bicycle")
}

Nous pouvons maintenant créer des instances de ces nouveaux types. Une voiture c de type Car peut être créée ainsi :

c := &Car{
    Base: Vehicle{
        Weight: 2600,
    },
    Model: "Bentley",
}

et un vélo b de type Bicycle alors :

b := &Bicycle{
    Vehicle: Vehicle{
        Weight: 20,
    },
    WheelSize: 28,
}

On peut appeler la méthode Start de la voiture c :

c.Start()

et nous obtenons :

Starting Bentley

Si nous appelons cette même méthode sur le vélo b :

b.Start()

nous obtenons :

Starting Vehicle

Comme la méthode Start n’est pas définie pour le type Bicycle, go appelle la méthode attachée au type de base (Vehicle)

Comme le type de base de la voiture (Car) a un nom (Base), on peut aussi appeler la méthode Start de ce type de base :

c.Base.Start()

nous obtenons également :

Starting Vehicle

Supposons maintenant que nous avons les classes suivantes :

  • Un animal peut manger.
  • Un chien est un animal et il aboie
  • Un chat est un animal et il miaule
  • Un robot peut être enclenché
  • Un robot nettoyeur est un robot et il nettoie
  • Un robot marcheur est un robot et il marche

Supposons maintenant que nous souhaitons une classe pour représenter Spot, un robot marcheur qui ressemble à un chien et qui peut aboyer

Il est très difficile de modéliser Spot en fonction de ce qu’il est. Par contre, c’est facile de la modéliser compte tenu de ce qu’il fait. Nous décomposons les types par rapport aux actions (par exemple un objet qui peut aboyer est un aboyeur, un objet qui peut marcher est un marcheur) et nous construisons les types initiaux par composition :

Un chien est désormais composé d’un mangeur et d’un aboyeur et nous pouvons modéliser Spot comme étant composé d’un aboyeur, d’un enclenchable et d’un marcheur.

Penser un programme en termes de composition au lieu d’héritage demande un petit temps d’adaptation et très souvent, le résultat est au moins tout aussi élégant.

Constructeurs

Il n’y a pas de constructeur explicite en Go, mais il est très courant de mettre à disposition une fonction qui construit un objet. Cette fonction est typiquement nommée New<Type>. Par exemple, pour créer un Robot, on écrit typiquement une fonction NewRobot qui retourne un pointeur vers un Robot :

type Robot struct {
    Name string
}

func NewRobot(name string) *Robot {
    return &Robot{
        Name: name,
    }
}

Polymorphisme

Le polymorphisme en Go est réalisé à l’aide d’interfaces. Si l’on reprend l’exemple précédent avec les véhicules (Vehicle), les voitures (Car) et les vélos (Bicycle). Si l’on veut pouvoir démarrer une collection de véhicules, on définira l’interface suivante :

type Starter interface {
    Start()
}

Si b est un vélo (Bicycle) et c est une voiture (Car), on peut définir une collection de véhicules ainsi :

collection := []Starter{b, c}

et on démarre tous ces véhicules de cette façon :

for _, v := range collection {
    v.Start()
}

Enums

Les enums de base tels qu’ils existent en C sont relativement simples. Toutefois, les langages orientés objet tels que Java ou Rust en ont fait des classes avec beaucoup plus de fonctionnalités.

En Go, il n’y a pas d’enum explicite, mais le mot clé iota lié à un groupe de constantes permet de définir une séquence. Dans l’exemple suivant, les constantes North, South, East et West auront respectivement les valeurs 0, 1, 2, et 3 :

type Direction int

const (
    North Direction = iota
    South
    East
    West
)

On peut aussi utiliser le iota avec des constructions plus complexes, comme dans l’exemple suivant :

const (
    _  = iota // ignore first value by assigning to blank identifier
    KiB = 1 << (10 * iota)
    MiB
    GiB
)

Notez que ces constantes sont des entiers et Go ne permet pas de restreindre les valeurs d’un entier à être dans un ensemble donné. Dans l’exemple ci-dessous, la fonction f prend un paramètre de type Direction, mais ce n’est pas interdit de passer MiB comme argument.

func f(d Direction) {
    fmt.Println(d)
}

func main() {
    f(GiB) // no error
}

Cette liberté peut être vue comme une faiblesse du langage, mais d’une autre côté, elle offre des constructions intéressantes. Si vous avez besoin de représenter 13 GiB, vous pouvez simplement écrire 13 * GiB. Cette technique est d’ailleurs utilisée dans le package time de la bibliothèque standard :

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

Une durée de 3 minutes s’écrit simplement 3 * time.Minute.

Génériques

Les génériques sont apparus récemment (mars 2022) dans la version 1.18 de Go. Nous n’avons pas encore beaucoup d’exemples de cas d’utilisation de cette nouvelle fonctionnalité, mais nous ne doutons pas qu’elle ouvre une nouvelle dimension au langage.

Itérateurs

A partir de Go 1.23, et motivé par l’introduction des génériques, Go a introduit le concept d’itérateurs. Les itérateurs sont des objets qui permettent de parcourir une collection d’éléments. Les itérateurs sont utilisés dans les boucles for pour parcourir les éléments d’une collection.

Nous pouvons illustrer cela avec un exemple simple qui itère sur les fameux nombre de Fibonacci :

func IterateFibonacci(yield func(int64) bool) {
    f0, f1 := int64(0), int64(1)
    for yield(f0) {
        f0, f1 = f1, f0+f1
    }
}

func main() {
    count := 0
    for v := range IterateFibonacci {
        println(v)
        count++
        if count == 50 {
            break
        }
    }
}

Si vous souhaitez en savoir plus sur les itérateurs, vous pouvez consulter l’article de Bitfield Consulting.