Che materia stai cercando?

Anteprima

ESTRATTO DOCUMENTO

2. Il nome dell’operazione di ogg2 che ogg1 desidera eseguire.

3. Qualunque informazione supplementare (parametro) che sarà necessaria a ogg2 per eseguire

la sua operazione.

L’oggetto che invia il messaggio ( ogg1 nell’esempio precedente) viene chiamato mittente (sen-

der ) mentre l’oggetto che riceve il marcatore ( ogg2 ) prende il nome di destinatario (target) – altri

termini utilizzati per mittente e destinatario sono rispettivamente client e server . Il software del

robot offre diversi esempi di messaggi, uno dei quali è il seguente:

a1.avanza();

In questa istruzione, a1 punta all’oggetto destinatario del messaggio (ossia ne contiene la mani-

glia); se vi ricordate, la maniglia è stata assegnata ad a1 con l’istruzione di assegnamento var a1

: Robot = Robot.New() , mentre avanza() è il nome dell’operazione (appartenente all’og-

getto destinatario) che deve essere eseguita (questo messaggio non ha bisogno di parametri, perché

avanza() sposta il robot sempre di una posizione).

L’invio di un messaggio equivale alla tradizionale chiamata a funzione o procedura; per esempio,

in un linguaggio procedurale avremmo potuto scrivere

call avanza (a1);

osservate però l’inversione: con le tecniche software tradizionali ricorriamo a un’entità procedurale

e le forniamo l’oggetto sul quale agire; con l’impostazione a oggetti, invece, ricorriamo a un oggetto,

che poi esegue una delle sue entità procedurali.

A questo punto questa distinzione sembra semplicemente sintattica, o quanto meno filosofica.

Tuttavia, quando parleremo (nella sezione 8) di polimorfismo, sovraccarico e legame dinamico,

vedremo che questa enfasi sul concetto l’oggetto per primo, la procedura per seconda porta a

un’importante differenza pratica tra la struttura a oggetti e la struttura tradizionale. Ciò avviene

perché diverse classi di oggetti possono utilizzare lo stesso nome di operazione per operazioni che

mettono in atto diversi comportamenti specifici della classe o comportamenti simili ma con nomi

diversi.

5.2 Parametri dei messaggi

Come avveniva con i vecchi sottoprogrammi, la maggior parte dei messaggi passa parametri avanti

e indietro. Per esempio, se modificassimo l’operazione avanza() facendo in modo che l’operazione

restituisca un indicatore (flag) contenente il risultato dell’avanzamento, allora avremmo:

a1.avanza( in numeroPosizioni, out avanzamentoOK);

oggetto destinatario nome operazione parametri input parametri output

messaggio

19

1 : avanza(in numeroDiPosizioni, out avanzamentoOK)

ogg : Object a1 : Robot

Figura 11: Il messaggio a1.avanza(in numeroPosizioni, out avanzamentoOK), espresso con la

notazione UML.

Pertanto la struttura di un messaggio a un oggetto destinatario viene definita dalla segnatura

(o prototipo) dell’operazione del destinatario da invocare, che è costituita da tre parti: il nome

dell’operazione, l’elenco dei parametri di input (con prefisso in ) e l’elenco degli parametri di

output (con prefisso out ). Entrambi gli elenchi di parametri potrebbero essere vuoti; per brevità,

normalmente si omette la parola chiave in , considerandola predefinita.

I parametri di un messaggio riflettono un altro fondamentale contrasto tra il software orientato a

oggetti e quello tradizionale. In un ambiente a oggetti puro (come quello di Smalltalk), i parametri

del messaggio non sono dati, bensı̀ maniglie di oggetti. Gli argomenti dei messaggi sono oggetti

vivi!

Per esempio, la Figura 11 illustra un messaggio tratto dal programma del robot, espresso con

la notazione UML.

Se dovessimo fare un’istantanea del programma del robot nel momento in cui sta eseguendo

questo messaggio ed estrarre i valori dei parametri del messaggio, troveremmo qualcosa di inatteso.

Per esempio, potremmo trovare:

- numeroDiOuadrati impostato a 123432

- avanzamentoOK impostato a 664730

Perché questi strani numeri? Perché 123432 potrebbe essere la maniglia dell’oggetto (della classe

Integer ) che potremmo considerare come l’intero 2, mentre 664730 potrebbe essere la maniglia

dell’oggetto (della classe Boolean ) che normalmente considereremmo come valore logico true .

Per fare un altro esempio, se dovessimo eseguire un sistema di gestione del personale orientato

agli oggetti e facessimo la stessa cosa, potremmo trovare l’argomento impDelMese impostato a

441523, che potrebbe essere la maniglia dell’oggetto (della classe Impiegato ) che rappresenta

Giacomo Colombo.

5.3 I ruoli degli oggetti nei messaggi

In questa sezione analizzo i tre ruoli possibili che abbiamo indicato per un sistema orientato agli

oggetti. Un oggetto può essere:

1. mittente di un messaggio;

2. destinatario di un messaggio;

3. riferito da una variabile all’interno di un altro oggetto.

20

1 : messaggio1(...) 2 : messaggio2(...)

ogg1 : ClasseA ogg2 : ClasseB ogg3 : ClasseC

Figura 12: Due messaggi tra due coppie di oggetti.

Un dato oggetto può assumere uno o più di questi ruoli nel corso della sua esistenza. Dalla Figura 12

si evince chiaramente che un oggetto non è mai un mittente nato o un destinatario nato. Infatti,

per messaggiol(), ogg1 è il mittente e ogg2 è il destinatario; per messaggio2() , invece, ogg2

è il mittente e ogg1 è il destinatario. Vediamo pertanto che in diversi momenti lo stesso oggetto

può recitare entrambe le parti. I termini mittente e destinatario sono pertanto relativi a un dato

messaggio e non sono proprietà fisse degli oggetti stessi.

Un ambiente orientato agli oggetti puro contiene solo oggetti che giocano uno o più dei tre ruoli

visti sopra. Nella tecnologia a oggetti pura non c’è alcun bisogno di dati, perché gli oggetti possono

fare tutto il lavoro software necessario ai dati; e in Smalltalk (un linguaggio a oggetti molto puro),

non c’è proprio alcun dato! Al momento dell’esecuzione ci sono solo oggetti che puntano ad altri

oggetti (tramite variabili) e comunicano reciprocamente passandosi avanti e indietro maniglie di

altri oggetti ancora.

Invece, in C++ (che è un linguaggio misto, per il fatto che utilizza dati e funzioni tradizionali

insieme agli oggetti), gli argomenti possono essere puntatori a qualunque cosa. Se il codice C++

è puro come in Smalltalk, tutti i parametri saranno puntatori a oggetti. Se invece si mescolano

oggetti e dati in un programma, alcuni dei parametri possono essere semplicemente dei dati (o

puntatori a dati). Un commento simile è valido per il codice Java, sebbene quest’ultimo sia un

