Composition plutôt qu'héritage
Fait partie de la section Programmation Orientée Objet du Journey C# de Coddy — leçon 41 sur 70.
L'héritage crée un couplage fort entre les classes. Lorsqu'une Car hérite de Vehicle, toute modification apportée à Vehicle peut casser Car. La composition offre une alternative plus flexible : au lieu d'hériter d'un comportement, une classe contient des objets qui fournissent ce comportement.
Considérez un personnage de jeu qui peut se déplacer et attaquer. Avec l'héritage, vous pourriez créer une hiérarchie complexe. Avec la composition, vous donnez simplement au personnage les composants dont il a besoin :
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}La différence clé est la relation. L'héritage représente « est un » (un Chien est un Animal), tandis que la composition représente « a un » (un Personnage a un comportement de mouvement). La composition vous permet d'échanger des comportements au moment de l'exécution, de combiner librement des capacités et d'éviter le problème de la classe de base fragile.
Utilisez l'héritage lorsqu'il existe une véritable relation hiérarchique. Choisissez la composition lorsque vous avez besoin de flexibilité, lorsque des comportements peuvent être partagés entre des classes non liées, ou lorsque vous souhaitez modifier le comportement de manière dynamique.
Défi
FacileConstruisons un système de notification qui démontre la puissance de la composition par rapport à l'héritage. Au lieu de créer une hiérarchie complexe de classes de notification, vous allez composer un NotificationSender en lui donnant des composants de comportement interchangeables.
Vous organiserez votre code sur trois fichiers :
Behaviors.cs: Créez deux classes de comportement dans l'espace de nomsNotificationsqui gèrent différents aspects de l'envoi de notifications :FormattingBehavior- possède une méthodeFormat(string message)qui renvoie le message enveloppé avec[ALERT]au début et[END]à la finDeliveryBehavior- possède une méthodeDeliver(string channel, string message)qui renvoie une chaîne au formatSending via {channel}: {message}
NotificationSender.cs: Créez une classeNotificationSenderdans l'espace de nomsNotificationsqui utilise la composition pour combiner les comportements. Votre expéditeur doit :- Contenir des instances de
FormattingBehavioretDeliveryBehavioren tant que champs privés - Avoir une propriété
Channel(string) qui stocke le nom du canal de distribution - Avoir un constructeur qui accepte le nom du canal
- Avoir une méthode
Send(string message)qui formate d'abord le message à l'aide du comportement de formatage, puis le livre à l'aide du comportement de livraison, et renvoie le résultat final
- Contenir des instances de
Program.cs: Dans votre fichier principal, créez unNotificationSenderavec un canal provenant de l'entrée, puis envoyez un message provenant de l'entrée. Affichez le résultat pour montrer comment les comportements composés fonctionnent ensemble.
Vous recevrez deux entrées :
- Le nom du canal de livraison (par exemple,
Email,SMS) - Le message à envoyer
Affichez le résultat de l'appel à Send() sur votre expéditeur de notification.
Par exemple, si les entrées sont Email et Server is down, la sortie doit être :
Sending via Email: [ALERT] Server is down [END]Remarquez comment le NotificationSender n'hérite de rien - il "possède" simplement des comportements de formatage et de livraison. Cette approche par composition signifie que vous pourriez facilement échanger l'un ou l'autre comportement sans modifier la classe de l'expéditeur, ou réutiliser ces comportements dans des classes complètement différentes !
Aide-mémoire
La composition est une alternative à l'héritage qui crée un code plus flexible en faisant en sorte qu'une classe contienne des objets qui fournissent un comportement, plutôt que d'hériter du comportement d'une classe parente.
Héritage vs Composition :
- L'héritage représente des relations de type « est un » (un Chien est un Animal)
- La composition représente des relations de type « a un » (un Personnage a un comportement de mouvement)
Avantages de la composition :
- Changer les comportements au moment de l'exécution
- Combiner les capacités librement
- Éviter le couplage fort et les problèmes de classe de base fragile
- Partager des comportements entre des classes non liées
Exemple avec des composants de comportement :
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}Quand utiliser chaque approche :
- Utilisez l'héritage pour les relations hiérarchiques réelles
- Choisissez la composition pour la flexibilité, les comportements partagés ou les changements de comportement dynamiques
Essayez vous-même
using System;
using Notifications;
class Program
{
public static void Main(string[] args)
{
// Lire l'entrée
string channel = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Créer un NotificationSender avec le canal
// TODO: Envoyer le message et afficher le résultat
}
}
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
Fichiers externesEspaces de noms et directivesIntro aux classes et objetsLe mot-clé 'this'Méthodes et paramètresChamps vs PropriétésConstructeursInitialiseurs d'objetsRécapitulatif - Calculatrice simple4Héritage
Syntaxe de base de l'héritage (:)Le mot-clé 'base'Mots-clés Virtual & OverrideClasses scelléesLa classe de base 'object'Récapitulatif - Hiérarchie des employés7Fonctionnalités avancées
Surcharge d'opérateursIndexeurs (this[])Redéfinition de ToString()Méthodes d'extensionRécapitulatif - Liste personnalisée10Patrons de conception - Partie 1
Introduction aux patrons de conceptionSingleton Thread-SafePatron FabriquePatron Observateur (Événements)Patron Stratégie2Propriétés et membres statiques
Propriétés auto-implémentéesPropriétés en lecture/écriture seuleChamps et méthodes statiquesClasses statiquesMembres à corps d'expression5Polymorphisme & Interfaces
Polymorphisme : Compilation vs ExécutionInterface vs Classe AbstraiteInterfaces MultiplesInterfaces ExplicitesUpcasting & DowncastingRécapitulatif - Calculateur de Formes8Concepts avancés de la POO
Composition plutôt qu'héritageGénériques (Classes et Méthodes)Délégués et ÉvénementsAttributs et RéflexionIDisposable et l'instruction usingBases de l'Injection de Dépendances11Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Méthode TemplatePatron ÉtatPatron Composite3Architecture des classes
Données d'instance vs statiquesMots-clés 'readonly' & 'const'Champs de supportRécapitulatif - Gestionnaire de compte bancaire6Encapsulation
Modificateurs d'accèsPropriétés pour l'encapsulationImplémentation du masquage de donnéesPatterns d'immuabilitéRécapitulatif - Dossiers d'étudiants12Projet : Système de gestion de bibliothèque
Structure du projetModèles Livre et Utilisateur