Komposition vs. Vererbung
Teil des Abschnitts Objektorientierte Programmierung der Dart-Journey von Coddy — Lektion 82 von 110.
Sie haben gelernt, dass Vererbung eine „is-a“-Beziehung erzeugt – ein Dog ist ein Animal. Aber es gibt einen anderen leistungsstarken Ansatz: Komposition, die eine „has-a“-Beziehung erzeugt. Anstatt Verhalten zu erben, enthält eine Klasse Instanzen anderer Klassen und delegiert die Arbeit an diese.
Betrachten Sie eine Car-Klasse. Mit Vererbung könnten Sie versuchen, eine Engine-Klasse zu erweitern – aber ein Auto ist kein Motor, es hat einen Motor:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Komposition: Ein Auto HAT einen Motor
void start() {
_engine.start(); // An den Motor delegieren
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}Der entscheidende Vorteil der Komposition ist die Flexibilität. Bei der Vererbung sind Sie an eine einzige Elternklasse gebunden. Mit Komposition können Sie mehrere Komponenten kombinieren und diese sogar zur Laufzeit austauschen:
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();
}
}
}Ein bekannter Grundsatz lautet „Komposition gegenüber Vererbung bevorzugen“. Verwenden Sie Vererbung, wenn eine echte „ist-ein“-Beziehung besteht und Sie Polymorphie benötigen. Verwenden Sie Komposition, wenn Sie Verhalten ohne enge Kopplung wiederverwenden möchten oder wenn ein Objekt Funktionen aus mehreren Quellen benötigt.
Aufgabe
EinfachLassen Sie uns ein Computersystem mittels Komposition bauen! Anstatt zu versuchen, einen Computer von seinen Teilen erben zu lassen, werden Sie separate Komponentenklassen erstellen und diese zusammensetzen – denn ein Computer hat eine CPU und hat Speicher, er ist keine CPU oder Speicher.
Sie werden Ihren Code in drei Dateien organisieren:
components.dart: Erstellen Sie zwei Komponentenklassen, die Computerteile repräsentieren:- Eine
CPU-Klasse mit einemString brandunddouble speedGHz. Fügen Sie eineprocess()-Methode hinzu, die einen String zurückgibt:'$brand processing at ${speedGHz}GHz' - Eine
Memory-Klasse mit einemint sizeGB. Fügen Sie eineload()-Methode hinzu, die einen String zurückgibt:'Loading ${sizeGB}GB of data'
- Eine
computer.dart: Erstellen Sie eineComputer-Klasse, die Komposition verwendet, um die Komponenten zu kombinieren. Ihr Computer sollte:- Ein
String name-Feld haben - Eine
CPU- und eineMemory-Instanz enthalten (Komposition – der Computer HAT diese Teile) - Alle notwendigen Werte über seinen Konstruktor akzeptieren, um beide Komponenten intern zu erstellen
- Eine
boot()-Methode haben, die an beide Komponenten delegiert und drei Zeilen ausgibt: den Namen des Computers gefolgt von' starting...', dann das Ergebnis der process-Methode der CPU, dann das Ergebnis der load-Methode des Speichers - Eine
specs()-Methode haben, die einen String zurückgibt, der das System beschreibt:'$name: ${cpu.brand} ${cpu.speedGHz}GHz, ${memory.sizeGB}GB RAM'
- Ein
main.dart: Importieren Sie Ihre Dateien und demonstrieren Sie die Komposition in Aktion:- Erstellen Sie einen
Computernamens'Workstation'mit einer'Intel'-CPU, die mit3.5GHz läuft, und16GB Speicher - Rufen Sie die
boot()-Methode auf - Geben Sie das Ergebnis von
specs()aus
- Erstellen Sie einen
Beachten Sie, dass die Computer-Klasse nicht CPU oder Memory erweitert – sie enthält diese und delegiert die Arbeit an sie. Dies ist die „hat-ein“-Beziehung (has-a), die durch Komposition entsteht!
Erwartete Ausgabe:
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAMSpickzettel
Komposition erstellt eine „hat-ein“-Beziehung, bei der eine Klasse Instanzen anderer Klassen enthält und Arbeit an diese delegiert, anstatt von ihnen zu erben.
Einfaches Beispiel für Komposition:
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');
}
}Komposition mit Flexibilität – Austausch von Komponenten:
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();
}
}
}Wann man Komposition gegenüber Vererbung bevorzugt:
- Verwenden Sie Vererbung für echte „ist-ein“-Beziehungen, wenn Sie Polymorphie benötigen
- Verwenden Sie Komposition, um Verhalten ohne enge Kopplung wiederzuverwenden oder wenn ein Objekt Funktionen aus mehreren Quellen benötigt
- Allgemeine Richtlinie: „Bevorzugen Sie Komposition gegenüber Vererbung“
Probier es selbst
import 'computer.dart';
void main() {
// TODO: Erstelle einen Computer namens "Workstation" mit:
// - Intel CPU mit 3.5 GHz
// - 16 GB Arbeitsspeicher
// TODO: Rufe die boot() Methode auf
// TODO: Gib das Ergebnis von specs() aus
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienBibliotheken & ImportsEinführung in die OOPKlassen vs. ObjekteDas this-SchlüsselwortMethodenInstanzvariablenKonstruktor-GrundlagenWiederholung – Einfacher Taschenrechner4Null-Sicherheit
Einführung in Null-SicherheitNullable vs. Non-NullableDie ? und ! OperatorenLate-Schlüsselwort & Null-SicherheitNull-aware OperatorenNull-Sicherheit in KlassenZusammenfassung – Benutzerprofil-System7Abstrakte Klassen & Interfaces
Abstrakte KlassenAbstrakte MethodenInterfaces in DartImplizite InterfacesImplementieren vs. ErweiternMehrfache InterfacesRückblick – Shape Calculator10Collections & Generics
Überblick über List, Set und MapTypsichere CollectionsGenerische KlassenGenerische MethodenGenerische ConstraintsIterable & IteratorZusammenfassung – Generische Speicherung13Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungExtension MethodsCallable ClassesSealed Classes (Dart 3)Records (Dart 3)Patterns & Matching (3.0)Enums mit Methoden16Projekt: Bibliotheksverwaltung
ProjektübersichtBuch- und Benutzerklassen2Konstruktoren in Dart
Standard-KonstruktorBenannte KonstruktorenInitialisierungslistenKonstante KonstruktorenFactory-KonstruktorenWeiterleitende KonstruktorenZusammenfassung – Shape Builder5Kapselung
Öffentliche vs. private MemberDie _ Präfix-KonventionSichtbarkeit auf Library-EbeneGetter & Setter im DetailInformation HidingRückblick – Schülerdatensätze8Mixins
Einführung in MixinsMixins erstellenMehrere Mixins verwendenDas on-Schlüsselwort in MixinsMixin vs. VererbungMixin vs. InterfaceZusammenfassung - Tiersystem11Spezielle Methoden
toString() überschreibenhashCode & == überschreibenComparable-Interfacecall()-MethodenoSuchMethod überschreibenZusammenfassung – Eigene Collection14Entwurfsmuster Teil 1
Einführung in EntwurfsmusterSingleton-MusterFactory-MusterObserver-MusterStrategy-Muster3Klasseneigenschaften
Instanz- vs. statische MemberFinal & Const FelderLate-VariablenStatische Methoden & FelderGetter und SetterZusammenfassung – Bankkonto-Manager6Vererbung
Grundlagen der VererbungDas super-SchlüsselwortMethoden überschreibenDie @override-AnnotationDas final-Klassen-SchlüsselwortKonstruktoren & VererbungRückblick – Mitarbeiter-Hierarchie9Polymorphismus
Polymorphismus-GrundlagenPolymorphismus über InterfacesTypprüfung zur LaufzeitDie is & as OperatorenDas covariant SchlüsselwortZusammenfassung – Payment Processor