Menu
Coddy logo textTech

Règles de satisfaction des interfaces

Fait partie de la section Programmation Orientée Objet du Journey GO de Coddy — leçon 43 sur 107.

Alors que Go utilise le duck typing pour la satisfaction implicite des interfaces, il existe des règles spécifiques qui déterminent si un type satisfait réellement une interface. Comprendre ces règles vous aide à éviter des bogues subtils.

La règle la plus importante concerne les récepteurs de pointeur. Si une méthode est définie avec un récepteur de pointeur, seul un pointeur vers ce type satisfait l'interface — et non la valeur elle-même :

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // récepteur de pointeur
    return "Saved: " + d.Name
}

func Process(s Saver) {
    fmt.Println(s.Save())
}

Parce que Save() a un récepteur de pointeur, seul *Document satisfait Saver :

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // fonctionne - le pointeur satisfait l'interface
    // Process(doc)  // erreur de compilation - la valeur ne satisfait pas l'interface
}

Cependant, l'inverse est plus flexible. Si une méthode a un récepteur de valeur, à la fois les types de valeur et de pointeur satisfont l'interface. Go déréférence automatiquement les pointeurs lors de l'appel de méthodes avec récepteur de valeur :

func (d Document) Info() string {  // récepteur de valeur
    return d.Name
}

// Document et *Document satisfont tous deux une interface nécessitant Info()

Cette asymétrie existe parce que Go peut toujours obtenir une valeur à partir d'un pointeur (par déréférencement), mais il ne peut pas toujours obtenir un pointeur à partir d'une valeur (la valeur peut ne pas être adressable). Garder cette règle à l'esprit permet d'éviter des erreurs de compilation confuses lors de l'utilisation des interfaces.

challenge icon

Défi

Facile

Construisons un système de configuration qui démontre comment les récepteurs de pointeur et de valeur affectent la satisfaction d'une interface. Vous allez créer des types où le choix du récepteur détermine si les valeurs, les pointeurs ou les deux peuvent être utilisés avec une interface.

Vous organiserez votre code sur trois fichiers :

  • config.go : Définissez une interface Configurable qui requiert deux méthodes : GetValue() string et SetValue(string). Créez ensuite deux types de configuration :
    • ReadOnlyConfig avec un champ Value—utilisez un récepteur de valeur pour GetValue() (retourne la Value) et un récepteur de pointeur pour SetValue() (met à jour la Value)
    • Setting avec un champ Data—utilisez des récepteurs de valeur pour les deux méthodes (GetValue retourne Data, SetValue affiche simplement "Cannot modify" sans rien changer)
  • processor.go : Créez une fonction appelée ProcessConfig qui accepte un Configurable et une nouvelle chaîne de caractères pour la valeur. Elle doit afficher la valeur actuelle en utilisant GetValue(), appeler SetValue() avec la nouvelle valeur, puis afficher à nouveau la valeur pour montrer les changements éventuels.
  • main.go : Lisez les détails de configuration depuis l'entrée standard et démontrez les règles de satisfaction d'interface :
    1. Créez un ReadOnlyConfig et passez un pointeur à ProcessConfig (requis car SetValue a un récepteur de pointeur)
    2. Créez un Setting et passez la valeur directement à ProcessConfig (fonctionne car les deux méthodes ont des récepteurs de valeur)

Les entrées suivantes seront fournies :

  • Ligne 1 : Valeur initiale pour ReadOnlyConfig
  • Ligne 2 : Nouvelle valeur à définir pour ReadOnlyConfig
  • Ligne 3 : Valeur initiale pour Setting
  • Ligne 4 : Nouvelle valeur à tenter pour Setting

Votre fonction ProcessConfig doit afficher les résultats dans ce format :

Current: [value]
Current: [value after SetValue]

Par exemple, avec debug, production, localhost, et remote, votre sortie devrait être :

Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhost

Remarquez comment ReadOnlyConfig change réellement sa valeur (parce que nous avons passé un pointeur), tandis que Setting reste inchangé (son SetValue avec un récepteur de valeur ne peut pas modifier l'original). L'idée clé est que les valeurs de ReadOnlyConfig seules ne satisferaient pas Configurable—seuls les pointeurs le font—alors que les valeurs de Setting fonctionnent directement parce que toutes ses méthodes utilisent des récepteurs de valeur.

Aide-mémoire

Go utilise le typage structurel (duck typing) pour la satisfaction implicite des interfaces, mais il existe des règles spécifiques concernant les récepteurs de pointeurs et de valeurs :

Règle du récepteur de pointeur

Si une méthode a un récepteur de pointeur, seul un pointeur vers ce type satisfait l'interface :

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // récepteur de pointeur
    return "Saved: " + d.Name
}

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // fonctionne - le pointeur satisfait l'interface
    // Process(doc)  // erreur de compilation - la valeur ne satisfait pas l'interface
}

Règle du récepteur de valeur

Si une méthode a un récepteur de valeur, le type valeur et le type pointeur satisfont tous deux l'interface :

func (d Document) Info() string {  // récepteur de valeur
    return d.Name
}

// Document et *Document satisfont tous deux une interface requérant Info()

Pourquoi cette asymétrie existe

Go peut toujours obtenir une valeur à partir d'un pointeur (par déréférencement), mais ne peut pas toujours obtenir un pointeur à partir d'une valeur (la valeur peut ne pas être adressable).

Essayez vous-même

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// Lire les entrées
	scanner.Scan()
	rocInitial := scanner.Text()
	scanner.Scan()
	rocNew := scanner.Text()
	scanner.Scan()
	settingInitial := scanner.Text()
	scanner.Scan()
	settingNew := scanner.Text()
	
	// TODO: Créer un ReadOnlyConfig avec la valeur rocInitial
	// TODO: Passer un POINTER à ProcessConfig (requis car SetValue a un récepteur de type pointeur)
	
	// TODO: Créer un Setting avec la valeur settingInitial
	// TODO: Passer la VALUE directement à ProcessConfig (fonctionne car les deux méthodes ont des récepteurs de type valeur)
	
	// Utiliser ces variables pour éviter les erreurs de variables non utilisées
	_ = rocInitial
	_ = rocNew
	_ = settingInitial
	_ = settingNew
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation Orientée Objet