MokaByte Numero 34  - Ottobre  99
Librerie in Java
II parte, un esempio
di
Antonio Cisternino
In questo articolo mostrerò con un esempio come realizzare un repository di codice in Java utilizzando gli strumenti illustrati nell’articolo precedente
In collaborazione con Dev


L’articolo precedente ha introdotto gli strumenti offerti dal linguaggio Java per documentare e riutilizzare il proprio codice. In questa seconda ed ultima puntata scriveremo una piccola libreria di esempio capire in concreto quali spunti possano, con poco sforzo, portare grandi benefici nell’economia dello sviluppo del software in Java.La gestione presentata non richiede altro che il java development kit. L’uso di strumenti per gestire i progetti non impedisce di impiegare le stesse tecniche qui presentate, si tratterà solo di capire come un ambiente di sviluppo consenta di organizzare il proprio codice secondo lo schema presentato.


Il problema

Per sviluppare una libreria innanzitutto si deve avere chiaro in mente il problema che si vuole risolvere. Se non si vuole sviluppare middleware, spesso la libreria è un sottoprodotto di un software che si scrive per altre ragioni; in questo caso il problema non è lo sviluppo della libreria ma quello di un software. Spesso però mentre si sviluppa il software ci si rende conto che una sua parte risolve un problema sufficientemente generico, e per tale ragione tale porzione debba essere sviluppata come libreria.Ecco quindi che si ha il problema di disegnare un modulo software che offra delle interfacce sufficientemente generiche da essere riutilizzate il maggior numero di volte possibile. Successivamente può capitare che ci si accorga che con poco sforzo si può risolvere un numero di problemi maggiore, magari offrendo una chiamata in più o aggiungendo un parametro in più. Quello che si deve valutare è lo sforzo necessario alla generalizzazione: se si dovesse rivelare troppo grande allora è meglio produrre una libreria più mirata.Capita spesso inoltre che a generalizzare troppo non si risolva più il problema poiché sono troppi i parametri richiesti e la complessità richiesta nell’uso della libreria è equivalente alla riscrittura del modulo stesso.Quando si sviluppa una libreria si ha un ben preciso problema: definire l’interfaccia e l’implementazione di un modulo per uno sviluppatore che lo integrerà nel proprio software. Una volta compresa la natura del problema in generale può capitare di dover scegliere il linguaggio di programmazione e le tecnologie in gioco, ma non è il nostro caso poiché stiamo parlando di librerie scritte in Java.Nel caso in cui una libreria nasca durante lo sviluppo di un software, accade altrettanto spesso che sia in realtà un pezzo di codice già scritto che viene confinato in un modulo chiamato libreria. In questo caso spesso non sarà possibile riutilizzare quel codice poiché le interfacce che offrirà la nuova libreria così creata saranno tipicamente molto vincolate al software stesso, riducendo quindi l’applicabilità in altri ambiti.Il problema che affronterò in questo articolo è quello di scrivere una libreria che astragga il concetto di canale di log. Quante volte capita di dover generare messaggi che descrivono l’andamento dell’esecuzione del programma? Accade veramente spesso anche se il mezzo di output è spesso e volentieri differente. Inoltre i programmi in genere permettono di specificare il grado di dettaglio del log da generare etichettando in qualche modo i messaggi offerti e selezionando in base alle etichette abilitate.In questo articolo verrà presentato lo sviluppo di una libreria che gestisca i canali di log consentendo di essere estesa facilmente per aggiungere nuove tipologie di canale. Il risultato sarà una piccola libreria contenuta nel package it.mokabyte.logmanager che permette di includere un supporto non banale della gestione del log nelle proprie applicazioni.

 

Il LogManager

Quando mi sono trovato ad affrontare il problema del log mi sono posto i seguenti quesiti: dove viene inviato il messaggio? Come è possibile consentire la selezione dell’output?Queste domande, anche se in apparenza banali, hanno originato un’interfaccia Java che io ho chiamato LogManager che astrae il concetto di canale di log. I metodi offerti da questa interfaccia riflettono la mia volontà di dare una risposta alle precedenti domande in modo parametrico. I metodi offerti dall’interfaccia sono 
 

public void addLog(int type, String msg); 
public void setClassLog(int type, boolean set); 
public boolean isSelective();
 

Si veda il Listato 1 per la definizione completa:
Listato 1 L’interfaccia LogManager
package it.mokabyte.logmanager;