linguaggio molto meno a ruota libera rispetto al C++.

5.4 Tipi di messaggi

Un oggetto può ricevere tre tipi di messaggi: informativi, interrogativi e imperativi. In questa

sezione definiamo brevemente e offriamo un esempio di ogni tipo di messaggio, ancora una volta

ricorrendo a codice relativo al controllo del robot.

Definizione 8 (Messaggio informativo) Un messaggio informativo è un messaggio a un oggetto

che fornisce a quest’ultimo delle informazioni per aggiornarsi (è noto anche come messaggio di

aggiornamento, di inoltro o push). E un messaggio orientato al passato per il fatto che in genere

informa l’oggetto di ciò che è già avvenuto altrove.

Un esempio di messaggio informativo è:

impiegato.sposato(dataMatrimonio: Data)

questo messaggio dice a un oggetto, che rappresenta un dipendente, che il dipendente in questione si

è sposato in una certa data. In generale, un messaggio informativo comunica a un oggetto qualcosa

che è accaduto nella parte di mondo reale rappresentata da quell’oggetto.

Definizione 9 (Messaggio interrogativo) Un messaggio interrogativo è un messaggio a un og-

getto che richiede a quest’ultimo di rivelare alcune informazioni su di sé (è noto anche come mes-

saggio di lettura, retrospettiva o pulI). È un messaggio orientato al presente per il fatto che chiede

all’oggetto di comunicare informazioni correnti. 21

Un esempio di messaggio interrogativo è:

a1.posizione()

che chiede al robot di comunicare la sua posizione corrente sulla griglia. Questo tipo di mes-

saggio non modifica nulla, essendo in genere un’interrogazione relativa a quella parte di mondo

rappresentata dall’oggetto destinatario.

I metodi invocati tramite messaggi informativi e interrogativi sono di solito rispettivamente

definiti mediante la seguente sintassi:

getNomeattributo(): Classe

setNomeattributo ( nomeattributo: Classe )

dove Nomeattributo è il nome di un attributo (appartenente alla classe Classe ). In genere, per

un attributo possono essere definiti entrambi i metodi, e vengono brevemente indicati come metodi

set e metodi get .

Definizione 10 (Messaggio imperativo) Un messaggio imperativo è un messaggio a un oggetto

che richiede a quest’ultimo di portare a termine qualche azione su se stesso, su un altro oggetto o

persino sull’ambiente attorno al sistema (è noto anche come messaggio di forzatura o di azione).

È un messaggio orientato al futuro per il fatto che chiede all’oggetto di compiere qualche azione

nell’immediato futuro.

Un esempio di messaggio imperativo è

a1.avanza()

che provoca lo spostamento in avanti del robot. Questo tipo di messaggio spesso si traduce nel-

l’esecuzione da parte dell’oggetto destinatario di qualche algoritmo che gli consenta di fare quanto

richiesto.

I sistemi in tempo reale orientati agli oggetti, nei quali gli oggetti controllano componenti

hardware, spesso contengono molti messaggi imperativi. Questi messaggi illustrano chiaramente lo

spirito rivolto al futuro di un messaggio imperativo. Considerate questo esempio tratto dal mondo

della robotica:

manoSinistraRobot.vaiAPosizione (x,y,z: Lunghezza, thetal,theta2,theta3: Angolo)

Questo messaggio stabilisce la posizione e l’orientamento nello spazio della mano sinistra di un

robot. L’algoritmo potrebbe richiedere lo spostamento della mano del robot, del suo braccio e/o

quello del robot stesso. I sei argomenti rappresentano i sei gradi di libertà della mano, un’en-

tità tridimensionale nello spazio. A questo punto ci spostiamo dai messaggi a un’altra proprietà

indiscutibilmente fondamentale per l’orientamento agli oggetti, la classe di oggetti.

6 Classi

Nel software di controllo del robot abbiamo creato un oggetto (che rappresenta un robot) eseguendo

Robot.New() . La classe Robot è servita da modello per creare gli oggetti robot (come quello con

maniglia 602237). Ogni volta che eseguiamo l’istruzione Robot.New() , istanziamo un oggetto che

è strutturalmente identico a tutti gli altri oggetti creati dall’istruzione Robot.New() , dove per

22 123456

robot_1

ISTANZIAMENTO 234567

Robot robot_2 530061

robot_3

Figura 13: Tre oggetti istanziati dalla stessa classe ( Robot ).

strutturalmente identico intendiamo dire che ogni oggetto robot ha le stesse operazioni e variabili, in

particolare quelle che il programmatore ha codificato quando ha scritto la classe Robot (Figura 13).

Definizione 11 (Classe) Una classe è la sagoma a partire dalla quale vengono creati (istanziati)

gli oggetti. Ogni oggetto ha la stessa struttura e comportamento della classe dalla quale è istanziato.

Se l’oggetto ogg appartiene alla classe C , diciamo che ogg è un’istanza di C .

Vi sono due differenze tra gli oggetti di una stessa classe: ogni oggetto ha una maniglia diversa

e, in qualunque momento particolare, ogni oggetto probabilmente avrà uno stato diverso (il che

significa diversi valori memorizzati nelle sue variabili). Dato che inizialmente la distinzione tra

classe e oggetto può creare una certa confusione, suggeriamo queste semplici definizioni, che possono

aiutare a fare un po’ di chiarezza.

• Una classe è ciò che progettate e programmate.

• Gli oggetti sono ciò che create (a partire da una classe) in fase di esecuzione.

Pacchetti software molto noti presentano una stretta analogia con classi e oggetti. Supponiamo

che acquistiate un programma per foglio di calcolo chiamato Visigoth 5.0 prodotto dalla Wallisoft

Corp (fondata dallo stesso Wally Soft). Il pacchetto in sé sarebbe un analogo della classe, mentre

i fogli di calcolo veri e propri che create a partire da esso sarebbero simili agli oggetti. Ogni foglio

di calcolo ha a disposizione tutti i meccanismi di foglio di calcolo in quanto istanza della classe

Visigoth.

In fase di esecuzione, una classe come Robot può generare 3, 300 o 3.000 oggetti (ossia istanze

di Robot ). Una classe pertanto assomiglia a una sagoma: una volta ritagliata, da essa è possibile

ricavare la stessa forma migliaia di volte, ottenendo oggetti tutti identici tra loro e, ovviamente,

uguali alla forma della sagoma originale. Per essere ancora più chiari, analizziamo più da vicino

la popolazione di oggetti generati da una singola classe: come abbiamo visto, tutti gli oggetti di

una classe hanno la stessa struttura, ossia lo stesso insieme di operazioni e di attributi. Pertanto

23

oggetto1 oggetto2 oggetto3

metodoA varV metodoA varV metodoA varV

