Massimo Caliman
Massimo Caliman
11 min read

Categories

  • java

Tags

  • Java
  • Debug
  • Mokabyte

Languages

  • Italian

Pubblicato su MokaByte Numero 36 Dicembre 1999,di Massimo Caliman e Nicola Merello. Questo articolo vuole mostrare come sia possibile creare un sistema di package che faciliti il debugging di programmi scritti in Java.

Oltre ad una esprerienza concreta (e’ 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 pero’ i seguenti vincoli:

  • Il codice doveva rimanere 100% PureJava.
  • La possibilita’ di gestire piu’ 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 pero’ non si presenta il problema di eliminarle dal codice perche’ magari e’ 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 e’ molto prezioso (del resto dove non lo e’ ?). E’ da questa premessa che abbiamo cominciato a cercare una soluzione che permetta di gestire tale situazione nella maniera piu’ pulita possibile. L’idea si e’ 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. Gia’ 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 puo’ vedere dal listato 1 nel package debugger e’ definita una classe Debug analoga a quella del package nodebugger ma l’implementazione e’ diversa, anzi nel package nodebugger il membro print(String msg) e’ 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 bastera’ modificare l’import della classe con quella nodebugger.Debug e tutte le chiamate alla nostra print non verranno piu’ risolte (o meglio un certo overhead probabilmente continuera’ ad esserci in quanto la chiamata verra’ comunque eseguita, ma sara’ 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 e’ 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 piu’ 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 pero’ 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() ) 
      //do 
   } 
}

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 e’ ottimale, per cui e’ 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 piu’ possibile di creare funzioni membro statiche di Debug che restituiscano void (cioe’ niente), per ovviare al caso dell’esempio nel listato 4. Quando non si puo’ 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 e’ 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 e’ 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 permettera’ 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 potra’ invece attivare tutti i debug attualmente disattivati.

Utilizzare, poi, all’interno delle proprie classi sempre una estensione delle MyDebug in modo tale da avere piu’ 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 piu’ 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 puo’ passare all’implementazione dei vari package che formeranno il debugger. Teniamo a precisare che quella qui riportata e’ una soluzione di base nata sul campo ed e’ attualmente utilizzata in diversi programmi Java da noi sviluppati, ma nonostante tutto puo’ comunque essere ampliata e personalizzata secondo le varie esigenze. Il package principale debugger e’ suddiviso in ulteriori quattro package: assertion, debug, noassertion, nodebug. Il motivo della scelta di separare le asserzioni dal normale debug e’ 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) e’ completamente indipendente l’uno dall’altro. Passiamo ad analizzare le singole classi, premettendo pero’ che per approfondire ulteriormente si puo’ sempre far riferimento alla relativa documentazione html dell’intero package (nonche’ 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 e’ vera. La funzione prtErr(String), che tra l’altro e’ 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 e’ arrivati alla conclusione che la cosa avrebbe complicato maggiormente la gestione (soprattutto quella della cosi’ detta classe ‘fake’) ed i benefici, almeno nel nostro caso, non erano cosi’ meritevoli. Comunque se si ritiene indispensabile questa caratteristica si puo’ 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 piu’ complessa dei timer per cronometrare alcune parti del codice. Inoltre e’ 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 possibilita’ di avere un salvataggio su file di tutti i dati inviati allo standard output/error tramite una specie di log (attenzione questo non e’ da confondere con la gestione dei log, che e’ tutta un’altra cosa). La peculiarita’ di quest’ultima caratteristica e’ che il log e’ 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 attivera’ 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 quantita’ 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 puo’ 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!