/**
 * <b>File:</b> LogManager.java<br>
 * <b>Created:</b> Sat Apr 03 10:40:52 1999<br>
 * <b>Last modified:</b> <br> 
 * <p>
 * Interfaccia astratta che astrae il canale di Log.
 * Vengono definite otto classi per classificare i messaggi
 * di log. Le classi sono CLASS[1] == 0x01, ..., CLASS[8] == 0x80.
 * Questa definizione serve ad agevolare la rappresentazione in
 * un intero mediante operatori binari dello stato.
 * La classificazione del log &egrave; opzionale.
 * <b>CHANGES:</b>
 * <ul>
 * </ul>
 *
 * @author Antonio Cisternino
 * @version
 */
public interface LogManager  { 
  /** Classi di Log. */
  public final static int[] CLASS = { 
    0x00, 0x01, 0x02, 0x04, 0x08, 
    0x10, 0x20, 0x40, 0x80, 0xff
  };

  /** Indica nessuna classe in output */
  public final static int NONE = 0;

  /** Indica tutte le classi in output */
  public final static int ALL  = 9;

  /**
   * Aggiunge un messaggio all'output (se non filtrato).
   * @param type Classe a cui appartiene il messaggio.
   * @param msg  Messaggio da visualizzare.
   */
  public void addLog(int type, String msg);

  /**
   * Seleziona la classe di log attiva. Chi implementa la classe gestore
   * pu&ograve; non implementare il filtro del log anche se &egrave; 
   * auspicabile che lo faccia. 
   * @param type Classe di log. Pu&ograve; essere <code>NONE</code>,
   *             <code>ALL</code> o qualsiasi intero compreso tra 1 e 8.
   * @param set  Se <code>true</code> l'output per la classe &egrave;
   *             attivato, disattivato altrimenti.
   */
  public void setClassLog(int type, boolean set);

  /**
   * Dice se la classe che implementa l'interfaccia supporta il log selettivo.
   * @return Restituisce <code>true</code> se il log selettivo &egrave;
   *         supportato, <code>false</code> altrimenti.
   */
  public boolean isSelective();
} // LogManager


 

Il metodo fondamentale è il primo che consente di aggiungere al canale di log un messaggio di un dato tipo. Il tipo di un messaggio è un’informazione che classifica i messaggi e che successivamente permetterà un opportuno filtro per classi di messaggi.Il metodo setClassLog è quello che risponde all’esigenza di filtrare: permette infatti di abilitare o disabilitare tutti i messaggi etichettati con un certo tipo.Infine il metodo isSelective permette di sapere se l’implementazione dell’interfaccia supporta il log selettivo o meno. L’esistenza di questo metodo è dovuta alla volontà di rendere disponibile l’interfaccia anche per gli implementatori che vogliono solo astrarre il canale ma non offrire alcun tipo di filtro.L’interfaccia definisce anche le classi di log, ovvero le etichette utilizzabili per caratterizzare il log: sono otto interi scelti in modo da avere un solo bit a 1 per poi poter utilizzare gli operatori booleani per abilitare più classi con un solo comando. Ad esempio il comando:
 

setClassLog(CLASS[1] & CLASS[3], true)


abiliterà le due classi di log 1 e 3.Osservare come il disegno dell’interfaccia contenga già al suo interno alcune scelte che in qualche modo limitano il numero di problemi risolti: il numero di classi è limitato ad otto; l’etichetta del log è un intero potenza di due in modo da consentire l’uso degli operatori bit a bit.Entrambe le scelte sembrano ragionevoli anche se la prima è la più vincolante (anche se con poco sforzo si può estendere a 31 classi). Il vettore CLASS definito nell’interfaccia definisce le classi con indici da 1 a 8.L’interfaccia appartiene al package it.mokabyte.logmanager e lo si vede dalla prima riga del sorgente.La libreria fornisce poi alcune implementazioni utili dell’interfaccia che risolvono problemi tipici del log in modo da offrire la soluzione per alcune istanze del problema particolarmente diffuse. Il disegno della libreria però consente una facile estensione per definire nuove classi di canali quali ad esempio la rete o una finestra invece che la console.

 

Il GenericLogManager

Nel paragrafo precedente ho mostrato l’interfaccia attorno a cui si costruisce il concetto di canale di log; una libreria però normalmente deve anche offrire qualcosa in più di un’interfaccia. Tipicamente deve offrire l’implementazione dell’interfaccia per casi utili come ad esempio l’output del log su console.Nel nostro caso sarebbe anche utile fornire una gestione di default del sistema di filtro in modo tale che l’unica cosa che resta successivamente da implementare è la stampa dei messaggi di log sul canale appropriato. Ecco quindi che ho deciso di definire una classe base astratta che fornisca già una possibile implemetazione del meccanismo di filtro dei messaggi e che chiami un metodo print che stampa un messaggio sul canale. Ecco quindi che per ottenere un canale nuovo è sufficiente derivare una classe che implementa il metodo in questione per ottenere un nuovo gestore del canale di log.Il ConsoleLogManager è definito come segue:
 

