Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Versione 0.1 - Le classi comuni

Introduzione

In questa versione si scrivono le classi del package mastermind.common cioè le classi comuni a tutto il progetto ed usate in quasi tutti gli altri packages (forse non proprio in tutti). Queste classi non gestiscono la interfaccia utente, non sono affatto classi GUI nè gestiscono la logica del gioco ma servono per alleggerire il lavoro a tutte le altre classi del progetto.
Inoltre, scriveremo anche delle piccole applicazioni CLI di test per queste classi.

I files sorgente

Nella tabella seguente l'elenco dei files che compongono i due packages che stiamo analizzando in questo capitolo:

nome file package descrizione
MMLogger.java mastermind.common una utility per il log degli eventi
MMProperties.java mastermind.common una utility per la gestione delle proprietà della applicazione
Params.java mastermind.common i parametri di gioco per il Mastermind
Results.java mastermind.common i risultati del confronto tra la guess e la solution
Sequence.java mastermind.common gestisce e verifica le sequenze dei simboli
Version.java mastermind.common traccia le versioni della applicazione
MMException.java mastermind.common la classe base di tutte le eccezioni in MasterMind
PropertyException.java mastermind.common eccezione derivata da MMException
SequenceException.java mastermind.common eccezione derivata da MMException
ResultsException.java mastermind.common eccezione derivata da MMException
ParamsException.java mastermind.common eccezione derivata da MMException
TestParams.java mastermind.test.common app CLI di test
TestProperties.java mastermind.test.common app CLI di test
TestParams.java mastermind.test.common app CLI di test
TestResults.java mastermind.test.common app CLI di test
TestSequence.java mastermind.test.common app CLI di test
TestVersion.java mastermind.test.common app CLI di test

La classe MMLogger

Il nome deriva dal verbo inglese to log che letteralmente significa "registrare" ma viene usato normalmente come parola composta per esempio in logbook che significa "diario di bordo" oppure anche daylog che può essere tradotto in "diario giornaliero".

In informatica viene usato come sostantivo: il logger è una specie di "registratore" che annota tutti gli eventi importanti: in Linux/Unix™ vi è un servizio appositamente dedicato alla registrazione degli eventi; questo servizio fà parte del sistema operativo e si chiama syslog (SYStem LOGger = il logger di sistema).
Si tratta di un servizio molto complesso e articolato usato in maniera estensiva dagli altri servizi presenti sulla macchina come per esempio il server web, il server DNS e, in generale, tutti quei servizi che devono registrare gli accessi al sistema.

Java fornisce una utility molto simile per certi aspetti e anche molto versatile: basti pensare che i messaggi di log in Java possono essere inviati a diversi dispositivi: il terminale, un file su disco e persino su una connessione remota!
La classe che gestisce il logging in Java è la classe Logger che fà parte del package java.util.logging. Useremo questa classe per ottenere sul terminale la cronologia degli eventi che accadono quando la nostra applicazione è in esecuzione: il logger è un formidabile strumento di debugging al pari di un vero debugger.

I livelli di log

Ma quali sono gli eventi interessanti che dovremmo registrare? In un certo senso, tutti gli eventi sono interessanti; alcuni però lo sono di più di altri. Per esempio, se l'user muove il mouse potrebbe non essere un evento degno di nota mentre se egli ha completato la scelta della soluzione segreta è sicuramente un evento da registrare.

La classe Logger di Java implementa quello che viene definito un livello di log: un messaggio di log viene effettivamente emesso solo se il suo livello è maggiore o uguale al livello di log stabilito nel oggetto logger che lo deve emettere. Mi spiego meglio com un esempio:

// crea il logger e ne imposta il livello a 500
Logger logger = Logger.getLogger( "javatutor.mastermind" );
logger.setLevel( 500 );
// questo messaggio di log sarà emesso perchè ha un livello maggiore di 500
logger.log( 600, "messaggio di log con livello 600" );
// questo messaggio di log NON sarà emesso perchè ha un livello minore di 500
logger.log( 400, "messaggio di log con livello 400" );

I livelli di log sono espressi come numeri interi e possono avere un range di valori pari a quelli del tipo int.
Tuttavia, il package java.util.logging definisce anche alcune costanti mnemoniche per agevolare il programmatore nel decidere i livelli di log da assegnare ai messaggi. Queste costanti sono le seguenti:

  • Level.SEVERE (il livello più alto)
  • Level.WARNING
  • Level.INFO
  • Level.CONFIG
  • Level.FINE
  • Level.FINER
  • Level.FINEST (il livello più basso)

