Ciclo di sviluppo
Ora che abbiamo una idea di cosa sia la programmazione e dei suoi principali paradigmi, proviamo a capire in cosa consiste la fase di codifica effettiva di un programma per computer ovvero il ciclo di sviluppo.
Fasi di codifica ed esecuzione
L’attività di sviluppo consiste in due fasi alternate tra loro: la codifica del codice, e la sua esecuzione.
Durante la codifica il programmatore scrive il codice su un editor di testo, e nel farlo svolge una attività di tipo astratto e concettuale. Infatti definisce delle variabili, progetta un flusso del programma, fa delle ipotesi sui tipi di dati da utilizzare e restituire, ecc. Il risultato è scritto sotto forma di un codice formale fatto di istruzioni e simboli.
Non ha modo, però, mentre scrive il codice, di avere la certezza che quello che sta facendo è corretto. Fino a quando non viene eseguito il programma non si può verificarne il comportamento e quindi capire se ci sono errori nella sua progettazione e/o nello sviluppo. La scrittura di codice alla fine si riduce sempre alla scrittura di file di testo e non si ha modo di sapere se il programma funziona.
Per capirlo è necessaria l’esecuzione del programma. In questa fase l’elaboratore crea un processo di esecuzione ed il programma comincia a ricevere gli input, valorizza le variabili, esegue le istruzioni, accede alle risorse, ec. permettendo la verifica di quanto scritto in codifica.
Tuttavia non c’è modo, fino a quando si è in esecuzione, di modificare il codice mentre si è in esecuzione.
Dutante l’esecuzione il programma diventa una “scatola nera” (blackbox in inglese) che consente di vedere cosa sta facendo il computer ma non permette di intervenire immediatamente sul codice in caso di problemi. Il programmatore deve quindi prendere nota del comportamento, capire cosa va e cosa non va. Nel caso ci sia un errore deve capire cosa non ha funzionato, e poi modificare il codice per sistemare problemi o effettuare migliorie, dove però torna a lavorare in astratto e per formalismi.
Per capire bene facciamo un esempio, scriviamo un programma che richiede due numeri e calcola il risultato della divisione.
#include <iostream>
using namespace std;
int main() {
int numeratore, denominatore;
cout << "Inserisci il numeratore: ";
cin >> numeratore;
cout << "Inserisci il denominatore: ";
cin >> denominatore;
int risultato = numeratore / denominatore;
cout << "Il risultato è: " << risultato << endl;
return 0;
}Almeno in astratto, il programma sembra corretto. Il programmatore infatti
- definisce due variabili numeratore e denominatore;
- le richiede all’utente con le istruzioni corrette;
- calcola il risultato della divisione;
- stampa il risultato
Ma sarà veramente così?
In effetti alla prima esecuzione il programmatore testa con i valori 6 e 3 ed effettivamente ottiene 2. Ma si rende conto che questo programma ha una limitazione, non consente la divisione decimale, infatti 6 diviso 4 restituisce 1 e non 1,5. Quindi modifica il programma nel seguente modo:
#include <iostream>
using namespace std;
int main() {
float numeratore, denominatore;
cout << "Inserisci il numeratore: ";
cin >> numeratore;
cout << "Inserisci il denominatore: ";
cin >> denominatore;
float risultato = numeratore / denominatore;
cout << "Il risultato è: " << risultato << endl;
return 0;
}Stavolta il programma gestisce anche la divisione decimale. Ma allora funziona sempre? No. Ha dimenticato la divisione per 0, infatti in questo caso se mettesse 6 e 0, otterrebbe un errore di calcolo.
Deve necessariamente mettere un qualche tipo di controllo.
#include <iostream>
using namespace std;
int main() {
float numeratore = 0, denominatore;
cout << "Inserisci il numeratore: ";
cin >> numeratore;
while (denominatore == 0) {
cout << "Inserisci il denominatore: ";
cin >> denominatore;
}
float risultato = numeratore / denominatore;
cout << "Il risultato è: " << risultato << endl;
return 0;
}In questo caso inserisce un ciclo di controllo che consente di non avere mai errori di divisione per 0.
In un progetto reale, con tante variabili ed algoritmi complessi, possono essere molti gli errori di questo tipo, e per quanto un programmatore con l’esperienza impari a minimizzare gli errori, non ha mai la certezza in fase di codifica che tutto funzionerà correttamente. La programmazione è una operazione astratta e solo l’esecuzione garantisce che effettivamente l’idea si può oncretizzare.
La programmazione è in sostanza quindi una alternanza di fasi di codifica-esecuzione in cui lo sviluppatore scrive codice, poi esegue un test, sistema gli errori, testa di nuovo e così via in un insieme di cicli di di aggiunta e perfezionamento fino a quando non ottiene il risultato ottenuto.