metodoB varW metodoB varW metodoB varW

metodoC varX metodoC varX metodoC varX

metodoD varY metodoD varY metodoD varY

maniglia maniglia maniglia

varZ varZ varZ

400 byte 10 byte 400 byte 10 byte 400 byte 10 byte

6 byte 6 byte 6 byte

Figura 14: I metodi, le variabili e le maniglie per tre oggetti della stessa classe, insieme con i

requisiti di memoria degli oggetti (approccio con spreco di risorse).

ogni oggetto (istanza) di una classe ha la propria copia dell’insieme di metodi che gli occorrono

per implementare le operazioni e l’insieme di variabili necessarie per implementare gli attributi. In

linea di principio, in un dato momento ci sono tante copie dei metodi e delle variabili (0, 3, 300,...)

quanti sono gli oggetti istanziati in quel momento (Figura 14).

Al fine di spiegare ulteriormente la reale struttura di un insieme di oggetti della stessa classe,

che chiameremo C , analizziamo brevemente i dettagli dell’implementazione. Supponiamo che ogni

metodo che implementa una delle operazioni della Figura 14 occupi 100 byte, che ogni variabile

occupi 2 byte e che per ogni maniglia occorrano 6 byte. In base a ciò, oggetto i occuperà 416 byte

di memoria (ossia 100 * 4 + 5 * 2 + 6 * 1). I tre oggetti insieme pertanto occuperanno 1.248 byte

di memoria (ossia 3 * 416).

Questo approccio all’allocazione della memoria per gli oggetti provocherebbe però un notevole

spreco di risorse, perché ognuno dei tre insiemi di metodi dei tre oggetti è identico; e dal momento

che ciascun insieme di metodi contiene solo codice procedurale, tutti gli oggetti possono condividere

un solo insieme. Quindi, sebbene in linea di principio ogni oggetto abbia il proprio insieme di metodi

per l’implementazione delle operazioni, in pratica (per risparmiare spazio) condividono tutti la

stessa copia fisica.

Di converso, sebbene le maniglie e le variabili di ciascun oggetto siano identiche in struttura da

un oggetto all’altro, esse non possono essere condivise tra più oggetti per l’ovvio motivo che devono

contenere diversi valori in fase di esecuzione.

Cosı̀, sebbene gli oggetti della classe C condividano tutti lo stesso insieme di operazioni, la

memoria totale consumata dai tre oggetti di C sarà effettivamente di 448 byte (400 byte per il

singolo insieme di metodi, 30 byte per i 3 set di variabili e 18 byte per le 3 variabili). Questo

consumo di soli 448 byte è inferiore ai 1.248 byte della soluzione nativa e rappresenta il modo

normale in cui gli ambienti a oggetti allocano la memoria per gli oggetti (Figura 15). Ovviamente il

risparmio cresce con il crescere del numero di oggetti istanziati: se con soli tre oggetti si consuma il

35,9% della memoria (ovvero 448 byte rispetto ai previsti 1.248 totali), con 300 oggetti il consumo

scende al 4,1% (5.200 byte rispetto a 124.800).

Quasi tutte le operazioni e gli attributi analizzati in questa sezione appartengono a singoli

oggetti e prendono il nome di operazioni di istanza e attributi di istanza. Tuttavia esistono

anche operazioni di classe e attributi di classe. Per definizione esiste sempre esattamente un insieme

di operazioni di classe e di attributi di classe per una data classe, indipendentemente dalla quantità

di oggetti di quella classe che possono essere stati istanziati. Le operazioni e gli attributi di classe

24

oggetto1 oggetto2 oggetto3

varV varV varV

metodoA varW varW varW

metodoB varX varX varX

metodoC varY varY varY

metodoD maniglia maniglia maniglia

varZ varZ varZ

6 byte

6 byte 6 byte

400 byte 10 byte 10 byte 10 byte

Figura 15: Illustrazione schematica della memoria effettiva (448 byte) utilizzata da 3 oggetti della

stessa classe.

sono necessari per far fronte alle situazioni che non possono essere responsabilità di un qualunque

oggetto singolo: l’esempio più famoso di un’operazione di classe è New() , che istanzia un nuovo

oggetto di una data classe.

Il messaggio New() non potrebbe mai essere inviato a un singolo oggetto. Supponiamo, per

esempio, di avere tre oggetti della classe ClienteBanca , che rappresentano tre effettivi clienti di

una banca (faremo riferimento a questi oggetti con i nomi bob , carol e ted ). Supponiamo

inoltre che desideriamo istanziare un nuovo oggetto dalla classe ClienteBanca (per esempio alice

). A quale oggetto invieremmo il messaggio New() ? Non ci sarebbe nessun particolare motivo

per inviarlo a bob piuttosto che a carol o a ted ; peggio ancora, non avremmo mai potuto

istanziare il primo cliente della banca, in quanto inizialmente non ci sarebbe stato nessun oggetto

della classe ClienteBanca al quale inviare il messaggio New() .

Quindi, New() è un messaggio che deve essere inviato a una classe, invece che a un singolo

oggetto. L’esempio presente nel gioco del robot era Robot.New() , un messaggio di classe inviato

alla classe Robot per ottenere l’esecuzione della sua operazione di classe New() e creare cosı̀ un

nuovo oggetto, una nuova istanza della classe Robot .

Un esempio di attributo di classe potrebbe essere numeroDiRobotCreati: Integer , che

verrebbe incrementato da New() della classe Robot a ogni esecuzione di questa operazione.

Indipendentemente dal numero di oggetti robot, ci sarebbe solo una copia di questo attributo di

classe. Potreste progettare un’operazione di classe per offrire al mondo esterno la possibilità di

accesso a questo attributo di classe.

La Figura 16 illustra la struttura della memoria nel caso in cui la classe C abbia due operazioni

di classe (i metodi delle quali occupano 100 byte ciascuno) e tre attributi di classe (le variabili

dei quali occupano 2 byte ciascuna). Il numero di byte per il meccanismo di classe (206 in questo

esempio) rimarrà costante a prescindere dal numero di oggetti che C ha istanziato. Con l’aggiunta

di questo meccanismo di classe, C e il suo gregge di dodici oggetti ora occupa un totale di 798

(ossia 206 + 592) byte di memoria.

Osservate che, sia in linea di principio sia in pratica, c’è un solo insieme di metodi di classe

per classe: questo è in contrasto con i metodi di istanza, dove in linea di principio ogni oggetto ha

il proprio insieme (solo per risparmiare memoria facciamo in modo che gli oggetti condividano lo

stesso insieme di metodi per le loro operazioni). La distinzione tra variabili di classe e variabili di

istanza è quindi più chiara: ogni classe ha solo un insieme di variabili di classe, mentre esiste un

insieme di variabili di istanza per ogni oggetto della classe, sia in linea di principio sia di fatto.

25

oggetto1 oggetto2 oggetto3

varV varV varV

metodoA varW varW varW

metodoB varX varX varX

metodoC varY varY varY

metodoD maniglia maniglia maniglia

varZ varZ varZ

400 byte 6 byte

6 byte 6 byte

10 byte 10 byte

10 byte variabili di istanza e

metodi di istanza

metodoE varP maniglie (48 byte)

(400 byte)

metodoF varQ variabili di classe

varR

200 byte (6 byte)

metodi di classe

6 byte (200 byte)

Figura 16: Illustrazione schematica della memoria effettiva (846 byte) utilizzata da 3 oggetti e dal

meccanismo di classe

Quale è allora la differenza tra una classe e un TDA? La risposta è che un TDA descrive

un’interfaccia: è la facciata che dichiara cosa verrà fornito agli utenti del TDA, senza però dire

alcunché sul modo in cui il TDA verrà implementato. Una classe è una cosa in carne e ossa (o

almeno dotata di disegno interno e codice) che implementa un TDA. In effetti per un dato TDA

si potrebbero progettare e costruire diverse classi: per esempio, una di queste classi potrebbe

produrre oggetti molto efficienti in esecuzione, mentre da un’altra si potrebbero ottenere oggetti

che occupano poca memoria.

6.1 Composizione ed Aggregazione di classi

Quando in una classe si includono oggetti appartenenti ad altre classi si realizza il concetto di

associazione di tipo tutto/parte tra classi. Due sono i tipici modi di realizzare questa associazione:

composizione e aggregazione. Nel primo caso l’oggetto interno esiste solo per poter realizzare la

parte dell’oggetto esterno, mentre nel secondo caso l’oggetto interno esiste indipendentemente del

suo ruolo di parte. Spieghiamo in dettaglio, analizzando brevemente anche la notazione UML che

permette di rappresentare le due situazioni.

Composizione: La composizione è una struttura comune nei sistemi software, siano essi o meno

orientati agli oggetti, perché nella vita di ogni giorno abbiamo a che fare con molti oggetti composti.

Per esempio, un messaggio di posta elettronica è un composto che contiene un’intestazione e alcuni

paragrafi di testo. A sua volta, l’intestazione è composta dal nome del mittente, da quello del

destinatario, dall’oggetto del messaggio e da altre informazioni specifiche del sistema di trasmissione

elettronica dei messaggi. Ora però, prima di continuare, è necessario fare qualche precisazione

terminologica. L’associazione tutto/parte prende il nome di composizione, dove il tutto viene

chiamato (oggetto) composto, mentre la parte prende il nome di (oggetto) componente. Ecco le tre

caratteristiche più importanti della composizione.

26

Aliante 2

Fusoliera Coda Ala

Figura 17: Un oggetto composto ed i suoi componenti.

1. L’oggetto composto non esiste senza i suoi componenti. Per esempio, rimuovete le setole, il

manico e le piccole parti in gomma da uno spazzolino da denti, e non avrete più uno spazzolino

da denti. In effetti è sufficiente rimuovete le setole da uno spazzolino da denti perché diventi

difficile qualificarlo come tale. In questo senso diciamo che la vita di un oggetto composto

non può andare oltre quella dei suoi componenti.

2. In qualunque momento, ciascun oggetto componente dato può essere parte di un solo compo-

sto.

3. La composizione tipicamente è eterogenea, nel senso che è molto probabile che i com- ponenti

siano di tipi misti: alcune ruote, alcuni assi, dei pezzi di legno... ed ecco un carro.

L’oggetto composto della Figura 17 rappresenta un aliante semplificato formato da quattro com-

ponenti: una fusoliera, una coda, un’ala sinistra e un’ala destra. Come ulteriore esempio, vedi

la Figura 2, dove un oggetto robot è progettato come aggregato di una Posizione e di una

Direzione .

Analizziamo questa figura per capire il funzionamento di UML.

1. Un’associazione tra l’oggetto composto e ciascuno dei suoi componenti appare sul diagramma

come una linea di associazione, con un piccolo rombo nero collocato all’estremità accanto

all’oggetto composto.

2. La classe del composto, Aliante , appare a un’estremità della linea di associazione, mentre

all’altra estremità appare la classe di ciascun componente, Fusoliera , Coda e Ala

. Osservate che un componente come Ala può limitarsi a comparire una sola volta nel

diagramma; il numero componenti di tipo Ala (la sua molteplicità) compare numericamente

sulla linea di associazione, vicino alla classe contenuta.

3. E’ necessario indicare la molteplicità all’estremità del componente di ciascuna linea di associa-

zione. Se la molteplicità all’estremità del composto non viene indicata, allora si presuppone

che sia esattamente 1.

4. La linea di associazione non ha nome, che è la norma sia per la composizione sia per l’ag-

gregazione. Il motivo è che raramente, nel caso di un’associazione di composizione, un nome

27

aggiunge un significato che vada oltre quello di tutto/parte già indicato dalla simbologia. Le

forme verbali come ha, comprende, consiste e cosı̀ via non aggiungono nulla al modello.

Per quanto concerne l’implementazione, all’interno della classe Aliante possono essere dichiarate

le seguenti variabili:

fusoliera: Fusoliera;

coda: Coda;

alaSinistra: Ala;

alaDestra: Ala;

Quando un oggetto della classe Aliante , chiamiamolo aliante1 , viene istanziato e inizializzato,

la variabile coda punterà a un oggetto che rappresenta la coda di aliante1 . Analogamente, le

variabili fusoliera , alaSinistra e alaDestra contengono le maniglie degli altri componenti di

un oggetto della classe Aliante . Questa implementazione supporta la navigabilità da un oggetto

composto ai suoi oggetti componenti.

La composizione spesso è legata alla propagazione dei messaggi. Per esempio, per spostare un

simbolo di rettangolo su uno schermo, potreste chiedere all’oggetto rettangolo di spostare se stesso.

A sua volta, il rettangolo potrebbe inviare un messaggio a ciascuno dei suoi segmenti componenti e

dire loro di muoversi. Analogamente, per trovare il peso di una sedia, potreste inviare un messaggio

a ciascun componente della sedia richiedendone il peso.

Aggregazione: Come la composizione, l’aggregazione è un costrutto ben noto, per mezzo del

quale i sistemi rappresentano strutture tratte dal mondo reale. Per esempio, una città è un aggre-

gato di case, una foresta è un aggregato di alberi e un gregge è un aggregato di pecore. In altre

parole, l’aggregazione e un associazione gruppo/membri. Ancora una volta occorre qualche preci-

sazione terminologica. L’associazione prende il nome di aggregazione, dove il tutto viene chiamato

(oggetto) aggregato, mentre la parte prende il nome di (oggetto) costituente. Le tre caratteristiche

più importanti dell’aggregazione sono indicate di seguito.