public class ConsoleLogManager extends GenericLogManager {
public ConsoleLogManager() {
super();
}
public void print(String str) {
System.out.println(str);
}
}


e permette di definire in modo semplice un canale di log con filtro che usi lo standard output per stampare i messaggi. Se txt fosse stata una casella di testo allora il corpo del metodo print:
 

txt.appendText(str + "\n");


avrebbe dato un canale di log per una finestra.La definizione della classe GenericLogManager è riportata nel Listato 2

 

Listato 2 La classe GenericLogManager
package it.mokabyte.logmanager;

// Java packages
import java.util.Date;
import java.text.SimpleDateFormat;

/**
 * <b>File:</b> GenericLogManager.java<br>
 * <b>Created:</b> Sat Apr 03 13:48:28 1999<br>
 * <b>Last modified:</b> <br> 
 * <p>
 * Classe base astratta per l'implementazione di un log manager.
 * Per definire un log manager &egrave; sufficiente implementare
 * la funzione di stampa. Questa classe implementa il filtro del
 * log ed in generale &egrave; sufficiente per gestire i Log senza
 * scrivere classi che implementano l'interfaccia <code>LogManager</code>.
 * <P>
 * <b>CHANGES:</b>
 * <ul>
 * </ul>
 * @see LogManager
 * @author Antonio Cisternino
 * @version 1.0
 */
public abstract class GenericLogManager implements LogManager {
  /** Flag contenente i filtri attivi come bit a 1 di un intero */
  private int flags;

  /**
   * Flag che dice se visualizzare o meno
   * data e ora.<br>
   * <b>default:</b> <code>false</code>;
   */
  private boolean dateTime = false;

  /**
   * Costruttore. Inizializza i filtri tutti a 1.
   */
  public GenericLogManager() {
    flags = LogManager.CLASS[LogManager.ALL];
  }

  /**
   * Definire questa funzione per avere un log manager
   * generico. 
   * @param str Stringa da stampare.
   */
  protected abstract void print(String str);

  /**
   * Aggiunge un messaggio al log.
   * @param type Livello di log.
   * @param msg Messaggio da visualizzare.
   */
  public final  void addLog(int type, String msg) {
    // Se il tipo non e' tra 1 e 8 scarta il messaggio
    if ((type < 1) || (type > 8)) return;

    // Testa la classe
    if ((flags & LogManager.CLASS[type]) > 0) {
      // Visualizza la data e l'ora se necessario
      if (dateTime) {
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy MMMMM dd, hh:mm:ss '-' ");
        int idx = msg.indexOf("$$");

        if (idx != -1)
          msg = msg.substring(0, idx) + fmt.format(new Date()) + msg.substring(idx + 2);
      }

      // Stampa il messaggio
      print(msg);
    }
  }

  /**
   * Dice se nel log sono inserite le informazioni su
   * data e ora. Per default tali informazioni non vengono
   * aggiunte al log.
   * @return true se il log manager visualizza anche la data e l'ora.
   */
  public boolean displayDate() {
    return dateTime;
  }

  /**
   * Abilita o disabilita l'output del tempo sui messaggi di log.
   * @param newValue Se vale true viene abilitata l'emissione di
   *                 informazioni temporali. Con false viene disabilitata.
   * @return Il vecchio valore del flag.
   */
  public boolean displayDate(boolean newValue) {
    boolean ret = dateTime;
    dateTime = newValue;
    return ret;
  }

  /**
   * Determina quali tipi di log sono abilitati.
   * @param type Classe di log da abilitare/disabilitare. 
   *             Se vale <code>LogManager.NONE</code> disabilita tutti
   *             i log. Se vale <code>LogManager.ALL</code> abilita tutte
   *             le classi. In questi due casi il secondo parametro viene
   *             ignorato.
   * @param set  Se type vale 1-8 (non <code>ALL</code> o <code>NONE</code>)
   *             abilita o disabilita la singola classe di log 
   *             (<code>true</code> abilita e <code>false</code> disabilita).
   */
  public void setClassLog(int type, boolean set) {
    // Check del parametro
    if ((type < 0) || (type > 9)) return;

    // Flag speciale
    if ((type == LogManager.ALL) || (type == LogManager.NONE)) {
      flags = LogManager.CLASS[type];
    } else { // Classe

      // Setta o cancella il bit dai flags
      if (set)
        flags |= LogManager.CLASS[type];
      else
        flags &= ~LogManager.CLASS[type];
    }
  }