Riassumendo:
A compile time il programmatore analizza e scrive il codice sulla base di modelli, algoritmi ed ipotesi. In questa fase la programmazione è un processo astratto che è solo frutto di un ragionamento del programmatore che fa uso esclusivamente di simboli e formalismi.
A runtime il codice viene eseguito e diventa un processo concreto che trasforma il codice in istruzioni effettivamente eseguite ma dove entrano in gioco due nuovi elementi:
- il contesto di esecuzione, cioè la macchina reale dove il software viene eseguito, con le sue risorse di cpu, memoria e periferiche. L’hardware di esecuzione conta perché lo stesso programma potrebbe funzionare solo con certe risorse (es. ram, connessione ad internet, ecc.).
- i dati: una applicazione per funzionare ha bisogno di dati. Come visto nell’esempio iniziale, una ipotesi sbagliata sui dati può fare credere che un programma funzioni, salvo poi scoprire che non si è scelto accuratamente di testare il software con insiemi di dati differenti, come per esempio nella divisione per 0, costringendo a riscrivere il programma.
A runtime la programmazione diventa un concetto concreto dove tutte le astrazioni le ipotesi ed i ragionamenti vengono effettivamente verificati e messi in pratica, ma dove il programmatore non ha modo di mettere mano al codice.
Debugging
Tuttavia la maggior parte dei lingugaggi di programmazione prevede una modalità “debugging”.
Il debugger è uno strumento che permette una modalità di esecuzione speciale che rimuove una parte della black box, perché consente al programmatore di fermare l’esecuzione in un punto preciso, detto breakpoint e vedere, ad esecuzione interrotta, il valore delle variabili, lo stato della memoria e lo stack. E’ poi possibile eseguire le istruzioni una alla volta e vedere come queste variabili vengono modificate dalle istruzioni. In questa modalità di debug è quindi possibile vedere nel dettaglio qual è l’istruzione “colpevole” dell’errore o se l’esecuzione entra correttamente in una condizione o un ciclo viene correttamente eseguito.
Il debug non ci consente di modificare al volo il codice (ad ogni modifica bisogna riavviare) ma almeno consente di vedere cosa succede passo passo, funzione utile nei programmi più complessi.
Build time
In realtà tra compile time e runtime è presente una fase intermedia, detta build time. In questa fase il codice realizzato viene trasformato in codice eseguibile e viene predisposta l’applicazione che sarà eseguita a runtime. Il codice, le funzioni di libreria utilizzate, le configurazioni usate e tutte le altre risorse necessarie per il funzionamento del programma vengono accorpate in un unico pacchetto pronto ad essere eseguito.
A seconda della tipologia di linguaggio, sono previste due tecniche tra loro alternative: la compilazione o l’interpretazione.
Conclusioni
Qui viene rappresentato quindi tutto il ciclo di sviluppo, che si compone quindi di un insieme di cicli di codifica, build, esecuzione e debug fino a quando il programma (o una sua parte) si può considerare corretta. A sviluppo completato si procede col rilascio (deploy), ovvero quell’insieme di attività che hanno come obiettivo l’installazione del software nella macchina dove sarà eseguito dagli utenti e poi alla realizzazione della funzionalità successiva.

Nello sviluppo di software è sempre fondamentale tenere a mente queste fasi, e cosa è possibile fare in ciascuna di esse e quali cose è possibile fare dopo.
Il programmatore deve quindi sfruttare le particolarità di ogni fase, favorendo la massima astrazione nella fase di codifica, e la massima concretezza nella fase di esecuzione. Inoltre questo tipo di scelte ovviamente condiziona anche scelte tecnologie, come la tipologia di linguaggio, come vedremo nelle prossime lezioni.