1. L’oggetto aggregato potenzialmente può esistere senza i suoi oggetti costituenti. Per esem-

pio, un dipartimento continua a esistere anche nel caso in cui vengano licenziati tutti i suoi

dipendenti.

2. In qualunque momento, ciascun oggetto può essere costituente di più di un aggregato. Ancora

una volta un aggregato reale potrebbe anche non avvalersi di questa proprietà.

3. L’aggregazione tende a essere omogenea, il che significa che gli oggetti costituenti di un tipico

aggregato apparterranno alla stessa classe. Per esempio, i costituenti di un paragrafo sono le

frasi, mentre i costituenti di una foresta sono tutti gli alberi.

Vediamo ora la notazione UML per rappresentare il costrutto di aggregazione. La Figura 18 mostra

una cassa formata da bottiglie di birra (per un ulteriore esempio, vedi anche la relazione tra le classi

Griglia e Robot nella Figura 2). Su questa figura è possibile fare le seguenti osservazioni.

1. Un’associazione tra un aggregato e i suoi costituenti viene indicata da un piccolo rombo vuoto

sulla linea di associazione all’estremità dell’aggregato.

2. Le classi dell’aggregato ( Cassa ) e dei costituenti ( BottiglieBirra ) appaiono alle rispettive

estremità della linea di associazione. 28

Cassa

0..16

BottiglieBirra

Figura 18: Un oggetto aggregato ed i suoi costituenti.

3. Con l’aggregazione è necessario indicare la molteplicità a entrambe le estremità della linea

di associazione, perché non è mai possibile ipotizzarla, come invece avviene nel caso della

composizione. La molteplicità all’estremità dell’aggregato della Figura 18 è 1, il che significa

che una bottiglia di birra può appartenere ad una cassa, oppure non appartenere a nessuna

cassa. La molteplicità all’estremità del costituente è 0..16, il che significa che una cassa può

includere fino a 16 bottiglie di birra, o anche nessuno (nel caso di una cassa vuota).

Come avviene con la composizione, è possibile implementare l’aggregazione per mezzo di variabili.

Per la navigabilità dall’aggregato ai costituenti, una variabile nell’aggregato punterà ai costituenti.

Per esempio, la classe Cassa può contenere la seguente dichiarazione:

bottiglie: BottiglieBirra[16];

che nel nostro pseudo-linguaggio potrebbe denotare una variabile array di 16 componenti di tipo

BottiglieBirra .

7 Ereditarietà

Cosa fare nel caso fosse necessario scrivere il codice di una classe C e, allo stesso tempo, fosse

disponobile una classe D quasi identica a C tranne per alcuni attributi e operazioni aggiuntivi?

La semplice soluzione di duplicare tutti gli attributi e le operazioni di C per collocarli in D , oltre

a comportare del lavoro aggiuntivo, renderebbe seccante il lavoro di manutenzione. Una soluzione

migliore è far sı̀ che la classe D in qualche modo, chieda di utilizzare le operazioni della classe C :

questa soluzione prende il nome di ereditarietà.

Definizione 12 (Ereditarietà) L’ereditarietà (di una classe D da una classe C ) è il meccanismo

tramite il quale D ha implicitamente definito su di essa ciascuno degli attributi e delle operazioni

della classe C come se tali attributi e operazioni fossero stati definiti per D stessa. C viene

definita superclasse di D , mentre D è una sottoclasse di C .

In altre parole, attraverso l’ereditarietà gli oggetti della classe D possono utilizzare attributi

e operazioni che altrimenti sarebbero disponibili solo agli oggetti della classe C . L’ereditarietà

29

rappresenta un’altra delle caratteristiche principali grazie alle quali la tecnologia a oggetti si distacca

dagli approcci dei sistemi tradizionali; essa infatti permette effettivamente di costruire il software

in modo incrementale distinguendo due fasi fondamentali.

Fase 1: Innanzi tutto si costruiscono le classi destinate a far fronte alle situazioni più generali.

Fase 2: Poi, per affrontare i casi particolari, si aggiungono classi più specializzate che eredita-

no dalle classi generali. Una classe specializzata avrà pertanto il diritto di utilizzare tutte

le operazioni e gli attributi (operazioni e attributi sia di classe sia di istanza) della classe

originale.

Un esempio può essere di aiuto per illustrare questo principio. Supponiamo di avere, in un’applica-

zione aeronautica, una classe Velivolo che può aver definita un’operazione di istanza denominata

vira() e un attributo di istanza di nome rotta .

La classe Velivolo ha a che fare con tutta l’attività e le informazioni pertinenti a qualunque

tipo di apparecchio volante; tuttavia esistono tipi speciali di velivolo che svolgono speciali attività e

pertanto richiedono informazioni particolari. Per esempio, un aliante svolge attività speciali (come

sganciare il cavo di rimorchio) e potrebbe dover registrare speciali informazioni (per esempio, se è

attaccato a un cavo di rimorchio).

Ecco allora che possiamo definire un’altra classe, Aliante , che eredita da Velivolo e avrà

un’operazione di istanza chiamata sganciacavoRimorchio() e un attributo di istanza di nome

seCavoRimorchioAttaccato (di classe Boolean ). Questo ci dà la struttura mostrata nella

Figura 19, nella quale la freccia bianca denota l’ereditarietà.

Ora analizziamo i meccanismi dell’ereditarietà immaginando un po’ di codice a oggetti che crea

oggetti delle classi Velivolo e Aliante e in seguito gli invia dei messaggi. Il codice è seguito

da un’analisi delle quattro istruzioni contrassegnate da (1) a (4).

var v: Velivolo := Velivolo.New();

var a: Aliante := Aliante.New();

v.vira(nuovaRotta, out viraoK); (1)

a.sganciaCavoRimorchio(); (2)

a.vira(nuovaRotta, out viraoK); (3)

v.sganciaCavoRimorchio(); (4)

(1) L’oggetto cui punta v riceve il messaggio vira(nuovaRotta, out viraOK) , che fa sı̀ che

esso applichi l’operazione vira() (con i parametri opportuni). Dal momento che v è

un’istanza di Velivolo , esso utilizzerà semplicemente l’operazione vira() che è stata

definita nella classe Velivolo .

(2) L’oggetto cui punta a riceve il messaggio sganciaCavoRimorchio() , che fa si che esso appli-

chi l’operazione sganciaCavoRimorchio() (che non richiede argomenti). Dal momento che a

è un’istanza di Aliante , esso utilizzerà semplicemente l’operazione sganciaCavoRimorchio()

che è stata definita nella classe Aliante .

(3) L’oggetto cui punta a riceve il messaggio vira(nuovaRotta, out viraOK) , che fa sı̀ che

esso applichi l’operazione vira() (con gli argomenti opportuni). Senza ereditarietà, questo

