L'evoluzione della programmazione: dagli Spaghetti Code agli Oggetti.
Seconda parte.


Riprendiamo il discorso interrotto la scorsa puntata, e ripartiamo con l'analizzare i limiti della Programmazione Procedurale.

Con la Programmazione Procedurale le subroutine diventano delle "black box" (scatole nere) ed ancor di più nella Programmazione Strutturata. Con le black box dall'esterno è impossibile vedere o alterare quello che accade all'interno della subroutine e ciò garantisce un elevato grado di astrazione e di sicurezza, perché l'unico modo di interagire con il codice al suo interno è tramite il passaggio di parametri alla chiamata o dall'insieme dei risultati restituiti dalla subroutine. Questo è ciò che comunemente viene definito "interfaccia tra il mondo esterno e quello interno".
Per utilizzare una collezione di subroutine così organizzata, è sufficiente conoscere come passare i dati da elaborare e come recuperare i risultati indipendentemente da come avvenga la loro elaborazione.

Immagine struttura Procedure

Sicuramente questo modo di procedere è più sicuro rispetto allo Spaghetti Code, ma non elimina gli errori più insidiosi che si commettono nello sviluppo del software, e forse questo può essere indicato come il più grave limite della Programmazione Procedurale.
Il programmatore è forzato a dichiarare, prima del loro uso, variabili e strutture dati, ma non è tenuto ad inizializzarle correttamente prima del loro utilizzo. Per questo motivo le subroutine possono elaborare, senza saperlo, dati corrotti o privi di senso. Tutto ciò oltre a produrre risultati non attendibili, secondo la nota legge "Garbage in, garbage out" (se introduci spazzatura, ottieni spazzatura), può anche provocare problemi di instabilità nella macchina ospite.
Infatti se una subroutine deve manipolare una porzione di memoria contenente dati e riceve un puntatore errato, corre il rischio di alterare i dati relativi ad altri programmi, magari in esecuzione in multitasking sulla stessa macchina, scatenando così una reazione a catena che porterà in breve al blocco della macchina stessa, a meno che il sistema operativo che la correda non sia dotato di Memoria Protetta.
La Memoria Protetta è un tentativo di impedire il blocco del sistema ospite da parte di programmi impazziti o malfunzionanti, infatti a ciascuno di essi è associata una zona di memoria e nessuno può accedere alla memoria di altri programmi: ogni tentativo è intercettato e bloccato dalla Memoria Protetta.
Ciò non toglie che ciascun programma sia libero di massascrare dati ed istruzioni nella propria porzione di memoria, compromettendo comunque la stabilità della macchina o l'attendibilità dell'elaborazione eseguita.

Nella Programmazione Strutturata, che ha spopolato negli anni '80, il nodo cruciale resta il "codice", proprio perché ancora legato alla struttura stessa delle procedure: ottimizzare il programma equivaleva a scrivere lo stesso in modo che durante l'esecuzione risultasse il più veloce possibile.
In virtù di tale linea guida i programmatori violavano le regole del buon senso introdotte con la Programmazione Procedurale. Per evitare passaggi ritenuti inutili e lenti, si accedeva direttamente alle strutture dati per manipolarne i contenuti il più velocemente possibile, in questo modo l'uso delle interfaccie di astrazione non ha più ragione di esistere, ma quello che contava era che il programma funzionasse e che fosse veloce.
Ben presto ci si rese conto di aver sbagliato con un ragionamento simile ed ancora oggi ne paghiamo lo scotto. I programmi non erano progettati per evolvere nel tempo in modo da adattarsi alle mutate esigenze degli utenti, ma erano stati pensati come entità chiuse e di vita brevissima.

Esempio di Programmazione Strutturata.

