|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
Un bug (letteralmente significa "insetto", "bacherozzo") è un errore o difetto nel codice di un programma software che ne causa un malfunzionamento, portando a risultati inattesi, arresti anomali o perdita di dati. Questi difetti possono derivare da errori di programmazione e/o progettazione e la loro correzione avviene tramite un processo chiamato debug, che comporta la modifica del codice sorgente e il rilascio di aggiornamenti.
In questo capitolo cercheremo di individuare e correggere i bugs della nostra piccola applicazione. Sempre in questo capitolo vi suggerirò un piccolo trucco per tracciare e documentare i bugs senza dover ricorrere a strumenti sofisticati (ma complessi) come i bug trackers, tra i più famosi dei quali possiamo annoverare, ad oggi: Bugzilla, MantisBT e Redmine
La strategia di debug per questo piccolo progetto si basa su due approcci distinti:
ConnectionTEST che simula la presenza di un client remoto ma che, in realtà, esegue tutte le operazioni in locale, nello stesso processo del server; in questo test verificheremo il protocollo ufficiale di mastermind e le situazioni eccezionali come gli errori di trasmissione, la disconnessione e il ripristino della comunicazione La classe ConnectionTest, derivata da Connection, si trova nel package mastermind.net.impl e simula la connessione remota dal lato del server. Essa riceve i messaggi di notifica inviati dal server di gioco e, anzichè inoltrarli alla macchina remota (che non esiste), provvede a creare i messaggi di risposta che il server si aspetta dalla macchina client.
I messaggi per i quali il server di gioco si aspetta una risposta sono, sostanzialmente, solo tre:
Questa classe di test introduce anche un ritardo fittizio dal momento in cui riceve il messaggio di notifica al momento in cui invia la risposta: il ritardo è pari ad un terzo del valore specificato nella proprietà di sistema guessDelay (vedi La proprietà guessDelay). Il ritardo fittizio introdotto nella connessione simula la latenza della rete.
La classe ConnectionTest definisce i membri dati privati dal momento che non si predeve di derivare ulteriormente da questa classe:
params: i parametri di gioco, necessari al codice per generare la solution e le guess: i parametri vengono notificati con il messaggio di tipo 301 (=new game) forceError: il codice di forzatura degli errori; per verificare le situazioni eccezionali è possibile forzare errori di trasmissione e perdita della connessione delay: il ritardo da introdurre nelle operazioni usato per simulare la latenza della rete serverPort: la porta del server: essendo una connessione di test, non esiste alcuna "porta del server": il dato è utilizzato per forzare una eccezione in fase di prima connessione response: il messaggio di risposta che il server di aspetta di ricevere a seguito della notifica inviata al client serverSolution: la soluzione del umano (il programmatore): la connessione di test genera le guess in modo casuale ma è possibile forzarla ad inviare una guess vincente; questo si ottiene intercettando il messaggio di tipo 202 (=have solution) e memorizzando la soluzione del programmatore in questo membro datiIl costruttore di ConnectionTest è semplice: esso si limita ad estrarre le informazioni per la connessione dalle proprietà della applicazione. Ci sono solo due dati interessanti:
guessDelay usata per introdurre il ritardo fittizio serverPort usata per forzare le eccezioni in fase di prima connessioneDovendo emulare una vera connessione di rete, la classe connessione di test avrà il compito di sovrascrivere i metodi astratti della classe da cui deriva implementando la effettiva elaborazione della connessione e dell'invio / ricezione dei messaggi.
La classe dovrà quindi sovrascrivere i metodi astratti usati dal worker-thread (vedi I metodi di I/O della connessione) e che eseguono le effettive operazioni di connessione:
doConnect: essendo una connessione di test la operazione va sempre a buon fine a meno che non sia stato forzato un errore di connessione doClose: non essendoci una vera connessione da chiudere, questo metodo non esegue alcuna operazione doSendMessage: invia il messaggio al remoto; non essendoci alcun remoto questo metodo viene elaborato in modo particolare che analizzeremo in dettaglio in Simulare la connessione dal momento che è in questo metodo che vengono elaborate le risposte attese dal server doReadMessage: questo metodo restituisce il messaggio di risposta preparato da doSendMessage isServer: ritorna sempre TRUE. getRemoteAddr: ritorna la stringa "ConnectionTEST"In una vera connessione di rete il server di gioco invia al client i messaggi di notifica e si aspetta le risposte dal remoto. Pertanto, la connessione di test non deve fare altro che creare i messaggi di risposta ed inoltrarli al server di gioco. Poichè i messaggi di notifica transitano dal metodo doSendMessage è proprio in questo metodo che avviene la elaborazione delle risposte. A seconda del tipo di messaggio di notifica inviato dal server, la connessione di test prepara il messaggio di risposta:
createEchoResponse prepara un messaggio di risposta di tipo echo-response quando viene ricevuto il messaggio di tipo 101 (=echo-request) createPresentation prepara un messaggio di presentazione con nickname uguale a "ConnTEST" e versione pari a 0.8.0; questo metodo viene richiamato quando il server invia il messaggio 103 createSolution viene richiamato quando il server invia il messaggio di notifica con codice 301 (notifyNewGame); questo metodo ritorna un messaggio di risposta contenente la solution, ottenuta in modo casuale createGuess viene richiamato quando il server invia il messaggio di notifica con codice 304 (notifySwapTurn); questo metodo ritorna un messaggio di risposta contenente la guess, ottenuta in modo casuale, ma solo se il turno di gioco attuale è quello del remoto storeSolution memorizza la solution del programmatore quando riceve il messaggio di tipo 202 (=notifyHaveSolution) setForcedError interpreta come un intero la guess inviata dal programmatore: il valore numerico della guess viene usato come un codice per forzare le eccezioniUno dei grandi vantaggi della connessione di test è che è possibile scrivere del codice per forzare il sollevamento delle eccezioni e verificare che siano gestite correttamente.
La strategia usata in questa applicazione è quella di usare le sequenze scelte dal giocatore umano (che nel caso della connessione di test è il programmatore) come codici per informare la connessione di test di sollevare una eccezione nella operazione immediatamente successiva. Vi sono diversi codici per forzare le eccezioni e dipendono dalla fase e dalla operazione eseguita:
doSendMessage doReadMessage L'elenco completo dei codici di forzatura delle eccezioni è disponibile nella documentazione della Class ConnectionTest
Quando viene scritta una applicazione client/server si deve porre particolare attenzione al protocollo applicativo ed è necessario proteggere il server: dal momento che non sappiamo chi c'è "dall'altra parte" della connessione non dobbiamo fidarci di nessuno. Il mondo è pieno di frustrati che si divertono a mettere in crisi i servers in rete ed anche la nostra applicazione, sebbene sia solo un gioco per bambini, potrebbe essere presa di mira inviando al server:
Nel package mastermind.test.tcp è presente una app GUI chiamata TCPClient che consente di inviare messaggi arbitrari che contengono qualsiasi sequenza di caratteri e di qualsiasi lunghezza. Nella immagine seguente potete vedere uno screenshot della applicazione hostile.
Non descriverò la app in dettaglio nè le prove eseguite che potete comunque trovare documentate nei link in calce. Mi limito a riepilogare i bugs trovati sia usando la connessione di test che la app ostile.
E' molto importante tracciare i bugs della applicazione. Esistono soluzioni dedicate a questo scopo sia commerciali che open source e sono normalmente integrate nelle piattaforme di sviluppo: si tratta di strumenti piuttosto complessi che gestiscono progetti medio/grandi e che consentono di gestire anche decine di sviluppatori.
Per un piccolo progetto personale l'uso di questi strumenti è davveros troppo complesso ma il problema rimane: il tracciamento dei bugs e delle soluzioni adottate è di grande importanza.
Premesso che potete organizzarvi come più vi piace, vi darò un mio personale consiglio su come tracciare i bugs in modo facile e veloce. Si tratta di creare un sorgente Java fittizio, il cui nome file è KnownBugs.java (=errori logici conosciuti) che non contiene nemmeno una riga di codice se non la dichiarazione di una classe KnownBugs. La descrizione dei bugs avviene mediante i commenti javadoc. I bugs vanno numerati o, comunque, identificati in modo univoco nell'ambito di un progetto.
Gli strumenti di bug-tracking (=tracciamento dei bugs) assegnano automaticamente un identificativo ad ogni singolo bug riscontrato o segnalato dagli utenti ma poichè non abbiamo un tale strumento, ci inventiamo noi uno schema di identificazione. L'idea è quella di assegnare al bug un identificativo composto da due parti: la prima parte è composta dal numero di versione e la seconda parte è un numero progressivo. A questo link potete vederne il risultato: Bugs versione 0.8.
I files sorgente della nuova versione sono i seguenti:
| nome file | package | descrizione |
|---|---|---|
| MasterMindServer081.java | mastermind.game | il server di gioco versione bug-fix |
| MasterMindClient081.java | mastermind.game | il client di gioco versione bug-fix |
| Main081.java | mastermind.app | la classe principale versione 0.8.1 |
Ora passiamo a commentare il codice che risolve i bugs ma prima, una raccomandazione: non cedete alla tentazione di implementare nuove funzionalità nella versione di bug-fix. Lo so come vanno queste cose: visto che sto mettendo mano ai sorgenti e devo comunque rilasciare una nuova versione tanto vale che ci aggiungo anche quella feature che Tizio, Caio e Sempronio mi avevano chiesto già qualche tempo fa!
E' una pessima idea. e non tanto perchè và contro le politiche di versioning che abbiamo stabilito all'inizio del progetto (anche per quello) ma sopratutto perchè, oltre alla funzionalità, introdurremo di sicuro anche qualche altro bug!
Per correggere i bugs riscontrati dovremmo modificare i sorgenti della applicazione e, per tenere traccia delle modifiche, creeremo una nuova versione che, in linea con Lo schema di versioning chiameremo: 0.8.1 e che sarà una versione bug-fix.
E' pur vero che non sarebbe necessario rilasciare una versione bug-fix in questo momento: considerato che fino ad oggi non abbiamo rilasciato alcuna versione, si potrebbe benissimo modificare i sorgenti della versione 0.8. Una versione bug-fix ha senso se la applicazione 0.8 è stata rilasciata ed è in giro per il mondo. In caso contrario, a chi potrebbe interessare una versione bug-fix? Ovviamente a nessuno.
Ciononostante, questa mia opera non è "una applicazione Mastermind" ma "un tutorial Java" il cui scopo è insegnare qualcosa al lettore ed ecco perchè scriveremo una versione bug-fix. Le note sulla risoluzione dei bugs vengono riportate nella documentazione javadoc della versione specifica, nella pagina del sommario: The MesterMind project - version 0.8.1.
Argomento precedente - Argomento successivo - Indice Generale