messaggio provocherebbe un errore in fase di esecuzione (come vira(): operazione non

definita ) perché a è un’istanza di Aliante , che non ha alcuna operazione denominata

30 Un attributo della classe Angolo

Velivolo (probabilmente memorizzato come

-rotta : Angolo variabile privata) che rappresenta

la rotta del velivolo.

+vira() Un attributo della classe Boolean

Aliante (probabilmente memorizzato come

variabile privata) che rindica se il

-seCavoRimorchioAttaccato : Boolean cavo del rimorchio è attaccato

+sganciaCavoRimorchio() all’aliante.

Figura 19: Aliante è una sottoclasse che eredita dalla sua superclasse, Velivolo .

vira() . Tuttavia, dato che Velivolo è una superclasse di Aliante , anche l’oggetto

a può utilizzare a pieno titolo qualunque operazione di Velivolo (se Velivolo avesse

una superclasse OggettoVolante , a potrebbe anche utilizzare qualunque operazione di

questa classe). Pertanto la linea di codice contrassegnata con (3) funzionerà senza problemi

e l’operazione vira() , cosı̀ come è stata definita per Velivolo , verrà eseguita.

(4) Questo non funzionerà! v si riferisce a un’istanza di Velivolo , che non ha alcuna

operazione denominata sganciaCavoRimorchio() . L’ereditarietà non è di alcun aiuto in

questo caso, dal momento che Aliante è l’unica classe che definisce al suo interno l’operazione

sganciaCavoRimorchio() e Aliante è una sottoclasse di Velivolo . Dato che l’ereditarietà

non funziona in questa direzione, il sistema si blocca e segnala un errore di esecuzione. Se

ci pensiamo bene, comunque, ciò ha un senso, perché v potrebbe puntare a un oggetto che

rappresenta un grande aereo a reazione, per il quale sganciaCavoRimorchio() non avrebbe

significato.

Nella sezione 6 abbiamo visto la distinzione tra classe e oggetto; ora invece vedremo che esiste

anche una sottile distinzione tra oggetto e istanza. Sebbene finora abbiamo utilizzato i termini

oggetto e istanza quasi come sinonimi, vedremo che l’ereditarietà in un certo senso permette a

un singolo oggetto di essere contemporaneamente un’istanza di più di una classe. Ciò corrisponde

bene a quanto accade nel mondo reale. Se possedete un aliante, avete esattamente un oggetto

con un’identificazione (maniglia). Tuttavia questo aliante è (ovviamente) un esempio di aliante

e allo stesso tempo un esempio di velivolo. Dal punto di vista concettuale, quindi, l’oggetto che

rappresenta la cosa che possedete è un’istanza di Aliante e un’istanza di Velivolo .

Effettivamente l’esempio precedente costituisce un test rivelatore per un valido uso dell’eredita-

rietà, che prende il nome di test è un (is-a). Se potete dire: un D è un C , allora D quasi certamente

deve essere una sottoclasse di C . Quindi, dato che un aliante è un velivolo, la classe Aliante deve

essere una sottoclasse di Velivolo . Analizziamo ulteriormente la questione gettando uno sguardo

a ciò che avviene dietro le quinte dell’ereditarietà. L’oggetto cui fa riferimento a sarà rappresen-

tato in fase di esecuzione dall’unione di due parti: una parte saranno le operazioni e gli attributi di

istanza definiti per Aliante , l’altra le operazioni e gli attributi di istanza definiti per Velivolo .

Nella maggior parte dei linguaggi, la sottoclasse che eredita riceve tutto ciò che la superclasse ha da

offrire, senza scegliere cosa ereditare. Esistono tuttavia alcuni accorgimenti che permettono a una

31 Velivolo

VeicoloPasseggeri

VelivoloPasseggeri

Figura 20: Ereditarietà multipla: una sottoclasse con più superclassi.

sottoclasse di sovrascrivere (ossia modificare) le operazioni ereditate, come vedremo nella sezione 8.

Il codice per implementare effettivamente l’ereditarietà in un buon linguaggio a oggetti è semplice:

è sufficiente dichiarare la superclasse nella definizione di ogni sottoclasse che deve ereditare da essa.

Per esempio,

class Aliante inherits from Velivolo;

L’esempio presentato in questa sezione è di ereditarietà singola, il che significa che ogni classe

ha al massimo una superclasse diretta; vi sono anche casi di ereditarietà multipla, nel qual caso

ogni classe può avere un numero arbitrario di superclassi dirette. L’ereditarietà multipla trasforma

la struttura ad albero dell’ereditarietà singola in un reticolo di ereditarietà, come mostrato nella

Figura 20.

L’ereditarietà multipla introduce alcune difficoltà di progettazione, compresa la possibilità che una

sottoclasse erediti dai suoi progenitori operazioni o attributi in contrasto tra loro (le operazioni in

contrasto hanno lo stesso nome e la sottoclasse che eredita non riesce a decidere facilmente quale

ereditare). Difficoltà come il problema del contrasto di nomi hanno dato all’ereditarietà multipla

una cattiva reputazione. Nel corso degli anni sia la condanna sia la difesa dell’ereditarietà multipla

hanno raggiunto livelli parossistici. Comunque, dal momento che l’ereditarietà multipla può creare

strutture complesse e poco comprensibili, deve essere utilizzata sensatamente, ancora di più di

quanto avviene con l’ereditarietà singola. Attualmente due dei principali linguaggi a oggetti (C++

e Eiffel) permettono l’ereditarietà multipla, mentre gli altri due (Java e Smalltalk) non la accettano.

8 Polimorfismo

La parola polimorfismo deriva da due termini greci che significano rispettivamente molti e forme:

una cosa polimorfa pertanto ha la proprietà di assumere molte forme. I manuali di programmazione

a oggetti contengono due definizioni di polimorfismo, contrassegnate con (A) e (B) nella nota

sottostante. Si tratta di due definizioni valide, ed entrambe le proprietà del polimorfismo agiscono

di concerto per fornire una grande potenza alla tecnologia a oggetti. Più avanti in questa sezione

analizzeremo in modo più approfondito queste due definizioni.

Definizione 13 (Polimorfismo) 32

Figura 21: Alcuni poligoni piani.

poligono Esagono

Rettangolo

Triangolo

Figura 22: Poligono e le sue tre sottoclassi.

(a) Il polimorfismo è il meccanismo grazie al quale un singolo nome di operazione o di attributo

può essere definito per più di una classe e può assumere diverse implementazioni in ciascuna

di quelle classi.

(b) Il polimorfismo è la proprietà per mezzo della quale un attributo o una variabile può puntare

a (detenere la maniglia di) oggetti di diverse classi in diversi momenti.

Supponiamo di avere una classe Poligono che rappresenta il tipo di forme bidimensionali illustrato

dalla Figura 21. Potremmo definire un’operazione denominata calcolaArea() per Poligono ,

