Composition vs Héritage
Fait partie de la section Programmation Orientée Objet du Journey Dart de Coddy — leçon 82 sur 110.
Vous avez appris que l'héritage crée une relation « est un » - un Dog est un Animal. Mais il existe une autre approche puissante : la composition, qui crée une relation « a un ». Au lieu d'hériter d'un comportement, une classe contient des instances d'autres classes et leur délègue le travail.
Considérez une classe Car. Avec l'héritage, vous pourriez essayer d'étendre une classe Engine - mais une voiture n'est pas un moteur, elle a un moteur :
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Composition : La voiture A UN moteur
void start() {
_engine.start(); // Déléguer au moteur
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}L'avantage clé de la composition est la flexibilité. Avec l'héritage, vous êtes limité à une seule classe parente. Avec la composition, vous pouvez combiner plusieurs composants et même les échanger au moment de l'exécution :
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Une directive courante est « privilégier la composition plutôt que l'héritage ». Utilisez l'héritage lorsqu'il existe une véritable relation « est-un » (is-a) et que vous avez besoin de polymorphisme. Utilisez la composition lorsque vous souhaitez réutiliser un comportement sans couplage fort, ou lorsqu'un objet a besoin de capacités provenant de plusieurs sources.
Défi
FacileConstruisons un système informatique en utilisant la composition ! Au lieu d'essayer de faire hériter un ordinateur de ses composants, vous allez créer des classes de composants distinctes et les composer ensemble - car un ordinateur a un CPU et a de la mémoire, il n'est pas un CPU ou une mémoire.
Vous organiserez votre code en trois fichiers :
components.dart: Créez deux classes de composants qui représentent les pièces de l'ordinateur :- Une classe
CPUavec unString brandet undouble speedGHz. Incluez une méthodeprocess()qui retourne une chaîne de caractères :[brand] processing at [speedGHz]GHz - Une classe
Memoryavec unint sizeGB. Incluez une méthodeload()qui retourne une chaîne de caractères :Loading [sizeGB]GB of data
- Une classe
computer.dart: Créez une classeComputerqui utilise la composition pour combiner les composants. Votre ordinateur doit :- Avoir un champ
String name - Contenir une instance de
CPUet une instance deMemory(composition - l'ordinateur POSSÈDE ces pièces) - Accepter toutes les valeurs nécessaires via son constructeur pour créer les deux composants en interne
- Avoir une méthode
boot()qui délègue aux deux composants et affiche trois lignes : le nom de l'ordinateur suivi destarting..., puis le résultat de la méthode process du CPU, puis le résultat de la méthode load de la Memory - Avoir une méthode
specs()qui retourne une chaîne décrivant le système :[name]: [brand] [speedGHz]GHz, [sizeGB]GB RAM
- Avoir un champ
main.dart: Importez vos fichiers et démontrez la composition en action :- Créez un
ComputernomméWorkstationavec un CPUIntelcadencé à3.5GHz et16Go de mémoire - Appelez la méthode
boot() - Affichez le résultat de
specs()
- Créez un
Remarquez comment la classe Computer n'étend pas CPU ou Memory - elle les contient et leur délègue le travail. C'est la relation "a-un" (has-a) que la composition crée !
Sortie attendue :
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAMAide-mémoire
La composition crée une relation de type « a un » (has-a) où une classe contient des instances d'autres classes et leur délègue le travail, plutôt que d'en hériter.
Exemple de composition de base :
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Car HAS an Engine
void start() {
_engine.start(); // Delegate to the engine
print('Car is ready to drive');
}
}Composition avec flexibilité - échange de composants :
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Quand utiliser la composition par rapport à l'héritage :
- Utilisez l'héritage pour les véritables relations de type « est un » (is-a) lorsque vous avez besoin de polymorphisme
- Utilisez la composition pour réutiliser un comportement sans couplage fort, ou lorsqu'un objet a besoin de capacités provenant de sources multiples
- Ligne directrice générale : « privilégiez la composition plutôt que l'héritage »
Essayez vous-même
import 'computer.dart';
void main() {
// TODO : Créer un Computer nommé "Workstation" avec :
// - CPU Intel cadencé à 3.5 GHz
// - 16 GB de mémoire
// TODO : Appeler la méthode boot()
// TODO : Afficher le résultat de specs()
}
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
1Les bases de la POO
Fichiers externesBibliothèques et importsIntroduction à la POOClasses vs ObjetsLe mot-clé thisMéthodesVariables d'instanceBases des constructeursRécapitulatif - Calculatrice simple4Null Safety
Introduction à la Null SafetyNullable vs Non-NullableLes opérateurs ? et !Le mot-clé late et la Null SafetyLes opérateurs Null-AwareLa Null Safety dans les classesRécapitulatif - Système de profil utilisateur7Classes abstraites et Interfaces
Classes abstraitesMéthodes abstraitesInterfaces en DartInterfaces implicitesImplémentation vs ExtensionInterfaces multiplesRécapitulatif - Calculateur de formes10Collections et Génériques
Aperçu des List, Set et MapCollections Type-SafeClasses génériquesMéthodes génériquesContraintes de généricitéIterable & IteratorRécapitulatif - Stockage générique13Concepts avancés de la POO
Composition vs HéritageMéthodes d'extensionClasses appelablesClasses scellées (Dart 3)Records (Dart 3)Patterns et Matching (3.0)Enums avec méthodes2Les constructeurs en Dart
Constructeur par défautConstructeurs nommésListes d'initialisationConstructeurs constantsConstructeurs factoryConstructeurs de redirectionRécapitulatif - Shape Builder5Encapsulation
Membres publics vs privésLa convention du préfixe _Visibilité au niveau de la bibliothèqueApprofondissement des Getters & SettersMasquage d'informationsRécapitulatif - Dossiers d'étudiants8Mixins
Introduction aux MixinsCréation de MixinsUtilisation de plusieurs MixinsLe mot-clé on dans les MixinsMixin vs HéritageMixin vs InterfaceRécapitulatif - Système Animal11Méthodes spéciales
Redéfinition de toString()Redéfinition de hashCode & ==Interface ComparableMéthode call()Redéfinition de noSuchMethodRécapitulatif - Collection personnalisée14Patrons de conception Partie 1
Introduction aux patrons de conceptionPatron SingletonPatron FabriquePatron ObservateurPatron Stratégie3Propriétés de classe
Membres d'instance vs statiquesChamps Final & ConstVariables LateMéthodes et champs statiquesGetters et SettersRécapitulatif - Gestionnaire de compte bancaire6Héritage
Bases de l'héritageLe mot-clé superRedéfinition de méthodeL'annotation @overrideLe mot-clé final pour les classesConstructeurs et héritageRécapitulatif - Hiérarchie des employés9Polymorphisme
Bases du polymorphismePolymorphisme via les interfacesVérification de type à l'exécutionLes opérateurs is & asMot-clé covariantRécapitulatif - Processeur de paiement12POO Asynchrone
Futures & async/awaitBases des StreamsStream ControllersConstructeurs asynchronesL'asynchrone dans les méthodes de classeRécapitulatif - Data Fetcher15Patrons de conception Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Méthode ModèlePatron ÉtatPatron CompositePatron Repository