Sommario
< Home
Stampa

Ciclo di sviluppo

Lo sviluppo di una applicazione non è solo l’implementazione di un algoritmo in un linguaggio di programmazione, ma un processo che richiede più fasi e che richiede quindi di comprendere bene cosa succede durante lo sviluppo. Questo ha un impatto nel modo in cui si scrive il codice, e richiede una conoscenza anche degli strumenti di sviluppo a disposizione.

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.

Per aiutare il programmatore sono stati realizzati due strumenti di sviluppo che semplificano queste due fasi.

Build e validazione

Viene introdotta una nuova fase, detta di “build e validazione“, dove il codice viene prima validato tramite strumenti di verifica formale che controllano la sintassi e poi tradotto in codice eseguibile includendovi le librerie. La validazione ha un grande ruolo perché aiuta il programmatore sia a verificare che non ci siano errori di sintassi che i sorgenti e le librerie siano corrette, e con gli strumenti giusti, anche a verificare staticamente il codice, per esempio per condizioni che non possono verificarsi o cicli che non hanno via di uscita.

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.

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.

Compile time e runtime

Rivediamo quindi in sintesi tutto il ciclo di sviluppo:

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.

build time il codice e gli altri simboli, nonché tutte le risorse necessarie vengono accorpate in un unico pacchetto pronto ad essere eseguito. In questa fase l’ambiente di sviluppo fa uso di automatismi per verificare la correttezza formale del codice (errori di sintassi, variabili nulle, ecc.). Tuttavia il codice anche se formalmente corretto non è ancora testato, quindi potrebbero comunque esserci errori visibili solo in esecuzione, dovuti a errori concettuali o a errori di configurazione, librerie o altri elementi concreti non immediatamente rappresentabili in fase di codifica. Ad esempio potremmo cercare di aprire un file che non esiste, o cercare di usare una variabile non ancora valorizzata.

runtime il software (pacchettizzato con tutto quel che serve) viene eseguito e come detto in precedenza, si può fare debug, fermare l’esecuzione, vedere lo stato di variabili e memoria. In questo caso entrano in gioco due nuovi elementi:

  • il contesto di esecuzione (cioè la macchina reale dove il software viene eseguito, che ha specifiche risorse software ed hardware) detto anche environment. Questo elemento ha un enorme peso infatti lo stesso programma potrebbe funzionare con una certa configurazione hardware ma non in un’altra. Pensiamo per esempio ad un computer che ha poca memoria o un sistema che per funzionare deve essere connesso ad Internet.
  • 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.

Contesto e dati sono importanti. Ad esempio in determinati contesti di esecuzione e con alcuni set di dati non è possibile eseguire il debug (ad esempio per ragioni tecniche, o di sicurezza) ma occorre utilizzare altri strumenti che ci permettono di verificare la correttezza di un programma solo a posteriori. Li vedremo nel ciclo di vita del software.

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.

La scelta del linguaggio di programmazione

Il linguaggio di programmazione conta molto nel ciclo di sviluppo. Alcuni linguaggi di programmazione introducono un insieme di formalismi di programmazione come i seguenti:

  • tipizzazione “forte”: una variabile è sempre assegnata ad un tipo di dato;
  • controllo automatico di variabili con valore nullo: impediscono un errore molto comune in programmazione;
  • modificatori di accesso alle variabili: alcune variabili sono inaccessibili in alcune parti del programma, questo impedisce di scrivere dove non si dovrebbe;
  • introduzione di vincoli nel passaggio di parametri di funzioni: una funzione chiamata nel modo sbagliato viene considerata un errore;
  • contratti (interfacce) nella programmazione ad oggetti;
  • gestione automatica della memoria: non è possibile un accesso diretto alla memoria.

Questi linguaggi hanno lo scopo di limitare la libertà del programmatore e quindi il rischio di errori. Questo significa quindi che già nella fase di build molti errori vengono individuati, velocizzando di molto la fase di test a runtime. Di contro il codice è molto più verboso e richiede più righe di codice, e li rende meno adatti per programmatori alle prime armi, oppure dove non è richiesta una grande quantità di codice, oppure in quelle situazioni in cui è più efficiente avere il pieno controllo delle risorse come la memoria. Come spesso succede, una scelta tecnologica ha sempre dei pro e dei contro.

IDE

Un IDE è una applicazione realizzata per fornire al programmatore tutti gli strumenti necessari per lo sviluppo:

  • un editor di testo che permette di scrivere il codice, con funzioni di evidenziazione di variabili, parole chiave ed errori di sintassi. E’ inoltre possibile avere funzioni di autocompletamento di istruzioni e variabili.
  • un accesso rapido al compilatore o interprete tramite scorciatoie di tasti.
  • l’IDE ha un debugger integrato che consente di inserire i breakpoint e visualizzare le variabili.
  • permette di progettare interfacce grafiche e visuali.
  • consente per alcune tecnologie di realizzare già uno schema del progetto senza scrivere il codice iniziale.
  • ha tutti gli strumenti necessari per il deploy.

I due IDE più diffusi sono IntelliJ Idea (pensato in particolare per le applicazioni Java o linguaggi di quella famiglia, ma supporta anche Javascript, PHP e Python) e Visual Studio Code (pensato per Javascript, ma che supporta quasi tutti i linguaggi di programmazione tramite plugin).

Altri IDE particolarmente diffusi ma specifici per alcune tecnologie sono Visual Studio (progettato per applicazioni .NET su Windows), XCode (IDE per realizzare applicazioni per dispositivi Apple), Android Studio (variante di IntelliJ per il solo sviluppo per Android) e JavaBeans (per sole applicazioni Java).

Sono presenti anche molti strumenti di sviluppo basati su Web, come Colab Notebooks (per Python, non è un vero e proprio IDE ma consente di creare progetti e testare programmi) e Github codespaces (basato su VS Code).

Sebbene sia possibile scrivere programmi senza un IDE ma usando direttamente gli strumenti di build e deploy, è consigliabile farlo per essere più facilmente produttivi.