che restituisce il valore dell’area di un oggetto di tipo Poligono (osservate che area è un attributo

definito per Poligono e, tramite ereditarietà, per le sue sottoclassi). L’operazione calcolaArea()

ha bisogno di un algoritmo abbastanza sofisticato perché deve preoccuparsi dei vari poligoni di forma

strana della Figura 21.

Ora aggiungiamo alcune classi, per esempio Triangolo , Rettangolo ed Esagono , che sono

sottoclassi di Poligono e pertanto ereditano da essa. Ciò è accettabile perché un triangolo è un

poligono, un rettangolo è un poligono e cosı̀ via (Figura 22).

Osservate che nella Figura 22 anche le classi Triangolo e Rettangolo hanno delle operazioni

denominate calcolaArea() , che svolgono lo stesso compito della versione di calcolaArea()

presente nella classe Poligono , ossia calcolare l’area della superficie racchiusa dalla forma.

Il progettista/programmatore del codice che implementa calcolaArea() per Rettangolo

tuttavia scriverebbe il codice in modo molto diverso da quello dell’operazione calcolaArea()

di Poligono . Perché? Dato che è semplice calcolare l’area di un rettangolo (base altezza), il

codice dell’operazione calcolaArea() di Rettangolo è di conseguenza semplice ed efficiente. Dal

momento invece che l’algoritmo per calcolare l’area di un poligono arbitrario complesso è complicato

e meno efficiente, non desideriamo utilizzarlo per calcolare l’area di un rettangolo.

33

Se pertanto scrivessimo del codice che invia il seguente messaggio a un oggetto riferito da

formaBidimensionale :

formaBidimensionale.calcolaArea();

potremmo non sapere quale algoritmo per il calcolo dell’area verrà eseguito. Il motivo è che po-

tremmo non sapere esattamente a quale classe appartiene formaBidimensionale . Ci sono cinque

possibilità:

1. formaBidimensionale è un’istanza di Triangolo e verrà eseguita l’operazione calcolaArea()

nella forma definita per Triangolo .

2. formaBidimensionale è un’istanza di Rettangolo e verrà eseguita l’operazione calcolaArea()

nella forma definita per Rettangolo .

3. formaBidimensionale e un istanza di Esagono , ma dato che a questa classe manca

un’operazione di nome calcolaArea() , tramite ereditarietà verrà eseguita l’operazione

calcolaArea() nella forma definita per Poligono .

4. formaBidimensionale è un’istanza generale di forma arbitraria della classe Poligono e

verrà eseguita l’operazione calcolaArea() nella forma definita per Poligono .

5. formaBidimensionale e un istanza di una classe C diversa da tutte le classi precedenti.

Dal momento che probabilmente per C non è stata definita alcuna operazione denominata

calcolaArea() , l’invio del messaggio calcolaArea() provocherà un errore di compilazione

o esecuzione.

Anche se è possibile trovare strano che un oggetto possa non conoscere con precisione l’esatta clas-

se dell’oggetto destinatario cui sta inviando il messaggio, questa situazione è abbastanza comune.

Per esempio, nella linea finale del codice riportato qui sotto, al momento della compilazione non

è possibile sapere a quale classe di oggetti punterà p in fase di esecuzione: l’oggetto effettiva-

mente puntato sarà determinato da una scelta effettuata dall’utente all’ultimo momento (verificata

dall’istruzione if ).

var p: Poligono := Poligono.New();

var t: Triangolo := Triangolo.New();

var h: Esagono := Esagono.New();

...

if "l’utente dice OK"

then p := t

else p := h

endif;

...

p.calcolaArea(); // qui p pu\‘o fare riferimento a un oggetto

// Triangolo o Esagono

... 34

In questo frammento di codice a oggetti non abbiamo bisogno di un test attorno a p.calcolaArea()

per determinare quale versione di calcolaArea() eseguire. Questo è un esempio di occultamento

dell’implementazione molto pratico, che permette di aggiungere una nuova sottoclasse di Poligono

(per esempio Ottagono ) senza modificare in alcun modo il codice precedente. Per ricorrere a una

metafora, l’oggetto destinatario sa come calcolare la propria area e pertanto il mittente non si deve

preoccupare.

Osservate anche la dichiarazione var p: Poligono si tratta di una limitazione di sicurezza

al polimorfismo della variabile p . Nella sintassi di programmazione utilizzata qui significa che a

p è consentito puntare solo a oggetti della classe Poligono (o ad oggetti di una delle sottoclassi

di Poligono ). Se a p venisse assegnata la maniglia di un oggetto Cliente o Cavallo , il

programma si interromperebbe e segnalerebbe un errore di esecuzione.

L’operazione calcolaArea() , essendo definita per diverse classi, offre un valido esempio di

polimorfismo, cosı̀ come nella definizione 13(a); la variabile p invece, essendo in grado di puntare

a oggetti di molte classi diverse (per esempio Triangolo ed Esagono ), è un buon esempio della

definizione 13(b). L’esempio nel complesso mostra come i due aspetti del polimorfismo operino di

concerto per agevolare il compito del programmatore.

Un ambiente a oggetti spesso implementa il polimorfismo attraverso il legame dinamico: l’am-

biente verifica la classe effettiva dell’oggetto destinatario di un messaggio all’ultimo momento

possibile, ossia in fase di esecuzione, quando il messaggio viene inviato.

Definizione 14 (Dynamic binding) Il dynamic binding (legame dinamico o legame in esecu-

zione o legame tardivo) è la tecnica per cui l’esatto codice da eseguire viene determinato solo al

momento dell’esecuzione e non in fase di compilazione.

L’esempio precedente, in cui l’operazione calcolaArea() viene definita per Poligono e Triangolo

, dimostra anche il concetto di sovrascrittura (overriding).

Definizione 15 (Overriding) L’overriding (sovrascrittura) è la ridefinizione di un metodo defi-

nito per una classe C in una della sottoclassi di C .

L’operazione calcolaArea() , che in origine è stata definita per Poligono , viene sovrascritta

in Triangolo da un’operazione che ha lo stesso nome ma un algoritmo diverso. Occasionalmente

è possibile utilizzare le tecniche di sovrascrittura per neutralizzare un’operazione definita per una

classe C in una della sottoclassi di C . Ciò è possibile semplicemente ridefinendo l’operazione in

modo che restituisca un errore. Se tuttavia vi trovate ad affidarvi molto a queste azioni di neu-

tralizzazione, è probabile che siate partiti con una gerarchia di superclassi/sottoclassi traballante.

Correlato al concetto di polimorfismo c’è quello di overloading (sovraccarico), che non va confuso

con la sovrascrittura.

Definizione 16 (Overloading) L’overloading (sovraccarico) di un nome o simbolo avviene quan-

do diverse operazioni (o operatori) definite per la stessa classe hanno quel nome o simbolo. In questo