  /**
   * Dice se il console manager supporta il log selettivo.
   * @return Restituisce sempre <code>true</code>.
   */
  public boolean isSelective() {
    return true;
  }
} // GenericLogManager


 

Un’ultima cosa da osservare è la possibilità di inserire un time stamp nel messaggio utilizzando un segnaposto $$ nel messaggio di log.

 

Non bastava il GenericLogManager?

La domanda può apparire legittima. Può sembrare che la classe GenericLogManager sia sufficientemente generale da rendere inutile l’interfaccia LogManager da cui eravamo partiti. In realtà non è così: si supponga di voler realizzare un canale di Log che stampi su due canali differenti. Ci sono molte applicazioni di questa struttura: potrebbe essere infatti necessario filtrare i messaggi sullo standard output ma immagazzinare l’intero log su un file. Ovviamente con un notevole sforzo può essere fatto utilizzando la classe descritta nel paragrafo precedente. D’altra parte implementando direttamente l’interfaccia LogManager è possibile scrivere la classe LogManagerPair riportata nel Listato 3 e che funziona come replicatore di messaggi: dato un messaggio in input al canale lo replica su due canali di log precedentemente definiti.
 

Listato 3 La classe LogManagerPair

package it.mokabyte.logmanager;
 
 

/**

 * <b>File:</b> LogManagerPair.java<br>

 * <b>Created:</b> Sat Apr 03 14:18:50 1999<br>

 * <b>Last modified:</b> <br> 

 * <p>

 * Questa classe collega due log manager in modo che vengano

 * chiamati in cascata. Prima viene invocato il primo log manager

 * passato al costruttore e successivamente il secondo. Pu&ograve;

 * risultare particolarmente utile quando bisogna effettuare log su

 * canali multipli.

 * <P>

 * <b>CHANGES:</b>

 * <ul>

 * </ul>

 *

 * @author Antonio Cisternino

 * @version 1.0

 */

public class LogManagerPair implements LogManager {

  /** Primo log manager */

  private LogManager logFirst;
 
 

  /** Secondo log manager */

  private LogManager logSecond;
 
 

  /**

   * Costruttore.

   * @param loga Primo log manager

   * @param logb Secondo log manager

   */

  public LogManagerPair(LogManager loga, LogManager logb) {

    logFirst  = loga;

    logSecond = logb;

  }
 
 

  /**

   * Aggiunge un messaggio al log. Aggiunge il messaggio nell'ordine

   * prima al primo log managere e poi al secondo (dove per primo

   * log manager si intende il primo passato al costruttore).

   * @param type    Classe del messaggio di log.

   * @param msg     Messaggio da visualizzare.

   */

  public void addLog(int type, String msg) {

    logFirst.addLog(type, msg);

    logSecond.addLog(type, msg);

  }
 
 

  /**

   * Seleziona le classi di log attive. Questo metodo chiama nell'ordine

   * lo stesso metodo del primo e del secondo log manager.

   * @param type Classe di log da abilitare/disabilitare. 

   *             Se vale <code>LogManager.NONE</code> disabilita tutti

   *             i log. Se vale <code>LogManager.ALL</code> abilita tutte

   *             le classi. In questi due casi il secondo parametro viene

   *             ignorato.

   * @param set  Se type vale 1-8 (non <code>ALL</code> o <code>NONE</code>)

   *             abilita o disabilita la singola classe di log 

   *             (<code>true</code> abilita e <code>false</code> disabilita).

   */

  public void setClassLog(int type, boolean set) {

    logFirst.setClassLog(type, set);

    logSecond.setClassLog(type, set);

  }
 
 

  /**

   * Dice se il log selettivo &egrave; supportato.

   * @return Viene restituito <code>true</code> se entrambi i log manager

   *         supportano la selezione dei canali di log.

   */

  public boolean isSelective() {

    return (logFirst.isSelective() && logSecond.isSelective());

  }

} // LogManagerPair
 


È possibile quindi collegare tra loro canali per ottenere gestioni anche molto complesse del log per composizione.

Tornando alle librerie…

