Ci siamo finora concentrati sulle caratteristiche e funzionalità del nuovo linguaggio che abbiamo imparato, e sulle sue (vaste!) potenzialità di utilizzo. Non ci siamo chiesti se ci possano essere margini per migliorare o ripensare il nostro metodo di analisi dei problemi e di programmazione, indipendentemente dal linguaggio scelto.
A dir la verità può darsi che non sentiamo neanche la necessità
di un miglioramento in questo campo, e questa obiezione è sostanzialmente
fondata finché la complessità dei problemi, e dei sistemi informativi
che li affrontano, rimane "piccola" (cioè confrontabile con quanto un singolo
analista o programmatore possa trattare).
Sappiamo tuttavia (e ci ricolleghiamo a quanto detto nella
lezione introduttiva) che la natura dei problemi reali
da affrontare,
ormai anche nel campo della ricerca scientifica, trascende largamente le
potenzialità di un singolo sviluppatore, e richiede opportuni stratagemmi
per tenere sotto controllo la complessità del progetto software,
e le risorse (soprattutto umane) destinate ad affrontarla.
Uno di questi stratagemmi, coronato da un certo successo
pratico nell'applicazione a grandi progetti, e quindi spesso riproposto
e riutilizzato, consiste nell'analisi, progettazione e programmazione
orientata agli oggetti (OO, in breve).
La prima importante distinzione da rilevare è quella
fra il processo di analisi e progettazione OO, ed i linguaggi di programmazione
OO (che implementano cioè alcuni dei meccanismi tipici richiesti dal modello
OO, come vedremo fra un istante). E' infatti possibile scrivere codice OO
senza usare un linguaggio OO, e scrivere codice procedurale (usiamo
questo termine per indicare i "normali" metodi di programmazione non OO)
utilizzando un linguaggio specializzato OO (come ad esempio il C++).
In sostanza l'impatto fondamentale del paradigma OO avviene prima
della stesura del codice: nella fase di analisi e progettazione.
Nei progetti software di grande dimensione, infatti, il
tempo dedicato alla scrittura del codice non supera mai il 20 % circa del
totale, (il tempo restante è ocupato dalla raccolta e analisi
dei requisiti, dalla progettazione e soprattutto dal debugging).
I vantaggi offerti dal modello si riflettono nella migliore modularità del
software prodotto (sono meglio definite le interfacce fra le varie
componenti), nella maggiore facilità a riutilizzare algoritmi e strutture
già scritte (si evita la duplicazione del lavoro), e nella
maggiore facilità nel
descrivere la struttura e la funzionalità del codice
(rende più semplice il ricambio dei programmatori e garantisce una
certa resistenza generazionale).
Si possono utilizzare vari approcci nel descrivere il modello ad oggetti. Uno dei più noti, utilizzato nel capitolo introduttivo del famoso libro di G. Booch, Object Oriented Analysis and Design (Addison-Wesley, 1994) si concentra sulle caratteristiche intrinseche del modello, valutando poi "se" un determinato linguaggio di programmazione può essere considerato OO oppure no. Un metodo alternativo potrebbe essere quello di identificare le caratteristiche che differenziano un linguaggio di programmazione OO da uno "non-OO". Entrambi i metodi danno in qualche modo per scontato "cosa" sia un oggetto. Partiremo dunque cercando di dare una definizione di "oggetto" (tenendo sullo sfondo le definizioni più precise di Booch), e gradualmente cercheremo, in modo molto pratico, di spostare la discussione sulle caratteristiche del linguaggio, arrivando ad identificare quelle caratteristiche di un linguaggio di programmazione OO che non riscontriamo nei linguaggi procedurali a cui siamo abituati.
Genericamente parlando, un oggetto può essere:
Generalmente un oggetto è caratterizzato da uno stato, da un comportamento e da un'identità, come bene simboleggiato da questa illustrazione tratta, come quelle che seguono, da G. Booch, Object-oriented Analysis and Design:
Come trasportare questo concetto nel familiare ambiente delle
strutture di dati?
Proviamo a partire con una definizione operativa, probabilmente sbagliata
(o perlomeno molto limitata):
Un oggetto è una combinazione di strutture dati e procedure tali da realizzare una serie di funzionalità che possano essere attribuite astrattamente all'oggetto stesso.
Qualche tempo fa abbiamo fatto l'esempio delle funzioni di gestione dei
grafici
(graph_helper.c).
Nell'esempio erano presenti funzioni per creare un nuovo
grafico (graph_create_new), per distruggere un grafico
esistente (graph_free), per aggiungere nuovi punti
(graph_add_point), per tracciare il grafico
(graph_plot), per cancellare la finestra
(graph_clear). Il "collegamento" fra queste funzioni era una
struttura di dati che veniva creata assieme ad ogni grafico:
typedef struct graph_s
{
int cx, cy; /* Center coordinates */
int xal, yal; /* Axis length in pixels */
double dx, dy; /* Scale for 1 pixel along x and y */
double xmax, xmin, ymax, ymin; /* Axis extremes */
char lx[20],ly[20]; /* Axis labels */
int nval; /* Number of values in the graph */
double *vx,*vy;/* Arrays of values */
int wid; /* ID of created window */
} graph;
Ora, noi sappiamo che esistono anche i puntatori a funzione, e li abbiamo usati varie volte. Perché allora non associare alla struttura di dati anche le funzioni necessarie per operare sui dati stessi? Potremmo avere una definizione di questo tipo:
typedef struct graph_s
{
int cx, cy; /* Center coordinates */
int xal, yal; /* Axis length in pixels */
double dx, dy; /* Scale for 1 pixel along x and y */
double xmax, xmin, ymax, ymin; /* Axis extremes */
char lx[20],ly[20]; /* Axis labels */
int nval; /* Number of values in the graph */
double *vx,*vy;/* Arrays of values */
int wid; /* ID of created window */
int (*add_point)(graph *graph, double x, double y); /*Pointer to add_point*/
void (*plot)(graph *graph, char *color); /* Pointer to plot function */
void (*clear)(graph *graph); /* Pointer to clear function */
void (*free)(graph *graph); /* Pointer to free function */
} graph;
In questo modo, quando abbiamo bisogno, ad esempio, di disegnare un
grafico, non dobbiamo ricordarci il nome della funzione che disegna
i grafici, ma semplicemente invocare questo "metodo" (così si chiamano
le funzioni che operano su un oggetto) dell'oggetto graph:
graph->plot(graph);
Già nel fatto che dobbiamo ripetere graph due volte si
evidenzia come il C non sia un linguaggio OO (anche se permette,
come in questo caso, qualche forma di programmazione OO).
Il linguaggio C inoltre non può sapere come inizializzare
automaticamente i puntatori
a funzione quando un nuovo "oggetto" viene creato. Questa operazione va fatta
a mano:
newgraph = (graph *)malloc(sizeof(graph)); (...) newgraph->add_point = graph_add_point; newgraph->plot = graph_plot; newgraph->clear = graph_clear; newgraph->free = graph_free;
Il C permette dunque, in qualche modo, di implementare la nostra definizione primitiva di oggetto, ma non altre caratteristiche che possono essere attribuite agli oggetti stessi. Grady Booch ne definisce principalmente quattro, che ora brevemente commentiamo:
graph * (normalmente
utilizzato
per "trasferire" un oggetto da un posto all'altro) può accedere a tutte
le variabili della struttura. Alcune di queste variabili, però,
appartengono ad un livello logico inferiore. Per esempio sarebbe meglio
che le scale degli
assi dx e dy, oppure il numero che identifica
la finestra creata (wid) non
venissero toccate dai livelli di codice superiori. In C non esiste
un sistema per evitare che questo accada. Nel modello ad oggetti,
è un requisito importante poter realizzare questa separazione di contesti.
graph
ha la generica proprietà di poter essere disegnato, esattamente
come un cerchio o una retta. graph disegna sostanzialmente
una sequenza di punti, o poli-linea: una sua applicazione più
particolareggiata potrebbe essere un oggetto che implementa gli stessi
"metodi" di graph, ma permette di disegnare una generica
funzione, dunque in qualche modo estende le funzionalità di
graph. Si possono anche individuare altre "superclassi"
o "sottoclassi" in relazione col nostro oggetto. La capacità
di "estendere" le funzionalità di un oggetto non è facile da implementare
in C: si dovrebbe definire una "nuova" struttura che contenga la vecchia, e
la catena di richiami comincerebbe a diventare lunga (ad esempio
function_graph->graph->plot(function_graph->graph);).
Si rende dunque necessaria la capacità di ereditare semplicemente
le funzionalità di un oggetto di gerarchia più generale:
Vediamo ora di riassumere le tre principali caratteristiche che un linguaggio di programmazione deve avere per soddisfare questi requisiti fondamentali del modello OO, e che non sono presenti nei normali linguaggi procedurali:
+): potrebbe essere utile definire degli oggetti
per i quali ha senso un'operazione di somma (potrebbero essere numeri
complessi, oppure quadrivettori spazio/tempo). Anche per il
nostro oggetto graph potrebbe avere senso una somma che
risulta nella concatenazione dei due grafici. I linguaggi OO consentono
di determinare il comportamento di una funzione (o anche di un'operatore
intrinseco come +) a partire dal tipo degli operandi.
Esistono vari linguaggi che aderiscono al paradigma OO che abbiamo cercato di tratteggiare. Questi linguaggi hanno un'evoluzione storica ed un dominio di applicazione molto vario:
Nell'ambiente della ricerca scientifica sono due i linguaggi OO che è più probabile incontrare:
struct estesa
prima descritta. Il C++ soffre di un grosso problema,
che rende tuttora preferibile il
C per la scrittura di applicazioni sufficientemente semplici:
nel corso della creazione degli oggetti, come è facile intuire, vengono
dinamicamente allocate in memoria le relative strutture di dati. Dal
momento che è molto più difficile seguire il "flusso" di un programma
OO, è piuttosto facile dimenticarsi, in fase di codifica, di liberare
sempre tutta la memoria allocata. E' abbastanza tipico che i
primi programmi scritti in C++ da chiunque siano letteralmente costellati
di memory leak. Lo standard del C++ inoltre subisce ancora
l'evoluzione di alcune delle funzionalità, il che può causare
problemi di compatibilità dei compilatori.
Rispetto al C, inoltre, il debugging è
più complicato (come si fa a identificare le funzioni polimorfe ?).