A peggiorare le cose nel corso degli anni contribuì la migrazione dai grandi e costosissimi computer detti mainframe a calcolatori più piccoli ed economici, quindi alla portata di piccole aziende con personale non qualificato.
I mainframe erano studiati per essere utilizzati solo da personale preparato e per obbedire alla logica data-driven, ovvero il flusso elaborativo del programma segue pedissequamente il flusso dei dati.
Spesso l'elaborazione avveniva in modalità batch senza l'intervento diretto dell'operatore, il quale si limitava ad indicare al calcolatore la sequenza dei programmi da eseguire e per ciascuno di essi il file dei dati e quello dei risultati. L'interazione tra uomo e macchina era limitata alla compilazione della lista batch.
Con l'affermarsi dei piccoli computer nel mercato professionale, nacque l'esigenza di applicazioni event-driven nelle quali l'utente finale non è più costretto a creare la lista batch, bensì può dialogare con l'elaboratore mediante una più comoda ed intuitiva inferfaccia a menù.
L'applicazione all'avvio mostra una serie di opzioni disponibili, quindi attende che l'utente compia la sua scelta.
A questo punto non è più possibile prevedere il reale flusso delle elaborazioni eseguite dal computer e così il programmatore deve prestare molta attenzione a ciascuna combinazione di opzioni selezionabile dall'utente che, se particolarmente inesperto o distratto, può effettuare delle scelte del tutto illogiche, quali possono essere far elaborare dei dati prima che questi siano stati acquisiti dall'applicazione!
Ad esempio può banalmente accadere che una struttura dati non venga correttamente inizializzata prima del suo utilizzo, semplicemente perché il programmatore aveva previsto una sua elaborazione solo in circostanze particolari e magari aveva provveduto ad inzializzarla solo all'occorenza: cosa del tutto legale secondo i canoni della Programmazione Strutturata. Ma in questo modo il programma risulta inaffidabile, producendo la sporadica apparizione di bug che spesso non si riescono a riprodurre, sia perché l'utente in genere non presta molta attenzione a quello che fa, sia perché difficilmente ammetterà di aver fatto una cosa illogica durante l'utilizzo del programma.
In questo modo si finisce per inseguire il bug in porzioni di codice del tutto corrette, perdendo molto tempo prezioso mentre la reale causa è la mancata o tardiva inizializzazione di una struttura dati, o, peggio, una seconda ed imprevista reinizializzazione.

A complicare la vita alle software house ci si è messo anche il progresso tecnologico. Il passaggio dal monotasking al multitasking, dove ciascuna applicazione è divisa in thread che girano parallelamente e ciascuno concorre con gli altri alla risoluzione del problema, ha reso la situazione ancora più delicata.
Il multitasking rende il sistema più efficiente, ma anche più imprevedibile il flusso dell'elaborazione, quindi la fase di inizializzazione diviene ancora più critica! La soluzione consiste semplicemente nell'inizializzare immediatamente ciascuna struttura dati, ma questo richiede un lavoro, o meglio un attenzione maggiore da parte del programmatore che può benissimo dimenticarsi qualche inizializzazione. Sarebbe opportuno che il linguaggio obblighi il programmatore ad inizializzare la struttura dati e che il compilatore segnali tempestivamente qualunque dimenticanza. Ma questa condizione non è prevista nella Programmazione Strutturata.

Con il passare degli anni ed il comparire sulla scena di macchine sempre più potenti, il modo di programmare è mutato. Ottimizzare fino al limite dell'immaginabile il codice per ottenere buone prestazioni non era più una necessità primaria, così l'attenzione si è spostata dal codice ai dati, o meglio alla loro organizzazione.
Nasce così il modulo che altro non è che un insieme di procedure legate in qualche modo ai dati. Il programma viene suddiviso in modo che i dati siano nascosti all'interno delle varie unità-modulo. Questo modo di procedere è anche noto come "principio dei dati nascosti", e rappresenta il cardine della Programmazione Modulare.
La Programmazione Modulare non è in antitesi con la Programmazione Procedurale, bensì è un suo complemento, infatti le procedure continuano ad essere scritte secondo i dettami della Programmazione Procedurale, seguendo il più possibile lo stile della Programmazione Strutturata, specialmente se le procedure non sono particolarmente legate ai dati. Lo scheletro di un buon modulo è il seguente:

1) Fornire un'interfaccia utente per manipolare la struttura dati attorno alla quale agisce il modulo.

2) Garantire l'accesso ai campi della struttura dati solo attraverso l'interfaccia definita al punto precedente.

3) Far sì che la struttura dati sia correttamente inizializzata prima di essere usata la prima volta.

Alcuni linguaggi come ad esempio il Modula 2, sono fortemente legati a questo modo di procedere, mentre altri quali il C, semplicemente consentono di adottare tale metodologia senza obbligare il programmatore a seguirla.

