Komposition vs. Vererbung
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 76 von 104.
Beim Entwerfen von Beziehungen zwischen Klassen hast du zwei Hauptoptionen: Vererbung („ist-ein“) und Komposition („hat-ein“). Die Wahl des richtigen Ansatzes wirkt sich erheblich auf die Flexibilität und Wartbarkeit deines Codes aus.
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 Mitglieder ein und schafft dadurch eine flexiblere Beziehung. 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; // Car HAT einen Engine
public:
void start() { engine.start(); }
};Der entscheidende Vorteil der Komposition ist ihre Flexibilität. Du kannst Komponenten problemlos austauschen, das Verhalten zur Laufzeit ändern oder mehrere Fähigkeiten kombinieren, ohne durch eine starre Klassenhierarchie eingeschränkt zu sein. Beim modernen C++-Design wird in den meisten Fällen die Komposition der Vererbung vorgezogen.
Verwende Vererbung, wenn eine echte „is-a“-Beziehung besteht und du Polymorphismus benötigst. Verwende Komposition, wenn du Funktionalität wiederverwenden möchtest oder wenn sich die Beziehung besser als „has-a“ oder „uses-a“ beschreiben lässt.
Aufgabe
EinfachErstellen wir einen Computersystem-Simulator, der demonstriert, wann Komposition anstelle von Vererbung verwendet werden sollte. Du modellierst einen Computer, der Komponenten hat (Komposition), anstatt selbst eine Komponente zu sein, und zeigst damit, wie Komposition die Flexibilität bietet, Teile zur Laufzeit auszutauschen.
Du verteilst deinen Code auf drei Dateien:
Components.h: Definiere die einzelnen Komponenten, die ein Computer enthalten kann.Erstelle eine
CPU-Klasse mit einem privatenstd::string modelundint cores. Stelle einen Konstruktor bereit, der beide Werte entgegennimmt, eineprocess()-Methode, dieCPU [model] processing with [cores] coresausgibt, sowie einegetModel()-Methode.Erstelle eine
Memory-Klasse mit einem privatenint sizeGB. Stelle einen Konstruktor bereit, eineload()-Methode, dieLoading data into [sizeGB]GB RAMausgibt, sowie einegetSize()-Methode.Erstelle eine
Storage-Klasse mit einem privatenstd::string type(zum Beispiel „SSD“ oder „HDD“) undint capacityGB. Stelle einen Konstruktor, eineread()-Methode, dieReading from [capacityGB]GB [type]ausgibt, sowie Getter-Methoden für beide Felder bereit.Computer.h: Definiere eineComputer-Klasse, die Komposition verwendet, um Komponenten zu kombinieren.Dein
Computersollte private Memberobjekte haben: eineCPU, einenMemoryund einenStorage. Verwende die Initialisierungsliste des Konstruktors, um diese Komponenten anhand von Parametern zu initialisieren.Implementiere diese Methoden:
boot(): gibtBooting computer...aus und ruft anschließendload()für den Speicher,read()für den Speicher undprocess()für die CPU auf, jeweils in einer eigenen Zeilespecs(): gibt die Spezifikationen des Computers in diesem Format aus:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Lies sechs Eingaben ein (jeweils in einer eigenen Zeile):- CPU-Modellname (String)
- Anzahl der CPU-Kerne (Ganzzahl)
- Speichergröße in GB (Ganzzahl)
- Speichertyp (String, z. B. „SSD“)
- Speicherkapazität in GB (Ganzzahl)
- Ein Befehl: entweder „boot“ oder „specs“
Erstelle die Komponentenobjekte und anschließend mithilfe von Komposition ein
Computer-Objekt. Rufe abhängig vom Befehl entwederboot()oderspecs()auf.
Beispielsweise 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 HDDBeachte, dass die Computer-Klasse von keiner Komponente erbt. Sie enthält sie. Diese „hat-eine“-Beziehung bedeutet, dass du problemlos Computer mit unterschiedlichen Komponentenkombinationen erstellen oder Komponenten später sogar austauschen könntest, wenn du Setter-Methoden hinzufügen würdest. Diese Flexibilität ist der entscheidende Vorteil von Komposition gegenüber Vererbung.
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 & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins mittels CRTPPimpl-IdiomTyplöschungEnum-Klassen & starke TypisierungAusnahmebehandlung in OOPBenutzerdefinierte Ausnahmehierarchien3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler