Reimpiego per composizione
Benche` non sia stato esplicitamente mostrato, non c'e` alcun limite alla complessita`
di un membro dato di un oggetto; un attributo puo` avere sia tipo elementare
che tipo definito dall'utente, in particolare un attributo puo` a sua volta
essere un oggetto.
class Lavoro {
public:
Lavoro(/* Parametri */);
/* ... */
private:
/* ... */
};
class Lavoratore {
public:
Lavoratore(Lavoro* occupazione);
/* ... */
private:
Lavoro* Occupazione;
/* ... */
};
L'esempio mostrato suggerisce un modo di reimpiegare codice gia` pronto quando
si e` di fronte ad una relazione di tipo Has-a, in
cui una entita` piu` piccola e` effettivamente parte di una piu` grossa. In
questo caso il reimpiego e` servito per modellare una proprieta` della classe
Lavoratore , ma sono possibili casi ancora piu` complessi:
class Complex {
public:
Complex(float Real=0, float Immag=0);
Complex operator+(Complex &);
Complex operator-(Complex &);
/* ... */
private:
float Re, Im;
};
class Matrix {
public:
Matrix();
Matrix operator+(Matrix &);
/* ... */
private:
Complex Data[10][10];
};
In questo secondo esempio invece il reimpiego della classe Complex
ci consente anche di definire le operazioni sulla classe Matrix
in termini delle operazioni su Complex (un approccio matematicamente
corretto).
Tuttavia la composizione puo` essere utilizzata anche per modellare una relazione
di tipo Is-a, in cui invece una istanza di un certo
tipo puo` essere vista anche come istanza di un tipo piu` "piccolo":
class Person {
public:
Person(const char* name, unsigned age);
void PrintName();
/* ... */
private:
const char* Name;
unsiggned int Age;
};
class Student {
public:
Student(const char name, unsigned age,
const unsigned code);
void PrintName();
/* ... */
private:
Person Self;
const unsigned int IdCode; // numero di matricola
/* ... */
};
Student::Student(const char* name, unsigned age,
const unsigned code)
: Self(name, age), IdCode(code) {}
void Student::PrintName() {
Self.PrintName();
}
/* ... */
In sostanza la composizione puo` essere utilizzata anche quando vogliamo semplicemente
estendere le funzionalita` di una classe realizzata in precedenza (esistono
tecnologie basate su questo approccio).
Esistono due tecniche di composizione:
- Contenimento diretto;
- Contenimento tramite puntatori.
Nel primo caso un oggetto viene effettivamente inglobato all'interno di un altro
(come negli esempi visti), nel secondo invece l'oggetto contenitore in realta`
contiene un puntatore. Le due tecniche offrono vantaggi e svantaggi differenti.
Nel caso del contenimento tramite puntatori:
- L'uso di puntatori permette di modellare relazioni 1-n,
altrimenti non modellabili se non stabilendo un valore massimo per n;
- Non e` necessario conoscere il modo in cui va costruito una componente nel
momento in cui l'oggetto che la contiene viene istanziato;
- E` possibile che piu` oggetti contenitori condividano la stessa componente;
- Il contenimento tramite puntatori puo` essere utilizzato insieme all'ereditarieta`
e al polimorfismo per realizzare classi di oggetti che non sono completamente
definiti fino al momento in cui il tutto (compreso le parti accessibili tramite
puntatori) non e` totalmente costruito.
L'ultimo punto e` probabilmente il piu` difficile da capire e richiede la conoscenza
del concetto di ereditarieta` che sara` esaminato in seguito. Sostanzialmente
possiamo dire che poiche` il contenimento avviene tramite puntatori, in effetti
non possiamo conoscere l'esatto tipo del componente, ma solo una sua interfaccia
generica (classe base) costituita dai messaggi cui l'oggetto puntato sicuramente
risponde. Questo rende il contenimento tramite puntatori piu` flessibile e potente
(espressivo) del contenimento diretto, potendo realizzare oggetti il cui comportamento
puo` cambiare dinamicamente nel corso dell'esecuzione del programma (con il contenimento
diretto invece oltre all'interfaccia viene fissato anche il comportamento ovvero
l'implementazione del componente). Pensate al caso di una classe che modelli un'auto:
utilizzando un puntatore per accedere alla componente motore, se vogliamo testare
il comportamento dell'auto con un nuovo motore non dobbiamo fare altro che fare
in modo che il puntatore punti ad un nuovo motore. Con il contenimento diretto
la struttura del motore (corrispondente ai membri privati della componente) sarebbe
stata limitata e non avremmo potuto testare l'auto con un motore di nuova concezione
(ad esempio uno a propulsione anzicche` a scoppio). Come vedremo invece il polimorfismo
consente di superare tale limite. Tutto cio` sara` comunque piu` chiaro in seguito.
Consideriamo ora i principali vantaggi e svantaggi del contenimento diretto:
- L'accesso ai componenti non deve passare tramite puntatori;
- La struttura di una classe e` nota gia` in fase di compilazione, si conosce
subito l'esatto tipo del componente e il compilatore puo` effettuare molte
ottimizzazioni (e controlli) altrimenti impossibili (tipo espansione delle
funzioni inline dei componenti);
- Non e` necessario eseguire operazioni di allocazione e deallocazione per
costruire le componenti, ma e` necessario conoscere il modo in cui costruirle
gia` quando si istanzia (costruisce) l'oggetto contenitore.
Se da una parte queste caratteristice rendono il contenimento diretto meno
flessibile ed espressivo di quello tramite puntatore e anche vero che lo rendono
piu` efficente, non tanto perche` non e` necessario passare tramite i puntatori,
ma quanto per gli ultimi due punti.
|