MokaByte Numero  36  - Dicembre 99
Un package per il 
debugging in Java
di 
Max Caliman 
Nik Merello
Il debug e le asserzioni


Questo articolo vuole mostrare come sià possibile creare un sistema di package che faciliti il debugging di programmi scritti in Java. Oltre ad una esprerienza concreta (è disponibile il nostro package completo di tutti i sorgenti nonche' i relativi file di documentazione in html generati con javadoc ) vuole essere un punto di partenza per realizzare qualcosa di personalizzato,tagliato su misura

Premessa

Venendo da un’esperienza di C/C++ una delle prime cose di cui abbiamo sentito la mancanza in Java sono state le asserzioni: comode, pratiche e… quando non ti servono te ne puoi sempre sbarazzare inserendo negli appositi header delle direttive del precompilatore. Il nostro scopo era quello di riuscire a creare una situazione analoga anche in Java rispettando però i seguenti vincoli:

  • Il codice doveva rimanere 100% PureJava.
  • La possibilità di gestire più livelli di debug in modo semplice e diretto, in modo da poterli utilizzare in 

  • maniera autonoma gli uni dagli altri.

  • Poter disabilitare/abilitare il debugger senza effettuare onerose ricerche all’interno del nostro codice 

  • per commentare tutte le chiamate di debug.


Il problema
A Java non manca niente per poter creare (come del resto avevamo fatto nel C++) delle classi Assert che possano ricoprire in modo abbastanza completo tale ruolo, fino a che però non si presenta il problema di eliminarle dal codice perché magari è stato ritenuto maturo e pronto per essere rilasciato come release finale (su questo argomento ci sarebbe da aprire un acceso dibattito, ma lasciamo stare). In questi casi o si abbandona lo standard e ci si appoggia a qualche precompilatore che svolga lo stesso ruolo come nel C (metodo da noi subito scartato, in quanto non rispettava le linee di progetto che ci eravamo imposti), oppure ci si arma di sana pazienza e si esegue una ricerca di tutte le chiamate alla classe Assert rimovendole o meglio commentandole.Se poi mettiamo il caso che sia necessario rimettere mano al nostro codice per effettuare delle modifiche (cosa del tutto normale e frequente), ecco che dobbiamo nuovamente andare a inserire le asserzioni eliminate precedentemente (se ci va bene dobbiamo solo de-commentarle). Tutto questo costa fatica, ma soprattutto tempo, e si sa che nello sviluppo di software il tempo è molto prezioso (del resto dove non lo è ?). 
E’ da questa premessa che abbiamo cominciato a cercare una soluzione che permetta di gestire tale 
situazione nella maniera più pulita possibile.l’idea si è basata su una tecnica accennata da Bruce Eckel nel suo libro Thinking in Java ( disponibile gratuitamente al sito http://www.BruceEckel.com), precisamente nel capitolo 5. 

 

Un primo approccio
In pratica il gioco ruota attorno a due package simmetrici (nella definizione ma non nell’implementazione) che all’interno definiscono le stesse classi con gli stessi metodi. Per motivi prestazionali tutti i metodi sono definiti static in maniera tale che le chiamate siano eseguite tramite la classe e non un’istanza di questa.Vediamo un esempio per chiarire meglio il concetto. Definiamo due package, uno lo chiamiamo debugger e l’altro nodebugger. Già dai due nomi si dovrebbe capire quali siano le loro intenzioni:

LISTATO 1:
package debugger;
public class Debug {
[...]
public final static void print(String msg) {
 System.out.println(msg);
}
[...]
}
LISTATO 2:
package nodebugger;
public class Debug {
[...]
public final static void print(String msg) {
/*do nothing*/
}
[...]
}
Come  si può vedere  dal  listato 1 nel package debugger è definita una classe Debug analoga a quella del package nodebugger ma l’implementazione è diversa, anzi nel package nodebugger il membro print(String msg) è addirittura vuoto. Questo ci permette di utilizzare la classe debugger.Debug nel nostro codice sorgente richiamandone il metodo print qualora ci serva per stampare messaggi di debug, successivamente per disabilitare tale opzione basterà modificare l’import della classe con quella nodebugger.Debug e tutte le chiamate alla nostra print non verranno più risolte (o meglio un certo overhead probabilmente continuerà ad esserci in quanto la chiamata verrà comunque eseguita, ma sarà sicuramente molto inferiore rispetto al tempo di esecuzione della stampa sullo standard output che faceva la precedente funzione print. In pratica rimane solo la chiamata nello stack, che forse un compilatore di bytecode intelligente potrebbe eliminare. In ogni caso questo overhead è nella maggior parte dei casi sicuramente trascurabile). 
Logico che poi le varie funzioni di debug non si fermano solo alla semplice stampa di un messaggio sullo standard output/error ma ve ne saranno altre per molteplici scopi, dalle semplici funzioni di output di valori fino ad arrivare a funzioni più complesse per la gestione di timer, asserzioni, stack-trace, ecc. 
La regola e’ che per una funzione implementata nella classe debugger.Debug ci sia una definizione (ma non l’implementazione) anche nella classe nodebugger.Debug in modo da poter interscambiare tali classi. Ci sono però alcune eccezioni, infatti davanti a un membro statico del tipo:
 
LISTATO 3:
package debugger;
public class Debug {
[…]
public static long getFreeMemory() {
Runtime rt = Runtime.getRuntime();
return rt.freeMemory();
}
[…]
}


che viene utilizzato nel nostro codice sorgente in questo modo:

LISTATO 4:
package myprogram;
import debugger.debug.Debug;
public class MyClass {
[…]
public void myMember() {
[…]
if ( allocSize > Debug.getFreeMemory() ) […]
[…]
}
[…]
}
viene spontaneo chiedersi cosa succederebbe se si scambiasse la classe del package debugger con quella del nodebugger che magari non implementa tale funzione, ma restituisce un valore di default pari a zero o ancora peggio restituisce un puntatore null nel caso il valore di ritorno sia un oggetto.Sicuramente la soluzione presentata in questo caso non è ottimale, per cui è necessario imporsi delle limitazioni progettuali:
  • Non produrre mai delle situazioni di side-effect all’interno delle funzioni membro della classe Debug, in quanto queste potrebbero creare brutte sorprese quando viene a mancare l’implementazione che le produce durante lo scambio dei due package.
  • Cercare il più possibile di creare funzioni membro statiche di Debug che restituiscano void (cioè niente), per ovviare al caso dell’esempio nel listato 4.
  • Quando non si può rispettare la seconda raccomandazione riportare, oltre che alla definizione, anche la stessa implementazione del metodo della classe del package debugger nel package nodebugger (anche se purtroppo in questo caso non si ha alcun beneficio dal lato delle prestazioni): in questo modo si è sicuri che anche scambiando i package la nostra funzione getFreeMemory del precedente esempio si comporti sempre nello stesso modo.
Seguendo queste raccomandazioni si dovrebbe riuscire a ovviare ai problemi derivanti dal side-effect (che comunque rimane sempre una tecnica per la quale è sempre meglio valutare soluzioni alternative). 
 
 

Affiniamo la tecnica
Per perfezionare ancora meglio quanto detto sopra, ma soprattutto per capitalizzare meglio il codice che andremo a scrivere si potrebbero definire altre due raccomandazioni: 
Prima di utilizzare le classi di debug, definire un’ulteriore classe di supporto che semplicemente eredita dalla classe Debug originale:

LISTATO 5:
package myprogram.debug;
public class MyDebug extends debugger.debug.Debug {/**/}
Analogamente fare la stessa cosa per la classe Debug del package nodebug:
LISTATO 6:
package myprogram.nodebug;
public class MyDebug extends debugger.nodebug.Debug { /**/}


Questo ci permetterà di disabilitare tutti i debug della nostra applicazione semplicemente cambiando la classe genitore della myprogram.debug.MyDebug con la classe debugger.nodebug.Debug.Stessa cosa vale per la classe myprogram.nodebug.MyDebug che potrà invece attivare tutti i debug attualmente disattivati.

Utilizzare, poi, all’interno delle proprie classi sempre una estensione delle MyDebug in modo tale da avere 
più livelli di debug indipendenti (o quasi) l’uno dall’altro. In questo modo si potranno attivare dei livelli piuttosto che altri ed avere un output di debug più chiaro da capire. Per esempio:

LISTATO 7:
package myprogram.main;
//Definisco dei livelli di debug
class DebugLevel1 extends myprogram.debug.MyDebug { /**/ }
//attualmente attivo
class DebugLevel2 extends myprogram.nodebug.MyDebug { **/}
//attualmente disattivo
public class MyClass {
MyClass() {}
public void myMember1() {
DebugLevel1.prt(“Precondizione della funzione 
membro myMember1():”+data);
[…]
}
public void myMember2() {
DebugLevel2.prt(“Precondizione della
funzione membro myMember2(): ”+data);
[…]
}
[…]
}


Finalmente il package debugger
Mantenendo le linee di progetto fin qui definite si può passare all’implementazione dei vari package che formeranno il Debugger. Teniamo a precisare che quella qui riportata è una soluzione di base nata sul campo ed è attualmente utilizzata in diversi programmi Java da noi sviluppati, ma nonostante tutto può comunque essere ampliata e personalizzata secondo le varie esigenze. 
Il package principale debugger è suddiviso in ulteriori quattro package: assertion, debug, noassertion, nodebug. Il motivo della scelta di separare le asserzioni dal normale debug è dovuto soprattutto al peso che noi diamo all’interno del nostro codice a queste. Infatti molto spesso ci capita di voler disabilitare ogni stampa/informazione di debug ma lasciare comunque attive le chiamate alla classe Assert, magari anche nel codice finale che viene rilasciato. Questa scelta ci facilita le cose in quanto la gestione dei quattro package (simmetrici due a due) è completamente indipendente l’uno dall’altro. 
Passiamo ad analizzare le singole classi, premettendo però che per approfondire ulteriormente si può sempre far riferimento alla relativa documentazione html dell’intero package (nonché ai sorgenti java). 

 

Le asserzioni
All’interno del percorso debugger.assertion e debugger.noassertion troviamo le due classi ‘gemelle’ Assert, uguali nella loro definizione ma non nella loro implementazione. Infatti la classe appartenente al package debugger.noassertion non implementa nessuna delle cinque funzione membro. (Queste classi, proprio per il loro ruolo all’interno del package, vengono spesso definite da noi come classi ‘fake’).I metodi principali di Assert sono isTrue(boolean) / isTrue(boolean, String) che lanciano un’assert-fail (in pratica eseguono il metodo prtErr) nel qual caso l’espressione booleana non sia vera. Il parametro stringa della seconda funzione serve solo come label per riuscire ad identificarla meglio in caso questa fallisca. I corrispettivi metodi isFalse(boolean) / isFalse(boolean, String) funzionano analogamente, ma al contrario dei precedenti, lanciano l’assert-fail se l’espressione booleana è vera.La funzione prtErr(String), che tra l’altro è definita private, si occupa di inviare sullo standard error i dati dell’asserzione fallita e una stampa dello stak-trace per riuscire ad identificare meglio dove questa e’ stata lanciata. Inoltre per avvisare ulteriormente del fallimento viene inviato un segnale acustico allo speaker del PC.Si e’ discusso molto sul fatto che i metodi isTrue e isFalse dovessero o meno lanciare un’eccezione in modo da obbligare il relativo catch ed avere una gestione al livello superiore dell’asserzione fallita. Alla fine si è arrivati alla conclusione che la cosa avrebbe complicato maggiormente la gestione (soprattutto quella della così detta classe ‘fake’) ed i benefici, almeno nel nostro caso, non erano così meritevoli. Comunque se si ritiene indispensabile questa caratteristica si può sempre aggiungere ulteriori metodi che la implementino. 

 

Il Debug
I restanti due package debugger.debug e debugger.nodebug contengono rispettivamente la classe Debug. I metodi implementati sono di vario tipo a seconda della gestione che si vuole fare: si va dalla classica formattazione dei dati da inviare nello standard output (si vedano i vari metodi prt e prtValue) alla gestione più complessa dei timer per cronometrare alcune parti del codice. Inoltre è stato inserito un metodo isEnable() che ritorna true nel caso della classe debugger.debug.Debug e false nel caso della corrispettiva classe ‘fake’ debugger.nodebug.Debug, in modo da poter conoscere se l’attuale classe statica a cui si fa riferimento ha il debugger attivo oppure no.

E’ stata aggiunta anche la possibilità di avere un salvataggio su file di tutti i dati inviati allo standard output/error tramite una specie di log (attenzione questo non è da confondere con la gestione dei log, che è tutta un’altra cosa). La peculiarità di quest’ultima caratteristica è che il log è attivo/disattivo allo stesso momento per tutte le classi che ereditano da debugger.debug.Debug. Questo vuol dire che se io ho due classi DebugLevel1 e DebugLevel2 che entrambi derivano da tale classe (debug attivo per tutti e due i livelli) e attivo il log sul DebugLevel1 lo attiverò indirettamente anche su il DebugLevel2.
Per finire ci sono anche alcuni metodi che ritornano un valore (per esempio getFreeMemory() e getTotalMemory(), che servono rispettivamente per avere la quantità di memoria libera e totale disponibile per il processo corrente). Questi, per il motivo citato nella raccomandazione n. 3, sono stati mantenuti uguali anche nella relativa classe ‘fake’.

L'utilizzo
Vediamo un esempio di utilizzo del package debugger:

LISTATO 8:
package myprogram.nodebug;
public class MyDebug extends debugger.nodebug.Debug {
/*Inserire qui eventuali estensioni */
}
LISTATO 9:
package myprogram.debug;
public class MyDebug extends debugger.debug.Debug {
/*Inserire qui eventuali estensioni */
}
LISTATO 10:
package myprogram.noassertion;
public class MyAssert extends debugger.noassertion.Assert {
/*Inserire qui eventuali estensioni */
}
LISTATO 11:
package myprogram.assertion;
public class MyAssert extends debugger.assertion.Assert {
/*Inserire qui eventuali estensioni */
}
LISTATO 12:
package myprogram.main;
//Definisco dei livelli di debug
class DebugLevel1 extends myprogram.debug.MyDebug { /**/}
//attualmente attivo
class DebugLevel2 extends myprogram.nodebug.MyDebug { /**/}
//attualmente disattivo
public class MyClass {
//Definisco dei livelli di asserzioni
class AssertLevel1 extends myprogram.assertion.MyAssert { /**/ }
//attualmente attivo
class AssertLevel2 extends myprogram.noassertion.MyAssert { /**/ }
private int data = 0;
MyClass() {}
public void myMember1() {
AssertLevel1.isTrue(data>0,
“Precondizione della funzione membro myMember1()”);
int val = 0;
[…]
DebugLevel1.prtValue(“val”,val);
[…]
DebugLevel1.startTimer(“MyTimer1”);
[…]
DebugLevel1.prtTimer(“MyTimer1”);

[…]
DebugLevel1.stopTimer(“MyTimer1”);
[…]
}
public void myMember2() {
AssertLevel2.isTrue(data>0,
 “Precondizione della funzione membro myMember2()”);
[…]
DebugLevel2.prt(“Apertura del database…”);
[…]
}
[…]
}

Conclusione
Come si può notare con questo sistema siamo riusciti ad ottenere un debugger che risponde alle caratteristiche che ci eravamo prefissati ed ha un utilizzo abbastanza semplice ma soprattutto immediato. Sicuramente le migliorie che si possono fare sono moltissime, soprattutto la personalizzazione e adattamento alle proprie abitudini durante la stesura del codice. Per fare questo basta ampliare le classi che ereditano da quelle base del package debugger mantenendo sempre come riferimento le linee guida elencate poc’anzi.Per eventuali chiarimenti, critiche e consigli contattateci direttamente via e-mail. BUON DEBUG!

 

Nicola Merello , Analista - programmatore in C/C++ e Java. Ha sviluppato diversi programmi ambiente Win 32 che vanno da semplici gestionali, a programmi per il pronostico di giochi Totocalcio / Totogol mediante l’implementazione di reti neurali. Attualmente lavora come consulente Java , presso lo Studio Balbi s.u.r.l. di Genova, su un progetto per la gestione presenze ,accessi controllati, legate all’utilizzo di rilevatori per Badge. Può essere tramite l’indirizzo nikit@libero.it .

Massimo Caliman,Analista - programmatore in Java – E’ Diplomato in elettronica industriale è iscritto attualmente al corso di diploma di laurea in Informatica presso il Dipartimento di Informatica e Scienze dell' Informazione di Genova , lavora come Analista programmatore presso lo Studio Balbi di Genova, un IBM Business Partner che si occupa di software ed hardware per la rilevazione presenze e di campo. Membro della Java Italian Association è attualmente impegnato nello sviluppo di applicazioni scritte in Java per la gestione delle presenze e produzione . Puo’ essere contattato via email all’ indirizzo calimanm@tin.it

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it