In aggiunta a questi, vengono definiti anche Level.OFF che sopprime tutti i messaggi di log e Level.ALL che, al contrario, abilita tutti i messaggi di log.
Infine, la classe Logger definisce dei metodi "di comodo" che sostituiscono il metodo log, col quale si emette un messaggio, in modo da utilizzare quale livello lo stesso livello del nome del metodo. In altre parole, i seguenti due costrutti sono equivalenti:

logger.log( Level.INFO, "messaggio con livello INFO" );
logger.info( "messaggio con livello INFO" );

Gli handlers dei messaggi

Ho già accennato al fatto che la classe Logger può emettere i messaggi di log su diversi dispositivi quali il terminale, un file su disco oppure anche una connessione remota. E non solo: ogni oggetto logger può emettere i suoi messaggi su tutti e tre questi dispositivi contemporaneamente!

Questo feature viene realizzato consentendo ad ogni oggetto logger di avere una catena di handlers (=gestori) dei messaggi. E questo non è tutto: anche i singoli handlers possiedono un proprio livello di log; quindi posso inviare al terminale i messaggi di livello INFO, per esempio, mentre sulla connessione remota voglio avere solo quelli di livello WARNING.

Quando viene creato, un oggetto logger possiede un handler assegnato automaticamente: il terminale. Questo handler automatico si trova al grado più alto della catena e viene sempre usato nei messaggi di log assieme a quelli di grado più basso.
Poichè nel nostro programma non siamo interessati ad inviare i messaggi di log a nessun altro dispositivo che il terminale, aggiungeremo quale handler la classe ConsoleHandler (il terminale, per l'appunto) e disabiliteremo l'invio di messaggi a qualsiasi altro handler, anche quello assegnato automaticamente.

La classe di log specializzata

Scriveremo pertanto una classe logger personalizzata, che chiameremo MMLogger, che definisce due diverse versioni del metodo getLogger:

  • una versione che accetta il nome del logger ed imposta un livello di log pari a INFO
  • una seconda versione che accetta il nome del logger ed un secondo parametro che rappresenta la stringa del livello di log (p.es. "INFO")

Come ultima nota, la classe MMLogger disabilita l'handler padre, qualunque esso sia (oggi è il terminale ma domani, chissà): viene usata come handler solo la console creata da noi:

// File: MMLogger.java
public static Logger getLogger( String name, String level )
throws IllegalArgumentException
{
... omissis ...
// crea ed imposta un handler dei messaggi (il terminale)
Handler hdl = new ConsoleHandler();
hdl.setLevel( Level.ALL );
log.addHandler( hdl );
// disabilita l'invio dei mnessaggi al handler genitore
log.setUseParentHandlers( false );
... omissis ...
return log;
}

Come già accennato, i messaggi di log sono molto utili in fase di debug della applicazione specialmente per una applicazione GUI: mentre si osserva l'output grafico comparire sullo schermo, a terminale possiamo avere i messaggi di log che sono di questo tipo:

nov 19,  3:24:48 PM mastermind.gui.Main notifyHaveSolution
INFO: notifyHaveSolution - player: PlayerAI1, solution: 452
nov 19,  3:24:56 PM mastermind.gui.Main notifyHaveSolution
INFO: notifyHaveSolution - player: Lukas, solution: 012
nov 19,  3:24:56 PM mastermind.gui.Main notifyStartGame

Il nome del logger

Ogni logger viene creato con un nome univoco il quale lo identifica. Creare un oggetto logger con lo stesso nome di uno già creato, non crea una nuova istanza ma restituisce lo stesso logger già creato in precedenza. Vi sono due strategie per assegnare il nome ai logger della applicazione:

  • la prima è quella di assegnare lo stesso nome a tutte le classi che si intendono implementare: in questo modo abbiamo un log unico per tutta la durata della applicazione.
  • la seconda strategia è quella di creare un logger per ogni classe che si deve scrivere. Il vantaggio di questa strategia è che possiamo loggare i messaggi della sola parte di applicazione che ci interessa

Nella applicazione Mastermind adotterò la prima strategia: un unico oggetto logger per tutta la durata della app il cui nome viene definito staticamente nella classe MMLogger stessa:

public class MMLogger
{
public static final String loggerName = "javatutor.mastermind";

Per creare il logger che vale per tutta la applicazione:

  • nel metodo principale creiamo un logger e ne impostiamo il livello di log
  • nel costruttore di ogni classe creiamo un logger con lo stesso nome di quello creato nel main
// nella classe principale
public static void main( String[] args )
{
logger = MMLogger.getLogger( MMLogger.loggerName, "INFO" );
}
// in tutte le classi in cui si desidera il log
public class MyClass
{
private static Logger logger = null;
public MyClass
{
if ( logger == null ) {
logger = Logger.getLogger( MMLogger.loggerName );
}
}
void method_A()
{
logger.info( "MyClass.method_A() called" );
}
}

La classe MMProperties

Ogni applicazione ha i suoi parametri di funzionamento che possono, normalmente, essere modificati dall'utente. Quando viene eseguita per la prima volta, la applicazione usa parametri di default stabiliti dal programmatore; questi parametri vengono chiamati le proprietà della applicazione.

I parametri di funzionamento dei programmi CLI (Command Line Interface) vengono normalmente impostati mediante opzioni e/o argomenti alla command-line. Abbiamo incontrato numerosi esempi di questo approccio nella prima parte di questo mio tutorial come per esempio in Java Tutorial Parte Prima - la sintassi della applicazione Geometry
Se state leggendo questo tutorial l'autore presume che abbiate già una certa familiarità col concetto di command-line ma, se proprio non ne sapete nulla o volete ripassare l'argomento, clikkate il riferimento seguente: Java Tutorial Parte Prima - la riga di comando.

Generalità delle proprietà

Il linguaggio Java mette a disposizione del programmatore una classe specializzata per gestire le proprietà di una applicazione: la classe Properties che è contenuta nel package java.utils. Da notare che la classe Properties non si limita a gestire le proprietà della applicazione ma qualsiasi dato che può essere contenuto in una coppia di stringhe del tipo:

KEY = VALUE

dove KEY rappresenta una chiave di ricerca e VALUE rappresenta il valore associato a quella chiave. Entrambi questi dati (la chiave ed il valore) sono di tipo String.
La classe Properties consente di impostare ed ottenere una proprietà attraverso i suoi due metodi principali: getProperty, che ritorna il valore della proprietà e setProperty che imposta il valore di una proprietà. Vi sono altri due metodi molto interessanti definiti nella classe Properties:

  • load che legge le proprietà da uno stream
  • save che scrive le proprietà su uno stream

Lo stream deve contenere le proprietà su ogni riga logica in un formato ben definito che si può riassumere nel seguente testo:

#
# questa è una riga di commento
#
KEY1        VALUE1
KEY2=VALUE2 
KEY3:VALUE3 
KEY4            il value della key 4 è definito \
                su tre righe le prime due delle \
                quali terminano col backslash 

Le regole di formato dello stream che contiene le proprietà sono piuttosto semplici:

  • le righe vuote vengono ignorate
  • se una riga contiene solo whitespaces è considerata vuota
  • se il primo carattere non-whitespace della riga è il cancelletto (#) la riga viene ignorata; è una riga di commento
  • ogni riga naturale termina col carattere di fine-riga e contiene una ed una sola coppia KEY / VALUE
  • ogni riga logica termina col carattere di continuazione backslash e continua sulla riga naturale seguente
  • i caratteri che separano la KEY dal VALUE sono: i due-punti (:), il carattere di uguaglianza (=) oppure ogni carattere whitespace
  • il carattere di continuazione riga (backslash) NON si applica alle righe di commento

Ogni applicazione ha le proprie specifiche proprietà e anche quella che scriveremo noi ha le sue. Una limitazione piuttosto fastidiosa della classe Properties è che può gestire solo valori stringa mentre normalmente tutte le applicazioni hanno valori di proprietà anche di tipo numerico e, molto spesso anche di tipo boolean.
Per maggiori informazioni vedi la documentazione ufficiale della libreria Java java.util.Properties.

Le proprietà del Mastermind

Anche la nostra applicazione ha proprietà specifiche; questo è l'elenco delle proprietà del Mastermind che andremo a scrivere:

Proprietà Tipo Default Descrizione
n intero 10 numero di simboli a disponibili per una sequenza
k intero 3 lunghezza di una sequenza
repeat boolean false flag di ripetizione dei simboli in una sequenza
maxTries intero 9 numero massimo di tentativi
swapturn boolean true scambio del primo turno in partite multiple
playerType String HUMAN il tipo di giocatore del pannello sinistro
playerName String null il nome del giocatore del pannello sinistro
opponentType String AI il tipo di giocatore del pannello destro
opponentName String null il nome del giocatore del pannello destro
guessDelay intero 1000 ritardo in millisecondi per le sequenze guess
serverAddr String null indirizzo IP del server MasterMind
serverPort intero 18862 porta TCP/IP del server MasterMind
codex String NUMBERS tipo di codici della sequenza: NUMBERS,LETTERS,COLORS
iconSize intero 30 dimensione dei simboli in pixels
logLevel String INFO il livello di log
mcInterval intero 1000 intervallo di invio (ms) dei pacchetti UDP
mcTimeout intero 500 timeout di ricezione (ms) dei pacchetti UDP
mcAddress String 230.18.8.0 indirizzo IP multicast per i pacchetti UDP
mcPort intero 3963 porta multicast per i pacchetti UDP

Come potete osservare, molte proprietà sono di tipo numerico intero e molte altre di tipo booleano. E' comunque possibile impostare una proprietà come stringa usando il metodo toString che si applica sia ai tipi numerici che ai tipi booleani. Di contro è possibile ottenere una proprietà come valore stringa e poi usare i metodi statici:

  • Integer.parseint per convertirla in un intero
  • Boolean.parseBoolean per convertirla in un booleano
    Invece di lasciare queste incombenze alla applicazione, scriveremo una classe proprieties specializzata, che chiameremo MMProperties.

Il costruttore

Il costruttore della classe MMProperties memorizza i valori di default di tutte le proprietà della applicazione ed ottiene il nome del file dove le proprietà vengono salvate. Quando il giocatore cambia i parametri di gioco si aspetta che questi cambiamenti siano persistenti: ecco perchè le proprietà vanno scritte su un file. Per determinare il nome del file dove scrivere le proprietà ci sono tre strategie:

  • la prima è quella di usare un nome di file fisso, ubicato nella directory corrente della applicazione ma questa strada è percorribile solo se il computer viene usato da una sola persona altrimenti le proprietà vengono sovrascritte da altri user.
  • una seconda strategia è quella di memorizzare i files delle proprietà in una sottocartella della applicazione (tipicamente etc) e di nominare i files con il nome dello user ed una estensione fissa; per esempio: pippo.properties. Lo svantaggio di questa soluzione è che se la applicazione viene rimossa e reinstallata oppure upgradata, si possono perdere i files delle proprietà personalizzati
  • la terza e più efficente strategia è quella di memorizzare le proprietà in un file con nome fisso (p.es. mastermind.properties) ed ubicarlo nella cartella personale di ogni user

Il costruttore di MMProperties usa la terza strategia e richiama il metodo System.getProperty per ottenere il nome della cartella personale dello user e il carattere di separatore di files che è diverso tra le varie piattaforme.
Per mezzo del metodo statico System.getProperty si possono ottenere diverse proprietà del sistema in cui viene eseguita la JVM. Per maggiori info vedi: Le proprietà di sistema.

I metodi

La classe base Properties si trova nel package java.util e mette a disposizione solo due metodi per impostare ed ottenere i valori delle proprietà:

  • getProperty che ritorna il valore come stringa
  • setProperty che imposta la proprietà da una stringa

poichè la classe base di Java gestisce solo stringhe sia come chiave che come valore. Tuttavia, per facilitare il compito delle classi di Mastermind che leggono le proprietà di tipo intero e booleano, la classe specializzata MMProperties definisce dei metodi di comodo:

  • dei metodi setProperties sovraccaricati in grado di accettare valori di tipo int e boolean
  • dei metodi getProperties specializzati che ritornano valori di tipo int e boolean
  • un metodo parseCmdline che crea uno stream nel formato corretto da una array di stringhe (la command-line) per essere poi letto dal metodo Properties.load
  • il metodo storeDefaults col quale si inizializza l'oggetto properties ai valori di default specificati nella tabella di cui sopra

Definiremo inoltre la classe PropertyException, una eccezione di tipo checked che viene sollevata in caso di errori che possono accadere nei metodi di cui sopra.
Per mezzo della classe specializzata potremmo pertanto allocare un oggetto properties ed impostare i valori delle proprietà per mezzo della command-line in questo modo:

public static Main( String[] argv )
{
try {
properties = new MMProperties();
properties.parseCmdLine( argv, 0 );
logger = MMLogger.getLogger( MMLogger.loggerName,
properties.getPropertyString( "logLevel", "INFO" )); // il livello di LOG
}
catch( PropertyException ex )
... omissis ...

Per esempio, possiamo impostare un log level diverso dal default semplicemente specificandolo sulla command-line:

1>java -ea mastermind logLevel=FINER

Quanto sopra è' più intuitivo del dover specificare una opzione di un solo carattere.

La classe MMException

Perchè creare una classe base delle eccezioni dal momento che già esiste ed è la Exception definita nel package java.lang? Benchè la classe Exception possieda un costruttore per mezzo del quale è possibile passare come argomento una altra eccezione, che rappresenta la causa scatenante, essa non possiede alcun metodo per ottenere il messaggio di errore di questa causa.

La classe che andremo a scrivere il cui nome è MMException colma questa lacuna definendo il metodo getCompleteMessage che, come suggerisce il nome, restituisce una stringa che contiene il messaggio di errore di questo oggetto eccezione oltre ai messaggi di errore di tutte le eccezioni passate come argomento cause iterando nella catena completa di oggetti eccezione passati come argomenti.

Per esempio, supponendo di ottenere una eccezione nella fase di connessione con un giocatore remoto sarà sollevata una Exception con un messaggio del tipo:

ERROR: cannot connect to remote host: mastermind.acme.net

Tuttavia non sappiamo nulla sulla causa effettiva della mancata connessione; potrebbe essere un errore di rete oppure potrebbe essere un bug del programma.
Se però noi riportiamo tutte le eccezioni in cascata che hanno causato l'errore possiamo poi trovare più agevolmente la soluzione. Per esempio, se il metodo getCompleteMessage riportasse un messaggio come il seguente:

ERROR: cannot connect to remote host: mastermind.acme.net
CAUSE - MessageException: presentation message - error in message field #2 
CAUSE - ParamsException: string 'x:y:z' is not valid

comprenderemo subito che la mancata connessione è dovuta al fatto che il messaggio di presentazione del remoto è errato poichè il secondo campo del messaggio, che dovrebbe contenere i parametri di gioco, contiene una stringa ("x.y.x") che non è considerata valida.
Il messaggio suesposto denota un bug del programma piuttosto che una mancata connessione di rete la quale, invece, darebbe un messaggio di questo altro tipo:

ERROR: cannot connect to remote host: mastermind.acme.net
CAUSE - IOException: unreacheble address

La classe Params

Nel gioco del MasterMind i giocatori tentano di indovinare una sequenza di simboli che ognuno dei due sceglie segretamente. Tuttavia, ci si deve accordare su alcune regole da condividere:

  • quanti simboli abbiamo a disposizione
  • la lunghezza della sequenza
  • se è lecito ripetere uno o più simboli nella sequenza
  • il numero massimo di tentativi a disposizione

Per maggiori info vedi anche Le regole del gioco. Chiameremo queste regole parametri di gioco e vengono rappresentati da una classe specializzata chiamata Params.
La classe contiene quattro membri dati:

  • n: il numero di simboli a disposizione che per default sono 10 e vengono indirizzati con i numeri da "0" a "9". Il numero massimo di simboli a disposizione è 10 mentre il numero minimo è 6. Il default è 10.
  • k: la lunghezza della sequenza con un minimo di tre simboli ed un massimo di quattro; il default è 3
  • repeat: un booleano il cui valore true indica che la ripetizione dei simboli è ammessa. Il default è false
  • tries: il numero massimo di tentativi per giocatore raggiunto il quale la partita si intende patta. Il default è 9.

Potremmo avere altri simboli oltre ai numeri da "0" a "9"? Certo che si, anzi! In fondo il simbolo scelto non ha molta importanza; potremmo usare le casette, gli animali, le navi da guerra etc.
Quello che conta è solo l'indice del simbolo nell'insieme dei simboli a disposizone. Quindi, una sequenza come "012" indica semplicemente il primo, il secondo ed il terzo simbolo nell'insieme dei simboli che possono essere:

  • i numeri da "0" a "9", tipo di simbolo: NUMBERS
  • le lettere da "A" a "J", tipo di simbolo: LETTERS
  • dieci colori scelti da una tavolozza specifica, tipo di simbolo: COLORS

Questi che ho elencato sono effettivamente i tipi di codice che implementeremo nella applicazione. I dieci colori della tavolozza dei simboli di tipo COLORS sono elencati di seguito:

  • 0: orange
  • 1: red
  • 2: lime
  • 3: blue
  • 4: yellow
  • 5: cyan/aqua
  • 6: maroon
  • 7: magenta/fucsia
  • 8: green
  • 9: steel blue

Ovviamente, siete liberi di modificarli come meglio credete e non solo: siete invitati anche a creare alri gruppi di simboli ed implementarne le classi che li gestiscono. Grazie alla caratteristica del polimorfismo di Java è possibile implementare un insieme di nuovi simboli in modo semplice e veloce: basta derivare la nuova classe da mastermind.gui.SequenceGUI ed implementarne i metodi astratti.
Ma questo è un argomento che affronteremo più avanti (vedi Il pannello delle sequenze).

I costruttori

Un oggetto di classe Params può essere costruito in quattro modi diversi a cui corrispondono quattro costruttori:

  • il costruttore di default che imposta i parametri di default: n=10, k=3, repeat=false, tries=9
  • un costruttore che accetta tre dei quattro parametri di gioco con il quarto (tries) impostato al default
  • un costruttore che accetta tutti i quattro parametri di gioco come argomenti
  • un costruttore che accetta come argomento una stringa di caratteri nel formato n:k:repeat:tries e che viene interpretata come quattro campi separati dal carattere due-punti.

Considerato che i parametri di gioco devono essere costruiti con limiti ben precisi passati come argomenti ai costruttori và da se che in caso di valori fuori range sarà sollevata una eccezione di tipo ParamsException. Un caso a parte è costituito dal passare come argomenti n=0,k=0. In questo caso il costruttore della classe Params sceglierà a caso un valore ammissibile per i due parametri di gioco.

I metodi

Tutti i metodi di Params sono di facile lettura. Tutti i metodi setter possono sollevare una eccezione di tipo ParamsException se gli argomenti a questi metodi non sono validi o sono fuori range:

  • il parametro n deve essere compreso tra 6 e 10 oppure deve essere ZERO
  • il parametro k deve essere compreso tra 3 e 4 oppure deve essere ZERO
  • il parametro tries deve essere compreso tra 2 e 19
  • la stringa che descrive i parametri deve essere nel formato n:k:repeat:tries dove n, k, e tries devono essere numerici e repeat può solo essere true o false.

La classe Sequence

La sequenza è uno dei punti cardine del gioco: nella prima fase i due giocatori scelgono una sequenza segreta (che chiameremo solution) e, dopo che entrambi hanno scelto la solution inizia la seconda fase del gioco nella quale, a turno, i giocatori tentano di indovinare la solution dell'avversario.

Nella applicazione Mastermind una sequenza di simboli viene rappresentata dalla classe String; la sequenza non contiene i simboli ma i loro codici numerici da "0" a "9". Unica eccezione a questa regola è la stringa "resign" (=resa) che significa che il giocatore si è arreso.
Per facilitare il compito di verificare e gestire le sequenze di Mastermind è stata scritta una classe specializzata di nome Sequence che contiene solo metodi statici per:

  • verificare se la sequenza è valida in base a determinati parametri di gioco
  • verificare se la sequenza contiene simboli ripetuti
  • verificare se la sequenza contiene caratteri non ammessi
  • verificare se la sequenza contiene solo caratteri numerici
  • verificare se la sequenza contiene la stringa di resa
  • ottenere i singoli simboli della sequenza ed il loro valore numerico in base alla loro posizione nella sequenza
  • ottenere l'indice di un simbolo nella sequenza

In particolare, il metodo statico checkValid verifica la validità di una sequenza rispetto a determinati parametri di gioco e solleva una eccezione di tipo SequenceException che descrive nel proprio messaggio quale è il parametro che la sequenza non soddisfa. Questi sono esempi di messaggi di errore che possono essere sollevati:

Invalid sequence: 112 - repetition of chars is not allowed
Invalid sequence: 5679 - the char '9' is not allowed (n=8)

La classe Results

Il risultato del confronto tra le due sequenze di codici solution e la ipotesi guess è dato da due valori interi:

  • il numero di simboli presenti in entrambe le sequenze ma in posizione errata detto whites
  • il numero di simboli presenti in entrambe le sequenze ed in posizione esatta detto reds

Se il numero di reds coincide con la lunghezza della sequenza allora le due sequenze coincidono perfettamente e la ipotesi è vincente. I risultati del confronto tra la solution e la guess vengono rappresentati dalla classe Results.

L'oggetto Results, oltre al numero di reds e whites contiene anche la lunghezza delle sequenze in modo da poter verificare se questo oggetto risultati è vincente; il metodo isWinning della classe Results ritorna TRUE se il numero di reds è uguale alla lunghezza delle sequenze.
Nella seguente tabella sono riportati i risultati di alcuni confronti:

solution guess len reds whites isWinning
123 105 3 1 0 false
123 267 3 0 1 false
456 435 3 1 1 false
4567 4876 4 1 2 false
1234 1234 4 4 0 true

I costruttori

La classe Results non possiede un costruttore di default: questo perchè un argomento è essenziale: la lunghezza delle due sequenze confrontate. Vi sono tre costruttori che accettano come argomenti:

  • la lunghezza delle sequenze confrontate
  • la lunghezza delle sequenze confrontate ed il numero di reds e whites
  • una stringa che rappresenta i tre argomenti separati da due-punti nel formato k:r:w lo stesso formato ritornato dal metodo toString

I metodi

Il cui metodo più importante della classe Results è sicuramente il metodo statico seguente:

public static Results compareSequences( String solution, String guess )
{
... omissis ...
}

Questo metodo confronta le due sequenze e fornisce i risultati del confronto in un oggetto di classe Results. Il confronto avviene in questo modo:

  • per primi vengono confrontati i simboli nella stessa posizione e, se coincidono, si incrementa il contatore dei reds
  • successivamente ogni simbolo presente in guess viene confrontato con i simboli che appaiono in posizione diversa rispetto al simbolo di guess e, se un match viene riscontrato si incrementa il numero di whites

Da notare che se le due sequenze da confrontare hanno lunghezze diverse il metodo compareSequences ritorna null; un evento che non dovrebbe mai accadere in una partita di mastermind (salvo bugs, ovviamente).

La classe Version

La classe Version gestisce la versione specifica di ogni rilascio della applicazione. Può essere costruita in due modi:

  • specificando i tre interi che rappresentano la versione maggiore, minore e bug_fix
  • specificando un unico intero che ha lo stesso significato che ho descritto in La versione come intero.

La classe possiede i metodi per:

  • restituire uno qualsiasi dei tre interi che costituiscono il numero di versione
  • restituire il numero di versione come intero
  • restituire la versione come una stringa nel formato "x.y.z"

Notate che la classe Version restituisce la versione come intero esattamente come descritto in La versione come intero, il byte meno significativo rappresenta il campo bug_fix, il secondo byte meno significativo rappresenta il campo minor_version mentre i due bytes più significativi rappresentano la major_version.

Noterete che la definizione della classe Version è molto diversa da come siamo abituati.
A partire da Java 16 è stato introdotto un nuovo feature in Java chiamato record: Un record è una classe a tutti gli effetti ma con queste caratteristiche particolari:

  • è una classe immutabile (quindi è final)
  • vengono dichiarati automaticamente i membri dati final che sono gli argomenti del record
  • viene dichiarato automaticamente un costruttore che accetta gli argomenti al record
  • vengono dichiarati automaticamente i metodi getter di tutti i membri dati; i metodi hanno lo stesso nome del membro dati.
  • non ci sono metodi setter, dal momento che i membri dati sono final

In sostanza, la definizione di un record come la seguente:

public record Version ( int major, int minor, int bugfix )
{ }

equivale a:

public final class Version
{
private final int major, minor, bugfix;
public Version ( int major, int minor, int bugfix )
{
this.major = major;
this.minor = minor;
this.bugfix = bugfix;
}
public int major()
{
return major;
}
public int minor()
{
return major;
}
public int bugfix()
{
return bugfix;
}
}

E' comunque possibile aggiungere metodi specializzati, anche statici, e costruttori specializzati oltre alla possibilità di definire membri dati aggiuntivi. Per esempio, nella classe Version è stato aggiunto un costruttore che prende come argomento il numero di versione come intero ed il metodo asInt che, come prevedibile, restituisce l'oggetto Version come un intero.
L'unico vincolo con i record è che qualsiasi costruttore aggiuntivo richiami come prima istruzione il costruttore creato automaticamente dal compilatore:

public Version( int versionAsInt )
{
this( versionAsInt >> 16, // major
(versionAsInt & 0xFF00) >> 8, // minor
versionAsInt & 0xFF ); // bugfix
}

Una suite di test

Per coloro che hanno letto il mio Java tutorial parte prima: suite di test questo argomento non è una novità. Per tutti gli altri descriverò brevemente l'argomento.
Abbiamo scritto diverse classi che contengono molti metodi ma come possiamo essere sicuri che essi funzionino in modo corretto? E sopratutto, cosa si intende per in modo corretto?
Ebbene, la risposta alla seconda domanda è piuttosto semplice e ve la spiegherò nel modo più semplice possibile ed in linea con lo spirito di questo tutorial: con un esempio!

Prendiamo in esame il metodo statico getNotAllowedChar della classe Sequence il quale ritorna il primo carattere non ammesso in una sequenza. Per testare efficacemente questo metodo dobbiamo partire dalla documentazione del metodo stesso che trovate a questo link: scopriamo che il metodo ritorna il carattere non ammesso oppure -1 se tutti i caratteri nella sequenza sono ammessi. I caratteri sono ammessi se:

  • sono numerici
  • il valore numerico di ognuno di essi deve essere compreso tra ZERO e n-1

Un modo di testare il metodo è quello di scrivere una piccola applicazione che accetta come parametro la sequenza ed il parametro n e verificare la correttezza dell'output.
Questa soluzione è piuttosto inefficente e pure inefficace: inefficente perchè ci fà perdere un mucchio di tempo ed inefficace perchè per un essere umano è difficile non commettere errori nella verifica.

Un modo decisamente più efficente ed efficace è quello di lasciare al computer l'onere della verifica: si tratta solo di fornirgli i dati da analizzare. Essi sono i seguenti:

  • le sequenze di caratteri
  • il parametro n cioè il numero massimo di codici a disposizione
  • i risultati attesi cioè il valore ritornato dal metodo getNotAllowedChar

In questa tabella riepiloghiamo il risultato atteso per tre possibili valori di n e per cinque sequenze:

Sequenza n=10 n=8 n=6
12a3 'a' 'a' 'a'
12346 -1 -1 '6'
24561a 'a' 'a' '6'
g1234 'g' 'g' 'g'
0123456789 -1 '8' '6'

Possiamo pertanto scrivere un metodo di test automatizzzato usando il costrutto Java assert il quale verifica che la condizione fornita come operando sia vera. La condizione che deve essere soddisfatta nel costrutto assert è, ovviamente, che il valore restituito dal metodo getNotAllowedChar sia uguale al valore che noi abbiamo individuato come risultato atteso.
Nel file sorgente mastermind/test/common/TestSequence.java è stato scritto il metodo statico test02 che esegue proprio ciò che ho descritto poc'anzi:

// File: TestSequence.java
static void test02()
{
System.out.println();
System.out.println( "TEST #2: getNotAllowedChar() - START" );
String[] sequences = { "12a3","12346","24561a","g1234","0123456789" };
int[] paramN = { 10, 8, 6 };
int[] res = { 'a', 'a', 'a',
-1,-1,'6',
'a','a','6',
'g','g','g',
-1,'8', '6' };
for ( int x = 0; x < sequences.length; x++ ) {
System.out.println( "Verifying string: " + sequences[x] );
for ( int y = 0; y < paramN.length; y++ ) {
int cr = Sequence.getNotAllowedChar( paramN[y], sequences[x] );
System.out.println( " N="+paramN[y] + " char not allowed=" + (char)cr );
int idx = (x*3)+y;
assert( cr == res[idx] );
}
}
System.out.println( "TEST #2 - END" );
}

Dobbiamo scrivere dei metodi molto simili anche per gli altri metodi della classe Sequence e non solo: anche per le altre classi del package mastermind.common. In effetti, nel package mastermind.test sono contenuti queste piccole applicazioni ognuna delle quali testa i metodi di una classe specifica.

Tutti i test vengono eseguiti in automatico dal programma: unica accortezza da tenere presente è che è assolutamente necessario specificare la opzione -ea (=enable assertion) quando il programma viene mandato in esecuzione. Il programma di test accetta un argomento: il numero del test, che comunque non è obbligatorio. Se il numero del test non viene indicato, il programma di test li esegue tutti in sequenza.

L'insieme delle classi di test che trovate nel package mastermind.test viene comunemente chiamato test suite (non credo esista un corrispondente in italiano abbastanza calzante) ed è una pratica molto diffusa tra i programmatori esperti: verificare che le classi che scriviamo fanno esattamente il lavoro per cui sono state progettate (e documentate!!!) ci risparmia un mucchio di tempo in futuro speso alla ricerca dei bugs.

Ulteriore documentazione

La documentazione completa dei due packages descritti in questo capitolo è disponibile clikkando il seguente link che riporta alla documentazione Javadoc del progetto: The MasterMind Project Version 0.1

Argomento precedente - Argomento successivo - Indice Generale