Kompozycja a dziedziczenie
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 76 z 104.
Projektując relacje między klasami, masz dwie główne możliwości: dziedziczenie („jest”) i kompozycję („ma”). Wybór odpowiedniego podejścia znacząco wpływa na elastyczność i łatwość utrzymania kodu.
Dziedziczenie tworzy silne powiązanie między klasami. Obiekt Car, który dziedziczy po Vehicle, jest trwale związany z tą relacją:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Kompozycja osadza obiekty jako składowe, tworząc bardziej elastyczną relację. Car ma Engine, zamiast być silnikiem:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Car POSIADA Engine
public:
void start() { engine.start(); }
};Główną zaletą kompozycji jest elastyczność. Możesz łatwo wymieniać komponenty, zmieniać zachowanie w czasie działania programu lub łączyć wiele możliwości bez ograniczeń sztywnej hierarchii klas. W większości przypadków nowoczesne projektowanie w C++ opiera się na kompozycji zamiast dziedziczenia.
Używaj dziedziczenia, gdy występuje rzeczywista relacja „jest czymś” i potrzebujesz polimorfizmu. Używaj kompozycji, gdy chcesz ponownie wykorzystać funkcjonalność lub gdy relację lepiej opisują określenia „ma” lub „używa”.
Wyzwanie
ŁatwyZbudujmy symulator systemu komputerowego, który pokazuje, kiedy używać kompozycji zamiast dziedziczenia. Zamodelujesz komputer, który ma komponenty (kompozycja), zamiast być komponentem, pokazując, jak kompozycja zapewnia elastyczność wymiany części w czasie działania programu.
Rozmieścisz swój kod w trzech plikach:
Components.h: Zdefiniuj poszczególne komponenty, z których może składać się komputer.Utwórz klasę
CPUz prywatnymi polamistd::string modeliint cores. Zdefiniuj konstruktor przyjmujący obie wartości, metodęprocess(), która wyświetlaCPU [model] processing with [cores] cores, oraz metodęgetModel().Utwórz klasę
Memoryz prywatnym polemint sizeGB. Zdefiniuj konstruktor, metodęload(), która wyświetlaLoading data into [sizeGB]GB RAM, oraz metodęgetSize().Utwórz klasę
Storagez prywatnymi polamistd::string type(takimi jak "SSD" lub "HDD") orazint capacityGB. Zdefiniuj konstruktor, metodęread(), która wyświetlaReading from [capacityGB]GB [type], oraz metody pobierające wartości obu pól.Computer.h: Zdefiniuj klasęComputer, która łączy komponenty za pomocą kompozycji.Klasa
Computerpowinna mieć prywatne obiekty składowe:CPU,MemoryiStorage. Użyj listy inicjalizacyjnej konstruktora, aby zainicjalizować te komponenty na podstawie parametrów.Zaimplementuj następujące metody:
boot(): wyświetlaBooting computer..., a następnie wywołujeload()dla pamięci,read()dla pamięci masowej orazprocess()dla procesora, każdą z tych metod w osobnym wierszuspecs(): wyświetla specyfikację komputera w następującym formacie:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Wczytaj sześć danych wejściowych (każdą w osobnym wierszu):- Nazwa modelu procesora (ciąg znaków)
- Liczba rdzeni procesora (liczba całkowita)
- Rozmiar pamięci w GB (liczba całkowita)
- Typ pamięci masowej (ciąg znaków, np. "SSD")
- Pojemność pamięci masowej w GB (liczba całkowita)
- Polecenie: "boot" albo "specs"
Utwórz obiekty komponentów, a następnie utwórz obiekt
Computerza pomocą kompozycji. W zależności od polecenia wywołajboot()albospecs().
Na przykład, dla danych wejściowych Intel i7, 8, 16, SSD, 512 i boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresDla danych wejściowych AMD Ryzen, 6, 32, HDD, 1000 i specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDZwróć uwagę, że klasa Computer nie dziedziczy po żadnym komponencie. Ona je zawiera. Ta relacja „ma” oznacza, że możesz łatwo tworzyć komputery z różnymi zestawami komponentów, a nawet wymieniać komponenty później, jeśli dodasz metody ustawiające. Ta elastyczność jest kluczową zaletą kompozycji w porównaniu z dziedziczeniem.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// Wczytaj dane wejściowe
string cpuModel;
getline(cin, cpuModel); // Nazwa modelu CPU (może zawierać spacje)
int cpuCores;
cin >> cpuCores; // Liczba rdzeni CPU
int memorySize;
cin >> memorySize; // Rozmiar pamięci w GB
string storageType;
cin >> storageType; // Typ nośnika danych (np. "SSD")
int storageCapacity;
cin >> storageCapacity; // Pojemność nośnika danych w GB
string command;
cin >> command; // Polecenie: "boot" lub "specs"
// TODO: Utwórz obiekty komponentów (CPU, Memory, Storage)
// TODO: Utwórz obiekt Computer z użyciem kompozycji
// TODO: W zależności od polecenia wywołaj boot() lub specs()
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online