Diciannovesima lezione

Il modello a oggetti

(Introduzione...)

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.

Cos'è un oggetto?

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:

Caratteristiche di un linguaggio OO

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:

Linguaggi OO utilizzati in pratica

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: