Pourquoi Go n'a pas d'héritage
Fait partie de la section Programmation Orientée Objet du Journey GO de Coddy — leçon 34 sur 107.
Les langages orientés objet traditionnels comme Java et C++ utilisent l'héritage pour partager du code entre les types. Une classe enfant étend une classe parente, héritant de tous ses champs et méthodes. Go omet délibérément cette fonctionnalité.
L'héritage crée un couplage fort entre les types. Lorsqu'une classe parente change, toutes les classes enfants sont affectées.
Les hiérarchies d'héritage profondes deviennent difficiles à comprendre et à maintenir. Le « problème de la classe de base fragile » survient lorsque des modifications apportées à une classe de base cassent de manière inattendue les classes dérivées.
Les concepteurs de Go ont choisi une voie différente : la composition plutôt que l'héritage. Au lieu de dire « un Dog est un Animal », Go vous encourage à dire « un Dog a des comportements de type Animal ». Ce changement subtil mène à un code plus flexible et plus facile à maintenir.
Go permet la réutilisation de code grâce à deux mécanismes que vous avez déjà appris :
- Interfaces définissent des contrats de comportement sans détails d'implémentation
- L'imbrication de structures permet aux types d'inclure d'autres types et de réutiliser leurs méthodes
Considérez cette comparaison. En POO traditionnelle, vous pourriez écrire class Dog extends Animal. En Go, vous imbriquez une struct Animal à l'intérieur de Dog et implémentez des interfaces partagées. Le résultat est une fonctionnalité similaire avec un couplage plus lâche entre les types.
Ce chapitre explore l'imbrication de structs en profondeur, montrant comment Go tire parti des avantages de l'héritage sans ses inconvénients.
Défi
FacileConstruisons un système de notification qui démontre l'approche de composition de Go au lieu de l'héritage. Vous allez créer des types qui partagent un comportement via des interfaces et l'imbrication de structures (struct embedding) plutôt que par des hiérarchies de classes.
Vous organiserez votre code sur trois fichiers :
notifier.go: Définissez une interfaceNotifieravec une seule méthodeNotify(message string) string. Créez également une structureBaseNotifieravec un champNamequi sera imbriqué par d'autres types. Donnez àBaseNotifierune méthode appeléeFormat(message string) stringqui retourne le message préfixé par le nom du notificateur entre crochets.channels.go: Créez deux types de canaux de notification qui imbriquentBaseNotifieret implémentent l'interfaceNotifier:EmailNotifieravec un champ supplémentaireAddressSMSNotifieravec un champ supplémentairePhone
Notifyde chaque type doit utiliser la méthodeFormatimbriquée et inclure ses informations de canal spécifiques dans la sortie.main.go: Créez une fonction appeléeSendAlertqui accepte n'importe quelNotifieret un message, puis retourne le résultat de l'appel àNotify. Lisez les détails de la notification depuis l'entrée, créez les deux types de notificateurs et démontrez comment ils peuvent être utilisés de manière interchangeable via l'interface.
Les entrées suivantes seront fournies :
- Ligne 1 : Nom du notificateur pour l'e-mail
- Ligne 2 : Adresse e-mail
- Ligne 3 : Nom du notificateur pour le SMS
- Ligne 4 : Numéro de téléphone
- Ligne 5 : Message d'alerte
Votre méthode Format sur BaseNotifier doit retourner :
[Name] messageVos méthodes Notify doivent retourner :
- EmailNotifier :
Email to [Address]: [formatted message] - SMSNotifier :
SMS to [Phone]: [formatted message]
Par exemple, avec Alerts, user@mail.com, Urgent, 555-1234, et Server down, votre sortie devrait être :
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downRemarquez comment les deux types de notificateurs réutilisent la méthode Format de BaseNotifier grâce à l'imbrication, tandis que chacun fournit sa propre implémentation de Notify. La fonction SendAlert fonctionne avec n'importe quel Notifier sans connaître le type concret — c'est la composition plutôt que l'héritage en action.
Aide-mémoire
Go utilise la composition plutôt que l'héritage pour partager du code entre les types, évitant ainsi le couplage fort et les problèmes de classe de base fragile de l'héritage orienté objet traditionnel.
Au lieu des hiérarchies de classes, Go parvient à la réutilisation du code via :
- Les interfaces - définissent des contrats de comportement sans détails d'implémentation
- L'imbrication de structures (Struct embedding) - permet aux types d'inclure d'autres types et de réutiliser leurs méthodes
Plutôt que de dire « un Chien est un Animal » (héritage), Go encourage « un Chien a des comportements de type Animal » (composition).
Exemple d'imbrication de structures
Définissez une structure de base qui sera imbriquée :
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}Imbriquez la structure de base dans d'autres types pour réutiliser ses champs et ses méthodes :
type EmailNotifier struct {
BaseNotifier // structure imbriquée
Address string
}
func (e EmailNotifier) Notify(message string) string {
// Peut appeler la méthode Format imbriquée directement
return "Email to " + e.Address + ": " + e.Format(message)
}Définissez une interface que plusieurs types peuvent implémenter :
type Notifier interface {
Notify(message string) string
}Utilisez l'interface pour travailler avec différents types concrets de manière interchangeable :
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}Cette approche offre une fonctionnalité similaire à l'héritage mais avec un couplage plus lâche entre les types.
Essayez vous-même
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert accepte n'importe quel Notifier et un message, et retourne le résultat de l'appel à Notify
// TODO: Implémenter la fonction SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Lire le nom du notificateur d'e-mail
scanner.Scan()
emailName := scanner.Text()
// Lire l'adresse e-mail
scanner.Scan()
emailAddress := scanner.Text()
// Lire le nom du notificateur SMS
scanner.Scan()
smsName := scanner.Text()
// Lire le numéro de téléphone
scanner.Scan()
phoneNumber := scanner.Text()
// Lire le message d'alerte
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Créer un EmailNotifier avec emailName et emailAddress
// TODO: Créer un SMSNotifier avec smsName et phoneNumber
// TODO: Utiliser SendAlert pour envoyer le alertMessage via les deux notificateurs
// et afficher les résultats
// Supprimer les avertissements de variables inutilisées (les retirer lors de l'utilisation des variables)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
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
1Fondamentaux de la POO en Go
Fichiers externesEspace de travail et Modules GoPackages et ImportsNoms exportés vs non exportésIntroduction à la POO en GoLes Structs comme ClassesDéfinir des méthodes sur les StructsReceveurs par pointeur vs par valeurInitialisation de StructsFonctions constructeursRécapitulatif - Calculatrice simple4Interfaces
Introduction aux interfacesImplémentation impliciteL'interface comme contratInterface vide (any)Assertion de typeType SwitchComposition d'interfacesInterfaces Stringer & ErrorRécapitulatif - Calculateur de formes7Encapsulation
Champs exportés vs non exportésEncapsulation au niveau du packageMéthodes Getter & SetterMasquage d'informations en GoRécapitulatif - Dossiers étudiants10Génériques (Go 1.18+)
Introduction aux génériquesParamètres de typeContraintes de typeStructures génériquesContournement pour les méthodes génériquesRécapitulatif - Collections génériques13Patrons de conception, Partie 1
Introduction aux Design PatternsPatron SingletonPatron FactoryPatron Abstract FactoryPatron ObserverPatron Strategy2Exploration approfondie des Types et Structs
Types de base et compositesDéfinitions de types personnalisésTags de StructStructs anonymesStructs imbriquéesValeurs par défaut et Zero ValuesRécapitulatif - Carnet de contacts5Composition plutôt qu'héritage
Pourquoi Go n'a pas d'héritageLes bases de l'imbrication de structsPromotion de méthodesImbriquer plusieurs structsImbrication vs AgrégationMasquage de méthodes imbriquéesRécapitulatif - Hiérarchie des employés8Gestion des erreurs & POO
L'interface errorTypes d'erreurs personnalisésError Wrapping (fmt.Errorf)Erreurs sentinelleserrors.Is() et errors.As()Panic, Defer et RecoverRécapitulatif - File Parser11Bibliothèque standard et POO
io.Reader & io.WriterL'interface sort.InterfaceL'interface fmt.Stringerencoding/json avec les StructsL'interface http.HandlerRécapitulatif - Modèles d'API REST14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeMiddleware en tant que Décorateur3Pointeurs et Mémoire
Bases des pointeurs en GoPointeurs vers des structsPassage par valeur vs référenceLa fonction new()Garbage Collection en GoRécapitulatif - Constructeur de liste chaînée6Le polymorphisme en Go
Le polymorphisme via les interfacesLe Duck Typing en GoRègles de satisfaction des interfacesCollections polymorphesInjection de dépendancesRécapitulatif - Processeur de paiement9Concurrence et POO
Bases des GoroutinesChannels et CommunicationChannels avec buffer vs sans bufferL'instruction Selectsync.Mutex et sync.RWMutexsync.WaitGroupConception de structures Thread-SafeRécapitulatif - Worker Pool