ProgrammazioneArchitetto Software C++

Che cos'è l'aggregazione (aggregation) e la composizione (composition) in C++? Come si differenziano l'una dall'altra e quando utilizzare quale approccio?

Supera i colloqui con l'assistente IA Hintsage

Risposta.

Nella programmazione in C++, spesso si utilizzano due modi per unire oggetti: aggregazione e composizione. Queste concetti riflettono diverse relazioni tra le classi e influenzano il ciclo di vita e la responsabilità per la distruzione degli oggetti correlati.

Storia della Questione:

Nella progettazione orientata agli oggetti, è sempre stato importante separare le dipendenze tra gli oggetti. Con l'avvento dei linguaggi orientati agli oggetti (Smalltalk, C++, Java), sorge la questione di come modellare al meglio le relazioni "parte - tutto". In C++ questo è diventato particolarmente rilevante a causa della gestione manuale della memoria e del ciclo di vita degli oggetti.

Problema:

Una scelta errata tra aggregazione e composizione porta a perdite di memoria, duplicazione delle risorse o errori di distruzione degli oggetti. Inoltre, questi concetti vengono spesso confusi.

Soluzione:

  • Composizione: è una relazione in cui un oggetto possiede un oggetto-parte e si occupa della sua creazione/distruzione. In C++ questo è solitamente espresso tramite un membro-classe per valore o tramite unique_ptr.
  • Aggregazione: è un legame più debole, l'oggetto-parte esiste al di fuori del "tutto" e la responsabilità per il suo ciclo di vita non ricade sul proprietario. Di solito è implementata tramite un riferimento (pointer/reference) non owning.

Esempio di codice:

// Composizione: class Engine {}; class Car { Engine engine; // Engine è creato e distrutto insieme a Car }; // Aggregazione: class Person {}; class Team { std::vector<Person*> members; // Punta a oggetti Person, non li possiede };

Caratteristiche chiave:

  • Composizione — legame forte (parte di), possiede
  • Aggregazione — legame debole (usa), non possiede
  • La composizione automatizza la gestione della memoria, l'aggregazione richiede cautela e accordi sui proprietari

Domande trabocchetto.

Se in un membro-classe c'è un puntatore a un oggetto, è sempre aggregazione?

No! Se la classe possiede questo puntatore (ad esempio, tramite std::unique_ptr), è comunque composizione. Il tipo di legame è definito non dal tipo del campo, ma dalla responsabilità per il ciclo di vita.

class House { std::unique_ptr<Room> room; // composizione, House possiede Room };

Può la composizione essere implementata tramite un riferimento o raw pointer?

Può — ma solo se l'oggetto è creato e distrutto dal proprietario, e il riferimento o il puntatore è utilizzato per ottimizzazione. Tuttavia, è molto meglio utilizzare oggetti per valore o smart pointers per esprimere esplicitamente la proprietà.

Cosa succede se, nella composizione, l'oggetto-parte è creato al di fuori del proprietario e gli viene passato?

In tal caso si corre il rischio di violare gli invarianti della composizione: se l'oggetto creato esternamente è passato al proprietario e questo lo distrugge, mentre altrove esiste ancora un riferimento — si avrà un dangling pointer. È necessario definire rigorosamente i diritti di proprietà e la responsabilità per la distruzione nel progetto.

Errori tipici e anti-pattern

  • Mischiare i concetti di aggregazione e composizione (ad esempio, memorizzare puntatori raw non necessari ma cercare di distruggerli nel distruttore del proprietario)
  • Utilizzare aggregazione dove è necessario un ciclo di vita rigoroso (ad esempio, dettagli di un oggetto complesso)
  • Non liberare puntatori non-owning

Esempio dalla vita reale

Caso Negativo

Un team ha deciso di memorizzare tutti gli oggetti nidificati tramite puntatori raw in un contenitore e distruggerli manualmente nel distruttore. Tutto ha funzionato finché non hanno cambiato lo schema di proprietà. Di conseguenza, il puntatore è stato liberato due volte, causando un crash.

Pro:

  • Flessibilità architettonica per alcuni scenari (ad esempio, relazioni fluide tra oggetti)

Contro:

  • Alto rischio di errori nella gestione della memoria
  • Difficile da mantenere

Caso Positivo

Un altro team ha adottato std::unique_ptr per tutte le relazioni realmente proprietarie e ha utilizzato non-owning solo sotto forma di riferimenti temporanei. Questo ha espresso chiaramente l'architettura.

Pro:

  • Relazioni di proprietà trasparenti e comprensibili
  • Nessuna perdita o doppia liberazione

Contro:

  • Non sempre possibile la composizione ciclica
  • A volte è necessario adattare i protocolli di comunicazione tra gli oggetti