caso diciamo che un nome o simbolo è sovraccaricato.

Polimorfismo e overloading spesso richiedono che l’operazione specifica da eseguire venga scelta in

fase di esecuzione. Come abbiamo visto nel codice dell’esempio precedente, il motivo e che la classe

esatta dell’oggetto destinatario, e pertanto l’implementazione specifica dell’oggetto da eseguire, può

non essere nota fino al momento dell’esecuzione.

35

La normale distinzione tra polimorfismo e overloading è che il primo consente di definire lo

stesso nome di operazione in modo diverso per classi diverse, mentre con il secondo è possibile

definire lo stesso nome di operazione in modo diverso per più volte all’interno della stessa classe.

L’operazione polimorfa che verrà selezionata dipende solo dalla classe dell’oggetto destinatario

cui il messaggio è indirizzato. Ma nel caso di un’operazione soggetta a overloading, come avviene

il legame del frammento di codice corretto con il nome dell’operazione al momento dell’esecuzione?

Tramite la segnatura, ossia il numero e la classe degli argomenti, del messaggio. Ecco due esempi:

la. prodotto1.ribassa()

lb. prodotto1.ribassa(altaPercentuale)

2a. matrice1 * i

2b. matrice1 * matrice2

Nel primo esempio, il prezzo di un prodotto è ridotto dall’operazione ribassa. Se questa operazione

viene richiamata senza argomenti (come in 1a), l’operazione utilizza una percentuale di sconto

standard; se invece ribassa viene invocata con un argomento (l’argomento altaPercentuale di

1b), allora l’operazione applica il valore fornito da altaPercentuale .

Nel secondo esempio l’operatore di moltiplicazione * è sovraccaricato. Se il secondo operando

è un intero (come in 2a), allora l’operatore * indica una moltiplicazione scalare, mentre nel caso

in cui sia un’altra matrice (come in 2b) indica una moltiplicazione tra matrici.

9 Genericità

Definizione 17 (Genericità) La genericità è la costruzione di una classe C in modo tale che

una o più delle classi che essa utilizza internamente venga fornita solo in fase di esecuzione (al

momento in cui un oggetto della classe C viene istanziato).

Illustriamo il concetto di genericità mediante un esempio. Supponiamo di dover progettare e pro-

grammare un albero binario di ricerca bilanciato contenente numeri interi (Figura 23). Un albero

binario è detto di ricerca se le informazioni contenute nei nodi appartengono ad un insieme ordinato

I ed inoltre, per ogni nodo x dell’albero, valgono le seguenti proprietà:

• tutte le informazioni contenute nel sottoalbero sinistro di x precedono (secondo l’ordinamento

definito in I) l’informazione contenuta in x;

• tutte le informazioni contenute nel sottoalbero destro di x seguono (secondo l’ordinamento

definito in I) l’informazione contenuta in x. 2

Inoltre, un generico albero è detto bilanciato se, per ogni nodo, la differenza fra le profondità dei

suoi due sottoalberi è al più 1.

Tutto ciò è molto semplice, almeno fino a quando non si deve inserire un altro intero nella

struttura (per esempio 5, come nella Figura 24). A questo punto l’albero può sbilanciarsi e si

è costretti a eseguire delle operazioni contorte e faticose per far riguadagnare all’albero il suo

equilibrio. Queste operazioni faticose possono costituire un algoritmo di ribilanciamento. Se fosse

necessario, per altri motivi, mantenere degli elenchi aggiornati di clienti e prodotti, si potrebbe

recuperare il codice prodotto per la gestione di alberi di interi e copiarlo due volte, sostituendo in

una copia Integer con IDCliente e nell’altra Integer con IDprodotto .

2 La profondità di un albero è definita come la massima distanza tra la radice dell’albero ed una delle sue foglie

36 26

17 42

14 22 31 44

10 15 29 34

Figura 23: Un albero binario di ricerca bilanciato (contenente numeri interi).

26

17 42

14 22 31 44

10 15 29 34

5

Figura 24: L’albero di figura 23 dopo l’inserimento del numero 5. L’albero ottenuto non è più

bilanciato (cf. profondità dei sottoalberi del nodo contenente il numero 17).

Questa donazione del vecchio codice può aumentare notevolmente la produttività, anche se

questo approccio presenta un significativo pericolo di donazione: si può essere costretti a mantenere

tre copie di codice quasi identico.

Ciò significa che, nel caso in cui venga progettato un algoritmo migliore per il bilanciamento

dell’albero, è necessario rivedere tre parti di codice; questa gestione di tre versioni pertanto compor-

ta non solo del lavoro aggiuntivo, ma risulta anche complicata (a meno di non riuscire a realizzare

rapidamente una procedura automatizzata per la modifica). Ciò che occorre è un modo per scrivere

una sola volta la struttura fondamentale dell’algoritmo di bilanciamento dell’albero per poi poterla

applicare tutte le volte che né abbiamo necessità ad interi, clienti, prodotti o qualunque altro tipo

di dato.

In un caso come questo, la genericità fornisce un notevole aiuto. Se definiamo AlberoBilanciato

come classe parametrica (in C++ le classi parametriche sono note come template), significa che al-

meno una delle classi utilizzate all’interno di AlberoBilanciato non deve necessariamente essere

assegnata fino al momento dell’esecuzione. Questa presumibilmente sarebbe la classe degli elemen-

37


PAGINE

43

PESO

266.00 KB

AUTORE

Atreyu

PUBBLICATO

+1 anno fa


DESCRIZIONE DISPENSA

Il codice che viene presentato è una parte di una semplicissima applicazione a oggetti che visualizza una sorta di robot in miniatura che si sposta su una griglia sullo schermo (il genere di entità che è possibile vedere in un videogame). Sebbene l’orientamento agli oggetti non sia certamente un approccio limitato alle applicazioni grafiche, un’applicazione di questo tipo fornisce un eccellente esempio operativo. Vengono trattati i seguenti argomenti: incapsulamento, occultamento delle informazioni e dell’implementazione, conservazione dello stato, identità degli oggetti.


DETTAGLI
Corso di laurea: Corso di laurea in ingegneria informatica e automatica
SSD:
Università: L'Aquila - Univaq
A.A.: 2011-2012

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher Atreyu di informazioni apprese con la frequenza delle lezioni di Programmazione a oggetti e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università L'Aquila - Univaq o del prof Di Stefano Gabriele.

Acquista con carta o conto PayPal

Scarica il file tutte le volte che vuoi

Paga con un conto PayPal per usufruire della garanzia Soddisfatto o rimborsato

Recensioni
Ti è piaciuto questo appunto? Valutalo!

Altri appunti di Programmazione a oggetti

Gestione File in C++
Dispensa
Standard Template Library
Dispensa
Ethernet - Le diverse famiglie di reti Ethernet
Dispensa
Reti Wireless
Dispensa