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 traditionnels orientés objet comme Java et C++ utilisent l’héritage pour partager du code entre les types. Une classe enfant étend une classe parente et hérite de tous ses champs et méthodes. Go omet délibérément cette fonctionnalité.
L’héritage crée un couplage étroit 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 interrompent 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 has des comportements semblables à ceux d'un Animal ». Ce changement subtil mène à un code plus flexible et plus facile à maintenir.
Go réalise la réutilisation du 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’intégration 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 intégrez une structure Animal dans Dog et implémentez des interfaces partagées. Le résultat offre des fonctionnalités similaires avec un couplage plus faible entre les types.
Ce chapitre explore en profondeur l’intégration de structures, en montrant comment Go obtient les avantages de l’héritage sans ses inconvénients.
Défi
FacileConstruisons un système de notifications qui illustre l'approche de composition de Go plutôt que l'héritage. Vous allez créer des types qui partagent un comportement grâce aux interfaces et à l'intégration de structures, plutôt qu'à 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 intégrée par les autres types. Donnez àBaseNotifierune méthode appeléeFormat(message string) stringqui renvoie le message précédé du nom du notificateur entre crochets.channels.go: créez deux types de canaux de notification qui intègrentBaseNotifieret implémentent l'interfaceNotifier:EmailNotifieravec un champ supplémentaireAddressSMSNotifieravec un champ supplémentairePhone
Notifyde chaque type doit utiliser la méthodeFormatintégrée et inclure les informations spécifiques à son canal dans le résultat.main.go: créez une fonction appeléeSendAlertqui accepte n'importe quelNotifierainsi qu'un message, puis renvoie le résultat de l'appel àNotify. Lisez les détails des notifications depuis l'entrée, créez les deux types de notificateurs et montrez comment ils peuvent être utilisés de manière interchangeable grâce à 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 renvoyer :
[Name] messageVos méthodes Notify doivent renvoyer :
- 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 doit ê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'intégration, 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.
Essayez vous-même
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert accepte n'importe quel Notifier et un message, renvoie 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 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 non utilisées (retirez-les lorsque vous utilisez les 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 et non exportésIntroduction à la POO en GoStructs comme classesDéfinir des méthodes sur des structsRécepteurs pointeurs ou par valeurInitialisation des structsFonctions constructeursRécapitulatif - Calculatrice simple4Interfaces
Introduction aux interfacesImplémentation impliciteL’interface comme contratInterface vide (any)Assertion de typeCommutation de typeComposition d’interfacesInterfaces Stringer et ErrorRécapitulatif - Calculateur de formes7Encapsulation
Champs exportés vs non exportésEncapsulation au niveau du packageMéthodes Getter et SetterDissimulation de l’information en GoRécapitulatif – Fiches d’étudiants10Génériques (Go 1.18+)
Introduction aux génériquesParamètres de typeContraintes de typeStructures génériquesSolution de contournement pour les méthodes génériquesRécapitulatif - Collection générique2Plongée approfondie dans les types et les structs
Types de base et compositesDéfinitions de types personnalisésTags de structStructs anonymesStructs imbriquésValeurs zéro et valeurs par défautRécapitulatif - Carnet de contacts5La composition plutôt que l’héritage
Pourquoi Go n’a pas d’héritageBases de l’inclusion de structsPromotion des méthodesInclure plusieurs structsInclusion ou agrégationMasquage des méthodes inclusesRécapitulatif : hiérarchie des employés8Gestion des erreurs et POO
L’interface errorTypes d’erreurs personnalisésEnrobage des erreurs (fmt.Errorf)Erreurs sentinelleserrors.Is() et errors.As()Panic, Defer et RecoverRécapitulatif - Analyseur de fichiers11Bibliothèque standard et POO
io.Reader et io.Writersort.InterfaceInterface fmt.Stringerencoding/json avec des structsInterface http.HandlerRécapitulatif - Modèles d’API REST14Modèles de conception – Partie 2
Modèle CommandeModèle AdaptateurModèle DécorateurModèle Méthode modèleModèle ÉtatModèle CompositeMiddleware comme décorateur3Pointeurs et mémoire
Notions de base des pointeurs en GoPointeurs vers des structuresPassage par valeur ou par référenceLa fonction new()Garbage collection en GoRécapitulatif - Constructeur de listes chaînées6Polymorphisme en Go
Polymorphisme via les interfacesDuck typing en GoRègles de satisfaction des interfacesCollections polymorphesInjection de dépendancesRécapitulatif – Processeur de paiements9Concurrence et POO
Bases des GoroutinesCanaux et communicationCanaux tamponnés ou non tamponnésInstruction selectsync.Mutex et sync.RWMutexsync.WaitGroupConception de structures thread-safeRécapitulatif - Pool de workersEntraînez-vous par vous-même : Compilateur Go en ligne