1. Il lavoro di un programmatore

Molto spesso i programmatori alle prime armi pensano al lavoro di un programmatore in modo completamente diverso da come lo pensano i programmatori esperti .

I principianti spesso dicono qualcosa come "Il programma funziona, cos'altro ti serve?" Un programmatore esperto sa che "funziona correttamente" è solo uno dei requisiti per un programma , e non è nemmeno la cosa più importante !

Leggibilità del codice

La cosa più importante è che il codice del programma sia comprensibile ad altri programmatori . Questo è più importante di un programma che funzioni correttamente. Molto di piu.

Se hai un programma che non funziona correttamente, puoi risolverlo. Ma se hai un programma il cui codice è incomprensibile, non puoi farci niente.

Basta prendere qualsiasi programma compilato, come il blocco note, e cambiarne il colore di sfondo in rosso. Hai un programma funzionante, ma non hai un codice sorgente comprensibile: è impossibile apportare modifiche a un programma del genere.

Un esempio da manuale è quando gli sviluppatori Microsoft hanno rimosso il gioco Pinball da Windows perché non potevano portarlo su un'architettura a 64 bit. E avevano persino il suo codice sorgente. Semplicemente non riuscivano a capire come funzionava il codice .

Contabilità per ogni caso d'uso

Il secondo requisito più importante per un programma è tenere conto di ogni scenario. Spesso le cose sono un po' più complicate di quanto sembri.

Come un programmatore alle prime armi vede l'invio di messaggi SMS:

Un programma correttamente funzionante

Come lo vede un programmatore professionista:

Un programma correttamente funzionante

Lo scenario "funziona correttamente" di solito è solo uno dei tanti possibili. Ed è per questo che molti neofiti si lamentano del validatore di attività di CodeGym: solo uno scenario su 10 funziona e il programmatore principiante pensa che sia sufficiente.


2. Situazioni anomale

Situazioni anomale

Possono verificarsi situazioni anomale nell'esecuzione di qualsiasi programma.

Ad esempio, decidi di salvare un file ma non c'è spazio su disco. Oppure il programma sta tentando di scrivere dati nella memoria, ma la memoria disponibile è bassa. Oppure scarichi un'immagine da Internet, ma la connessione si interrompe durante il processo di download.

Per ogni situazione anomala, il programmatore (l'autore del programma) deve a) anticiparla , b) decidere esattamente come il programma dovrebbe gestirla e c) scrivere una soluzione che sia il più vicino possibile a quella desiderata.

Ecco perché i programmi hanno avuto un comportamento molto semplice per molto tempo: se si verificava un errore nel programma, il programma terminava. E questo è stato un approccio piuttosto buono.

Supponiamo che tu voglia salvare un documento su disco, durante il processo di salvataggio scopri che non c'è abbastanza spazio su disco. Quale comportamento vorresti di più:

  • Il programma termina
  • Il programma continua a essere eseguito, ma non salva il file.

Un programmatore alle prime armi potrebbe pensare che la seconda opzione sia migliore, perché il programma è ancora in esecuzione. Ma in realtà non è così.

Immagina di aver digitato un documento in Word per 3 ore, ma due minuti dopo l'inizio del processo di scrittura è diventato chiaro che il programma non sarebbe stato in grado di salvare il documento su disco. È meglio perdere due minuti di lavoro o tre ore?

Se il programma non può fare ciò di cui ha bisogno, è meglio lasciarlo chiudere piuttosto che continuare a fingere che tutto vada bene. La cosa migliore che un programma può fare quando incontra un errore che non può risolvere da solo è segnalare immediatamente il problema all'utente.


3. Background sulle eccezioni

I programmi non sono gli unici ad affrontare situazioni anomale. Si verificano anche all'interno dei programmi, nei metodi. Per esempio:

  • Un metodo vuole scrivere un file su disco, ma non c'è spazio.
  • Un metodo vuole chiamare una funzione su una variabile, ma la variabile è uguale a null.
  • La divisione per 0 avviene in un metodo.

In questo caso, il metodo chiamante potrebbe eventualmente correggere la situazione (eseguire uno scenario alternativo) se sa che tipo di problema si è verificato nel metodo chiamato.

Se stiamo cercando di salvare un file su disco e tale file esiste già, possiamo semplicemente chiedere all'utente di confermare che dobbiamo sovrascrivere il file. Se non c'è spazio su disco disponibile, possiamo visualizzare un messaggio per l'utente e chiedere all'utente di selezionare un disco diverso. Ma se il programma esaurisce la memoria, andrà in crash.

C'era una volta, i programmatori hanno riflettuto su questa domanda e hanno trovato la seguente soluzione: tutti i metodi/funzioni devono restituire un codice di errore che indica il risultato della loro esecuzione. Se una funzione ha funzionato perfettamente, ha restituito 0 . In caso contrario, ha restituito un codice di errore (non zero).

Con questo approccio agli errori, dopo quasi ogni chiamata di funzione, i programmatori dovevano aggiungere un controllo per vedere se la funzione terminava con un errore. Il codice si è gonfiato di dimensioni ed è arrivato ad assomigliare a questo:

Codice senza gestione degli errori Codice con gestione degli errori
File file = new File("ca:\\note.txt");
file.writeLine("Text");
file.close();
File file = new File("ca:\\note.txt");
int status = file.writeLine("Text");
if (status == 1)
{
   ...
}
else if (status == 2)
{
   ...
}
status = file.close();
if (status == 3)
{
   ...
}

Inoltre, molto spesso una funzione che ha scoperto che si è verificato un errore non sapeva cosa farsene: il chiamante doveva restituire l'errore e il chiamante del chiamante lo ha restituito al suo chiamante e così via.

In un programma di grandi dimensioni, una catena di dozzine di chiamate di funzione è la norma: a volte puoi persino trovare una profondità di chiamata di centinaia di funzioni. E ora devi passare il codice di errore dal basso verso l'alto. E se da qualche parte lungo il percorso qualche funzione non gestisce il codice di uscita, l'errore andrà perso.

Un altro svantaggio di questo approccio è che se le funzioni restituiscono un codice di errore, non possono più restituire i risultati del proprio lavoro. Il risultato dei calcoli doveva essere passato tramite parametri di riferimento. Ciò rendeva il codice ancora più macchinoso e aumentava ulteriormente il numero di errori.