Composizione vs ereditarietà
Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 76 di 104.
Quando progetti relazioni tra classi, hai due opzioni principali: ereditarietà («è un») e composizione («ha un»). Scegliere l’approccio giusto influisce notevolmente sulla flessibilità e sulla manutenibilità del tuo codice.
L'ereditarietà crea un forte accoppiamento tra le classi. Una Car che eredita da Vehicle è legata in modo permanente a questa relazione:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Composizione incorpora oggetti come membri, creando una relazione più flessibile. Un Car ha un Engine invece di essere un motore:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Car possiede un Engine
public:
void start() { engine.start(); }
};Il vantaggio principale della composizione è la flessibilità. Puoi sostituire facilmente i componenti, modificare il comportamento durante l’esecuzione o combinare più funzionalità senza i vincoli di una gerarchia di classi rigida. Nella maggior parte dei casi, la progettazione moderna in C++ privilegia la composizione rispetto all’ereditarietà.
Usa l'ereditarietà quando c'è una vera relazione «è un» e hai bisogno del polimorfismo. Usa la composizione quando vuoi riutilizzare funzionalità o quando la relazione è descritta meglio come «ha un» o «usa un».
Sfida
FacileCostruiamo un simulatore di sistema informatico che mostri quando usare la composizione invece dell'ereditarietà. Modellerai un computer che ha dei componenti (composizione), anziché essere un componente, mostrando come la composizione offra la flessibilità di sostituire le parti durante l'esecuzione.
Organizzerai il codice in tre file:
Components.h: definisci i singoli componenti che un computer può contenere.Crea una classe
CPUcon unstd::string modelprivato e unint cores. Fornisci un costruttore che accetti entrambi i valori, un metodoprocess()che stampiCPU [model] processing with [cores] corese un metodogetModel().Crea una classe
Memorycon unint sizeGBprivato. Fornisci un costruttore, un metodoload()che stampiLoading data into [sizeGB]GB RAMe un metodogetSize().Crea una classe
Storagecon unstd::string typeprivato (come "SSD" o "HDD") e unint capacityGB. Fornisci un costruttore, un metodoread()che stampiReading from [capacityGB]GB [type]e metodi getter per entrambi i campi.Computer.h: definisci una classeComputerche utilizzi la composizione per combinare i componenti.La classe
Computerdovrebbe avere oggetti membro privati: unaCPU, unaMemorye unoStorage. Usa la lista di inizializzazione del costruttore per inizializzare questi componenti a partire dai parametri.Implementa questi metodi:
boot(): stampaBooting computer..., poi chiamaload()sulla memoria,read()sullo storage eprocess()sulla CPU, ciascuno su una riga separataspecs(): stampa le specifiche del computer in questo formato:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: leggi sei input (ciascuno su una riga separata):- Nome del modello della CPU (stringa)
- Numero di core della CPU (intero)
- Dimensione della memoria in GB (intero)
- Tipo di storage (stringa, ad esempio "SSD")
- Capacità dello storage in GB (intero)
- Un comando: "boot" o "specs"
Crea gli oggetti dei componenti, poi crea un
Computerusando la composizione. In base al comando, chiamaboot()oppurespecs().
Ad esempio, con gli input Intel i7, 8, 16, SSD, 512 e boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresCon gli input AMD Ryzen, 6, 32, HDD, 1000 e specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDNota che la classe Computer non eredita da alcun componente. Li contiene. Questa relazione «ha un» significa che potresti facilmente creare computer con diverse combinazioni di componenti o persino sostituire i componenti in seguito, aggiungendo metodi setter. Questa flessibilità è il vantaggio principale della composizione rispetto all'ereditarietà.
Provalo tu
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// Leggi gli input
string cpuModel;
getline(cin, cpuModel); // Nome del modello della CPU (può contenere spazi)
int cpuCores;
cin >> cpuCores; // Numero di core della CPU
int memorySize;
cin >> memorySize; // Dimensione della memoria in GB
string storageType;
cin >> storageType; // Tipo di archiviazione (ad es., "SSD")
int storageCapacity;
cin >> storageCapacity; // Capacità di archiviazione in GB
string command;
cin >> command; // Comando: "boot" o "specs"
// TODO: Crea gli oggetti dei componenti (CPU, Memory, Storage)
// TODO: Crea un oggetto Computer usando la composizione
// TODO: In base al comando, chiama boot() oppure specs()
return 0;
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti della programmazione orientata agli oggetti
File esterniBuild e compilazione in C++File header e file sorgenteNamespace e ambitoIntroduzione alla programmazione orientata agli oggetti in C++Classi e oggetti a confrontoIl puntatore 'this'Metodi (funzioni membro)Attributi (membri dati)Fondamenti di costruttori e distruttoriRiepilogo - Calcolatrice semplice4Proprietà delle classi
Membri di istanza e staticiGetter e setterFunzioni membro constParola chiave mutableMetodi e variabili staticiFunzioni e classi friendRiepilogo - Gestore di conti bancari7Ereditarietà
Ereditarietà di baseLivelli di accesso nell’ereditarietàOrdine di chiamata di costruttori e distruttoriRidefinizione dei metodiFunzioni virtuali e VTableEreditarietà multiplaEreditarietà virtualeRiepilogo - Gerarchia dei dipendenti2Gestione della memoria
Memoria Stack vs HeapPuntatori e riferimentiMemoria dinamica (new/delete)Puntatori intelligenti in C++RAII in C++Riepilogo - Gestore di array dinamico5Incapsulamento
Specificatori di accesso in C++Specificatori di accesso in dettaglioOccultamento delle informazioniStruct vs classClassi annidate e interneRiepilogo - Sistema di registrazione degli studenti8Polimorfismo
Polimorfismo a compile time e a runtimeOverload delle funzioniFunzioni virtuali: ripassoFunzioni virtuali pureClassi astratteProgettazione delle interfacce in C++Dynamic casting e RTTIRipasso: calcolatrice di forme11Concetti avanzati di OOP
Composizione vs ereditarietàMixin tramite CRTPIdiom PimplType ErasureEnum class e tipizzazione forteGestione delle eccezioni in OOPGerarchie personalizzate di eccezioni14Pattern di progettazione - Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeRAII come pattern3Costruttori e distruttori
Costruttore predefinitoCostruttore con parametriCostruttore di copiaCostruttore di spostamentoListe di inizializzazione dei costruttoriCostruttori delegantiApprofondimento sui distruttoriRegola del Tre / Cinque / ZeroRipasso - classe String6Sovraccarico degli operatori
Introduzione al sovraccarico degli operatoriSovraccarico degli operatori aritmeticiSovraccarico degli operatori di confrontoOperatori di flussoSovraccarico dell'operatore di assegnazioneSovraccarico degli operatori [] e ()Operatori di conversione di tipoRipasso - Classe Matrix9Template
Template di funzioneTemplate di classeSpecializzazione dei templateTemplate variadiciBasi di SFINAE e dei trait di tipoRiepilogo - Contenitore genericoEsercitati da solo: Compilatore C++ online