Komposition vs. Vererbung
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 76 von 104.
Beim Entwurf von Beziehungen zwischen Klassen haben Sie zwei Hauptoptionen: Vererbung („ist-ein“) und Komposition („hat-ein“). Die Wahl des richtigen Ansatzes hat erhebliche Auswirkungen auf die Flexibilität und Wartbarkeit Ihres Codes.
Vererbung erzeugt eine enge Kopplung zwischen Klassen. Ein Car, das von Vehicle erbt, ist dauerhaft an diese Beziehung gebunden:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Komposition bettet Objekte als Member ein, wodurch eine flexiblere Beziehung entsteht. Ein Car hat einen Engine, anstatt ein Motor zu sein:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Auto HAT einen Motor
public:
void start() { engine.start(); }
};Der entscheidende Vorteil der Komposition ist Flexibilität. Sie können Komponenten einfach austauschen, das Verhalten zur Laufzeit ändern oder mehrere Funktionen kombinieren, ohne die Einschränkungen einer starren Klassenhierarchie. Modernes C++-Design bevorzugt in den meisten Fällen Komposition gegenüber Vererbung.
Verwenden Sie Vererbung, wenn eine echte „is-a“-Beziehung besteht und Sie Polymorphismus benötigen. Verwenden Sie Komposition, wenn Sie Funktionalität wiederverwenden möchten oder wenn die Beziehung besser als „has-a“ oder „uses-a“ beschrieben wird.
Aufgabe
EinfachLassen Sie uns einen Computersystem-Simulator bauen, der demonstriert, wann Komposition gegenüber Vererbung bevorzugt werden sollte. Sie werden einen Computer modellieren, der Komponenten besitzt (Komposition), anstatt selbst eine Komponente zu sein. Dies zeigt, wie Komposition die Flexibilität bietet, Teile zur Laufzeit auszutauschen.
Sie werden Ihren Code in drei Dateien organisieren:
Components.h: Definieren Sie die einzelnen Komponenten, die ein Computer enthalten kann.Erstellen Sie eine
CPU-Klasse mit einem privatenstd::string modelundint cores. Stellen Sie einen Konstruktor bereit, der beide Werte entgegennimmt, eineprocess()-Methode, dieCPU [model] processing with [cores] coresausgibt, und einegetModel()-Methode.Erstellen Sie eine
Memory-Klasse mit einem privatenint sizeGB. Stellen Sie einen Konstruktor bereit, eineload()-Methode, dieLoading data into [sizeGB]GB RAMausgibt, und einegetSize()-Methode.Erstellen Sie eine
Storage-Klasse mit einem privatenstd::string type(wie "SSD" oder "HDD") undint capacityGB. Stellen Sie einen Konstruktor bereit, eineread()-Methode, dieReading from [capacityGB]GB [type]ausgibt, und Getter-Methoden für beide Felder.Computer.h: Definieren Sie eineComputer-Klasse, die Komposition verwendet, um Komponenten zu kombinieren.Ihr
Computersollte private Member-Objekte haben: eineCPU, einMemoryund einStorage. Verwenden Sie die Konstruktor-Initialisierungsliste, um diese Komponenten aus den Parametern zu initialisieren.Implementieren Sie diese Methoden:
boot()— gibtBooting computer...aus und ruft dannload()für den Speicher,read()für den Datenträger undprocess()für die CPU auf, jeweils in einer eigenen Zeile.specs()— gibt die Spezifikationen des Computers in diesem Format aus:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Lesen Sie sechs Eingaben ein (jede in einer eigenen Zeile):- CPU-Modellname (String)
- Anzahl der CPU-Kerne (Integer)
- Speichergröße in GB (Integer)
- Speichertyp (String, z. B. "SSD")
- Speicherkapazität in GB (Integer)
- Ein Befehl: entweder "boot" oder "specs"
Erstellen Sie die Komponentenobjekte und erstellen Sie dann einen
Computermittels Komposition. Rufen Sie je nach Befehl entwederboot()oderspecs()auf.
Zum Beispiel mit den Eingaben Intel i7, 8, 16, SSD, 512 und boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresMit den Eingaben AMD Ryzen, 6, 32, HDD, 1000 und specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDBeachten Sie, dass die Computer-Klasse nicht von einer Komponente erbt – sie enthält sie. Diese "Hat-ein"-Beziehung bedeutet, dass Sie problemlos Computer mit verschiedenen Komponentenkombinationen erstellen oder Komponenten später sogar austauschen könnten, wenn Sie Setter-Methoden hinzufügen würden. Diese Flexibilität ist der entscheidende Vorteil der Komposition gegenüber der Vererbung.
Spickzettel
Wählen Sie beim Entwurf von Beziehungen zwischen Klassen zwischen Vererbung („is-a“) und Komposition („has-a“).
Vererbung erzeugt eine enge Kopplung zwischen Klassen:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Komposition bettet Objekte als Member ein und schafft so flexible Beziehungen:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Car HAS an Engine
public:
void start() { engine.start(); }
};Wann man was verwendet:
- Verwenden Sie Vererbung für echte „is-a“-Beziehungen, wenn Sie Polymorphie benötigen
- Verwenden Sie Komposition, um Funktionalität wiederzuverwenden oder für „has-a“/„uses-a“-Beziehungen
- Modernes C++-Design bevorzugt Komposition wegen ihrer Flexibilität – Sie können Komponenten austauschen, das Verhalten zur Laufzeit ändern und Funktionen kombinieren, ohne starre Hierarchien zu schaffen
Probier es selbst
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// Eingaben lesen
string cpuModel;
getline(cin, cpuModel); // CPU-Modellname (kann Leerzeichen enthalten)
int cpuCores;
cin >> cpuCores; // Anzahl der CPU-Kerne
int memorySize;
cin >> memorySize; // Speichergröße in GB
string storageType;
cin >> storageType; // Speichertyp (z. B. "SSD")
int storageCapacity;
cin >> storageCapacity; // Speicherkapazität in GB
string command;
cin >> command; // Befehl: "boot" oder "specs"
// TODO: Komponentenobjekte erstellen (CPU, Memory, Storage)
// TODO: Ein Computer-Objekt mittels Komposition erstellen
// TODO: Basierend auf dem Befehl entweder boot() oder specs() aufrufen
return 0;
}
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 DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container