Perché nascondere i dati? La prima giustificazione che può venir in mente è quella di impedire manipolazioni indesiderate delle informazioni, ma non è certamente la più importante. Un vantaggioso effetto collaterale del nasconderli consiste nel poter radicalmente cambiare la struttura dati, senza che l'utente debba cambiare il modo di accedere ai dati, ed è questo il più grande pregio della Programmazione Modulare che rende finalmente possibile la tanto auspicata possibilità di riutilizzo del codice.

Per chiarire questo punto fondamentale, ricorriamo ad un esempio. Supponiamo di scrivere un modulo per gestire una struttura dati di tipo Stack, nota anche con il nome di Last In First Out (LIFO), o (italianizzando) di PILA.
Nello Stack i dati vengono accatastati uno sull'altro e per non far crollare la pila, si può prendere solo l'elmento affiorante. L'interfaccia ci fornisce due procedure di solito dette Push() e Pop(), rispettivamente per l'inserimento ed il recupero dell'ultimo/primo dato nella struttura dati, e solo attraverso queste possiamo manipolare il contenuto dello Stack.
Abbiamo così rispettato i primi due punti dello scheletro prima enunciato. Per rispettare anche il terzo punto possiamo creare una procedura detta Init(), che provvede ad inizializzare correttamente lo Stack, posizionando lo Stack Pointer in modo che punti al primo elemento libero. Anche questa procedura farà parte dell'interfaccia e sarà compito del programmatore invocarla non appena dichiarerà una variabile di tipo Stack.
Supponiamo, che per realizzare un prototipo in breve tempo, si decida di realizzare lo Stack mediante un array. Le procedure di Init(), Push() e Pop() saranno progettare per manipolare il contenuto dell'array, ma l'utente del modulo non sa come sia stato realizzato lo Stack, pertanto si limita ad invocare le tre procedure secondo quanto riportato nella documentazione che correda il modulo.
Se in un secondo momento ci si rende conto che scegliere l'array, pur consentendo una prototipazione rapida, non è stata la scelta più felice e sarebbe meglio realizzare lo Stack con una struttura dati di tipo dinamico, ad esempio una Lista. Con il vecchio paradigma si era costretti a cambiare anche il modo di chiamare le procedure, ora è sufficiente sostituire all'array la lista e cambiare solo le procedure Init(), Push() e Pop() ed infine ricompilare il modulo.
L'interfaccia è la stessa di prima ed anche la documentazione. Quindi l'utente finale non noterà nulla di diverso, se non una maggiore flessibilità o maggiore velocità fornita dalla nuova versione della struttura dati Stack.

In altre parole, con la Programmazione Modulare tutti i dati di uno stesso tipo sono sotto il controllo di un singolo modulo, che potremmo definire gestore del tipo di dato.
Qui nasce il primo limite: volendo dichiarare due o più variabili dello stesso tipo, dobbiamo fare in modo che il gestore possa allocare dinamicamente le strutture dati, restituendo un handler univoco per ciascuna variabile di quel tipo e che il programmatore deve utilizzare per indicare al gestore a quale struttura si sta riferendo. La init() si trasforma così in una Create(), ma bisogna anche fornire una procedura per rilasciare la memoria riservata in precedenza e legata a ciascun handler quando il programmatore, esplicitamente o meno, dichiarerà di non averne più bisogno: indicheremo tale procedura con il nome di Destroy().
Sebbene i linguaggi forniscano il supporto per le strutture dati dinamiche, i compilatori, durante la traduzione, non sono più in grado di verificare se effettivamente l'elenco dei parametri passati ad una procedura durante la chiamata, o i risultati da questa restituita siano effettivamente quelli desiderati dal programmatore. Tutto ciò perché i dati definiti dall'utente non sono riconosciuti dai compilatori come i tipi predefiniti del linguaggio, per i quali esistono precise regole.
Con l'uso dei moduli si tenta di sbarrare tutte le porte agli errori e questi si affacciano alle finestre. Nulla di grave, purché il programmatore sappia ciò che sta facendo.

Nella prossima ed ultima puntata saranno analizzati pregi e difetti della Programmazione Modulare ed Orientata agli Oggetti.

Francesco De Napoli