Dopo aver raccontato la genesi e lo sviluppo di una mini libreria è bene soffermarsi a riflettere su ciò che si è fatto e come questo sia legato a quello che voglio descrivere. La libreria appena descritta è nata da un problema comune in cui la possibilità di riutilizzare il codice consente un risparmio nel tempo di sviluppo e quindi una maggiore produttività. È questa caratteristica che fa si che si possa parlare di libreria piuttosto che modulo in un sistema software.Una volta definito il problema si è cercato di dare una soluzione soddisfacente che con un costo relativamente ridotto permettesse di ottenere un modulo realmente riutilizzabile. Inoltre siamo riusciti ad ottenere una libreria facilmente estendibile, come si è potuto osservare dall’esempio del ConsoleLogManager.Ci possiamo chiedere ora: si poteva fare meglio? La risposta è ovviamente affermativa: si poteva, ad esempio, sfruttare il concetto di stream offerto da Java per definire il canale ottenendo una libreria più integrata con la libreria standard. D’altra parte probabilmente il gioco non sarebbe valso la candela poiché lo sforzo concettuale per definire le interfacce sarebbe stato sicuramente superiore.Fatta questa premessa veniamo ai tool descritti nel capitolo precedente e a come possono essere utilizzati per impacchettare la libreria e renderla distribuibile.Per compilare la libreria è sufficiente, come abbiamo visto nella puntata precedente, il comando

javac –d . *.java
eseguito nella directory contenente il codice. Il risultato della compilazione è la directory it contenuta nella stessa cartella contenente la directory mokabyte all’interno della quale si trova logmanager che contiene le classi compilate. Se si fosse specificata un’altra destinazione al posto di ‘.’ avremmo potuto generare il codice direttamente in un repository di classi sempre presente nel CLASSPATH.Ecco quindi che la nostra libreria è pronta per essere utilizzata nelle nostre applicazioni. Cosa fare ora per distribuirla? Ovviamente bisogna preparare la documentazione; ma io durante lo sviluppo del codice sono stato accorto ed ho annotato tutti i metodi e i membri delle classi con i commenti compatibili con javadoc. È quindi possibile generare automaticamente generare la documentazione della libreria. Osservare nel codice riportato nei riquadri come il commento sia in formato javadoc e utilizzi i marcatori speciali ‘@’ per definire a cosa sono associati i commenti. Inoltre la parola chiave @see consente di specificare un riferimento ad una classe che il generatore di documentazione traduce in un link alla documentazione della classe specificata.
 
 
 

Verso nuovi orizzonti

Una volta vista la nascita di una libreria dovrebbe essere semplice produrre le proprie librerie ottenendo in poco tempo un notevole repository di codice usabile con successo nei propri progetti. Quali elementi però mancano in questo panorama? Quali tecniche sono disponibili e non sono state ancora utilizzate?L’organizzazione in package del proprio codice è un passo necessario per costruire librerie; un’altra necessità che si presenta spesso è quella di accedere a delle risorse distribuite insieme alla libreria.Per mostrare queste capacità in più assumerò di sviluppare una libreria che contiene componenti grafici riutilizzabili. In particolare la libreria it.mokabyte.gx conterrà una comodissima message box come quella fornita con il package swing contenuto nella versione 1.2 del JDK. Tralasciando la parte di sviluppo grafico, vorremmo poter distribuire insieme alla libreria anche le icone (immagini) da visualizzare accanto al testo e rendere parametrica in base alla lingua il titolo della message box.Per poter includere risorse nelle proprie librerie è possibile a partire dalla versione 1.1 di Java utilizzare i metodi getResource e getResourceAsStream forniti dalla classe Class. Sfruttando questi due metodi è possibile localizzare le immagini direttamente nelle directory contenenti i file .class generati dal compilatore. Inoltre è possibile utilizzare la classe ResourceBundle per poter caricare le risorse dipendenti dalla località in cui viene eseguito il codice. In particolare la classe PropertyResourceBundle consente di ottenere risorse dipendenti dalla località persistenti e quindi caricate da uno stream magari ottenuto con il metodo getResourceAsStream.Sfruttando questi strumenti efficaci è quindi possibile ottenere la funzionalità essenziale di una libreria di poter accedere a risorse esterne.
 

 

Conclusioni

In questo articolo ho descritto brevemente lo sviluppo di una libreria per gestire il log di un’applicazione, cercando di mostrare come scelte progettuali ne condizionino il disegno e quali siano i passi da seguire. Successivamente ho discusso come utilizzare gli strumenti offerti dal JDK per impacchettare la libreria ed infine sono stati descritti alcuni strumenti che non erano stati utilizzati ma che spesso risultano essenziali.
 


  
 

MokaByte rivista web su Java

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