Il
framework
La
riprova della validità di tutte le scelte sin qui effettuate, per
quanto perfettamente giustificabili in sede teorica, non si sarebbe potuta
avere che nel corso della realizzazione del framework.
Alla
luce dei canoni odierni nello sviluppo del software, ed in misura ancora
maggiore all’interno di quei progetti concepiti in seguito ad una precisa
e rigorosa fase di modellazione, è innegabile come esso rappresenti
la spina dorsale del programma e sia in grado in qualche modo di anticipare
tanto quelli che saranno i punti di forza dell’applicazione definitiva
quanto le sue debolezze.
Il
nostro team era, in questo caso, perfettamente conscio di muoversi in un
terreno per gran parte inesplorato: mentre infatti abbondavano le discussioni
teoriche e pratiche in materia di framework per l’utilizzo del linguaggio
Java lato server, documentazione analoga per il suo utilizzo in applicazioni
stand-alone non era assolutamente rintracciabile. Il mondo dello sviluppo
sembrava ignorare bellamente la questione, e chi si era già trovato
ad affrontarla - quali che ne fossero gli esiti - si guardava bene dal
renderne noti i risultati.
La
prima scelta focale che ci trovammo davanti fu quella del modello di cui
si sarebbe avvalso Irmgard per gestire la base di dati SQL. Identificammo
fin dall’inizio due casistiche tipiche di utilizzo delle tabelle:
-
Utilizzo
“leggero”, tipicamente associato alle finestre riepilogative e di dettaglio
che implementano l’interfaccia di gestione delle tabelle, in cui identificammo
come prioritaria la facilità di codifica e design delle maschere
di input. In questa modalità di utilizzo, infatti, è l’interazione
tra utente e programma ad essere preponderante, sino al punto di coprire
eventuali inefficienze del codice, tanto più che la mole di dati
scambiati è quasi sempre limitata alla sola riga “corrente”;
-
utilizzo
“pesante”, richiesto dalle procedure batch di aggiornamento e stampa della
base dati e - in misura minore - in alcuni particolari scenari dell’interfaccia
utente, come quelli legati alla gestione di tabelle con vista gerarchica
(master/detail). In questo caso ci trovammo tutti d’accordo nel privilegiare
la velocità di gestione dei dati e di esecuzione del codice rispetto
alla fase di codifica, ricorrendo a delle stored procedure nelle situazioni
di maggior carico relazionale.
Le
nostre passate esperienze ci permisero di scartare, velocemente e senza
rimpianti, tutte le librerie di gestione dati prodotte da terze parti,
vuoi per lacune implementative o prestazionali, vuoi per lo scarso senso
di fiducia e solidità trasmessoci dalla più parte dei produttori.
Avendo rinunciato sin dal principio anche alle API JBCL, considerate obsolete
dalla stessa Borland/Inprise, ci restavano in mano due possibilità:
JDBC e dbSwing, vale a dire il set di API, oggetti e componenti orientati
alla gestione dei database realizzato da Borland/Inprise per il suo JBuilder
3.0 in sostituzione dell’obsoleto JBCL, e completamente basato su Swing.
Dal
momento che sulla reale fattibilità della soluzione dbSwing molti
di noi manifestavano delle fondate perplessità, essenzialmente legate
a questioni di solidità e performance, stabilimmo che l’unico modo
per chiarire completamente ed in maniera definitiva i pro ed i contro di
ogni soluzione era quello di implementare dei framework di test, sia per
l’utilizzo “leggero” che per quello “pesante”, tanto in JDBC che in dbSwing.
I risultati di questa fase preliminare di codifica furono molto interessanti,
e ci permisero di identificare chiaramente e con sicurezza le aree preferenziali
di utilizzo delle API, dandoci inoltre la certezza di aver trovato una
soluzione “allo stato dell’arte” e sufficientemente generale da poter venire
applicata anche nei nostri progetti futuri:
-
Il dbSwing
si prestava realmente all’utilizzo “leggero” della base dei dati: il componente
JdbTable, opportunamente personalizzato ed affiancato a strumenti di navigazione,
ricerca e selezione, rappresentava un’ottima soluzione al problema della
rappresentazione tabellare degli archivi SQL, così come i componenti
orientati ai dati permettevano di realizzare con rapidità form di
data-entry intuitivi e funzionali; al contrario, l’utilizzo del JDBC in
queste situazioni, a fronte di una maggior complessità di codifica,
non sembrava portare vantaggi significativi in termini di performance;
-
gli scenari
“pesanti”, in cui il dbSwing mostrava la corda, venivano invece risolti
brillantemente da un approccio basato sul JDBC, sino al punto di rendere
inutili nella grande maggioranza delle situazioni le stored procedure,
cosa che – en passant – ci ha consentito di aumentare drammaticamente la
portabilità dello strato SQL della nostra applicazione;
-
le prove
sul campo ci rivelarono l’esistenza di uno scenario per così dire
“ibrido” e legato alla gestione di tabelle particolarmente complesse per
numero di colonne o per ricchezza di join: in questi casi il dbSwing, con
la sua gestione estremamente automatizzata, sembrava appesantire oltremodo
la gestione dei dati. Queste situazioni sono state risolte limitando l’uso
del dbSwing alle sole colonne più significative - ossia alle colonne
primarie ed alle colonne per cui l’utente può eseguire ordinamenti,
ricerche e selezioni – ed utilizzando il JDBC per le colonne rimanenti;
la gestione dei relativi form di input in “mixed-mode” non ha presentato
particolari problemi tecnici, garantendoci l’atteso guadagno in performance.
Chiarito
così il ruolo del dbSwing, restava aperta la questione dell’implementazione
del middleware di mappatura tra oggetti e JDBC: quanto l’SQL avrebbe dovuto
venire innestato nel programma, e quanto invece avrebbe dovuto venire nascosto
negli oggetti Row (del tutto slegati dalle classi ReadRow e ReadWriteRow
del dbSwing) che costituivano il punto di accesso dell’applicazione ai
dati ?
Nella
risposta a questa domanda, come a molte altre questioni sorte durante lo
sviluppo di Irmgard, il nostro team dovette accettare un compromesso sensato.
Ancora più stringente dei vincoli di manutenibilità ed espandibilità
era infatti per noi il requisito dell’usabilità. I test e le ricerche
condotti nelle fasi preliminari di design ci avevano fatto conoscere troppi
esempi di software in Java strutturalmente perfetti ma praticamente inutilizzabili,
e avevano fatto capire a tutti noi che la nostra sfida, ed il successo
di Irmgard, non sarebbe consistito nel realizzare un’impalcatura teorica
impeccabile ed elegante da trasmettere ai posteri come misura delle nostre
conoscenze e della nostra abilità, magnifica ma lontana dal mondo
reale come un’opera d’arte in un museo. No, quello che noi volevamo, e
che crediamo di essere riusciti a realizzare, è un programma che
compensa alcune lacune teoriche, da noi introdotte per scelta al fine di
garantire un accettabile livello di performance, con un’utilizzabilità
effettiva e reale: Irmgard non è l’ennesimo esempio teorico di programmazione
in Java, ma è uno strumento di lavoro.
In
sostanza il nostro programma avrebbe utilizzato la metafora object-based
delle Row per l’accesso ai dati nella maggioranza dei casi, scavalcandola
con un utilizzo diretto dell’SQL solo in presenza di elaborazioni particolarmente
pesanti ed onerose.
Un
altro compromesso venne raggiunto proprio per quanto riguarda la gestione
delle tabelle via dbSwing, accettando il fatto che i form realizzati con
queste API prescindessero dal nostro framework JDBC object-based ed utilizzassero
direttamente le classi di accesso ai dati di JBuilder, senza per questo
creare alcun problema pratico, ma piuttosto aprendo una questione “ideale”,
dal momento che le tabelle SQL di Irmgard si trovavano ad avere una sorta
di doppia identità, venendo viste dal programma come una collezione
di Row ma dal relativo form di gestione tabelle come un DataSet.
Assieme
alla gestione dei dati, un punto focale per ogni applicazione basata sul
data-entry consiste storicamente nella validazione dell’input dell’utente,
per la quale si possono individuare due approcci principali:
-
Approccio
“rigido”, nel quale all’utente non viene permesso lo spostamento da un
campo di input ad un altro in presenza di dati non validi: questo approccio
fornisce un feedback immediato in presenza di errori, ma può trasformare
il lavoro di data-entry in un’esperienza opprimente, in quanto limita in
maniera artificiale le possibilità di navigazione dell’operatore,
introducendo delle barriere che appaiono tanto più innaturali ed
arbitrarie all’interno delle moderne interfacce grafiche, la cui caratteristica
precipua sta proprio nella libertà di muoversi e spaziare tra applicazioni
e finestre a proprio piacere;
-
approccio
“libero”, nel quale il movimento tra campi è sempre possibile, ed
eventuali errori vengono segnalati al momento della conferma definitiva
dell’intero form.
La soluzione
da noi adottata nei progetti precedenti si poneva a metà fra questi
due estremi, e costituiva secondo noi una soluzione elegante e funzionale
all’annoso problema della gestione dell’input, che decidemmo di applicare
in toto anche al progetto Irmgard.
Per
la precisione, il nostro approccio “ibrido” combinava le due soluzioni
suddette, rendendo però non bloccanti i messaggi di errore della
soluzione “rigida”, i quali venivano visualizzati in un’apposita barra
di stato.
Una
situazione nuova, e non prevista, fu invece legata alla coesistenza - anche
nel medesimo form – dei controlli orientati ai dati, vale a dire dbSwing,
con i normali controlli Swing. I primi, infatti, effettuavano una validazione
ed una riformattazione dell’input che, seppur molto utile, confondeva l’utente,
che si trovava nella situazione di vedere gli stessi dati trattati differentemente
se inseriti in un punto del form piuttosto che in un altro. La soluzione
consistette nel creare due gerarchie parallele di controlli di input, una
orientata ai dati ed una no, le quali implementavano le medesime regole
di gestione dell’input (riformattazione delle date inserite dall’utente,
visualizzazione di importi valutari con inserimento di punti decimali per
aumentarne la leggibilità, e così via), ed utilizzarle in
maniera esclusiva all’interno dei form dell’applicazione.
Infine,
il cerchio del nostro framework venne chiuso dall’implementazione di un
certo numero di classi controller – tipiche di ogni ambiente gestionale
– dotate dei comportamenti atti alla gestione ed alla conversione di diversi
tipi di dati, quali valori numerici interi e decimali, oggetti java.util.Date,
java.sql.Date e java.sql.Timestamp. A queste classi se ne aggiunsero altre
necessarie per colmare alcune lacune dello Swing, o per sfruttarne al meglio
le potenzialità, fornendo nel contempo un look & feel ed una
operatività coerenti al nostro programma – in particolare, ci trovammo
a lavorare sui Model delle classi JTree e JTable – nonché una fondamentale
helper class per l’accesso a sorgenti di dati JDBC, in grado sia di semplificare
ed uniformare la gestione di Statement e ResultSet all’interno di Irmgard,
a tutto vantaggio di performance e stabilità, sia di farsi carico
in prima persona della gestione dei diversi livelli di API JDBC e delle
loro implementazioni da parte dei driver.
La gestione dei
BigDecimal e delle colonne BLOB
Il
solo fatto che questi argomenti meritino un paragrafo a parte è
piuttosto significativo. Infatti, come chiunque abbia esperienza di database
SQL non può fare a meno di provare un vago senso di nausea alla
sola idea di una query in cui uno dei parametri sia una data o – peggio
ancora – una colonna data/ora o timestamp (ed è un vero sollievo
scoprire come il JDBC grazie ai PreparedStatement e ad una sintassi “ufficiale”
per l’escaping delle espressioni data/ora risolva definitivamente questi
problemi), così i componenti del mio team si trovarono ben presto
ad essere inseguiti, nei loro sogni, da orde di BigDecimal e colonne BLOB.
Per
quanto riguarda i BigDecimal, essi hanno rappresentato per noi una vera
sfida, fortunatamente vinta, sia per quanto riguarda la loro gestione all’interno
del programma che a livello di SQL. Essi sembrano in effetti possedere
una filosofia di funzionamento propria, e non esattamente intuitiva:
-
Il costruttore
BigDecimal(String) - che è senz’altro quello da preferire in quanto
non comporta perdite di precisione od errori di arrotondamento – si trova
nella difficile situazione di dover inferire la scala del BigDecimal da
istanziare alla luce dell’argomento ricevuto. Questo significa, in pratica,
che un BigDecimal costruito con new BigDecimal(“0”) ed un altro costruito
con new BigDecimal(“0.00”) non si comporteranno allo stesso modo e obbliga
ad inizializzare tutti i valori decimali con delle stringhe contenenti
dei valori a virgola fissa e con un numero di zeri decimali pari alla precisione
specificata nella clausola di dominio della corrispondente colonna DECIMAL
a livello di SQL;
-
in tutte
le operazioni su BigDecimal passibili di perdite di precisione è
bene specificare sempre la scala desiderata, e – in caso di operazioni
con valori monetari - utilizzare ROUND_HALF_UP come modalità di
arrotondamento. Inoltre, come è d’altronde prassi in queste situazioni,
le operazioni “a rischio” dovrebbero sempre venire eseguite in coda ad
una serie di calcoli, per evitare la propagazione degli errori di arrotondamento.
-
i driver
JDBC non allineati alla versione 2 - come quello, già menzionato,
di DB2 - richiedono che per ogni operazione con colonne BigDecimal venga
esplicitamente fornita la scala della colonna stessa; questa situazione,
assieme a quelle citate ai due punti precedenti, rende la gestione dei
BigDecimal piuttosto ridondante ed onerosa, costringendo a specificarne
la scala praticamente ad ogni utilizzo.
La situazione
si presenta invece migliore per i BLOB, sulla cui gestione pratica mi soffermerò
brevemente: infatti il gran numero di metodi ad essi associati nelle API
JDBC sembra confondere molti sviluppatori. Essi possono comodamente venire
letti come stringhe (utilizzando il metodo getString() di un ResultSet)
o, qualora essi contengano una mole significativa di informazioni, attraverso
i metodi getBlob() e getClob() – il primo per i BLOB propriamente detti,
vale a dire per quelli contenenti informazioni binarie, il secondo per
i Character BLOB contenenti informazioni testuali – o attraverso le “scorciatoie”
getCharacterStream(), getAsciiStream() e getBinaryStream(). In particolare,
questi ultimi due metodi sono gli unici utilizzabili in presenza di driver
JDBC di livello 1. Per quanto riguarda la loro archiviazione la via più
semplice è quella di utilizzare dei PreparedStatement usufruendo
dei metodi paralleli setCharacterStream(), setAsciiStream() e setBinaryStream()
o, qualora ci si trovi a modificare delle righe già presenti nel
database, dei metodi setBlob() e setClob(). Tipicamente i dati da memorizzare
veranno inviati al layer JDBC attraverso dei ByteArrayInputStream o – solamente
nel caso di Character BLOB ed in presenza di API JDBC di livello 2 - degli
StringReader.
Infine,
per impostare una colonna BLOB a null l’approccio raccomandato è
quello di far ricorso alla chiamata setNull() – sempre associata ad un
PreparedStatement – specificando come tipo di dato Types.BLOB per BLOB
binari e Types.CLOB per Character BLOB; nel caso di un driver JDBC di livello
1 il tipo da utilizzare sarà invece Types.LONGVARCHAR.
Per
onestà devo ricordare come nessuno dei driver JDBC da me conosciuti
implementi completamente le API di livello 2, e questo non solamente nella
gestione dei cursori updateable o delle estensioni ad oggetti – che, in
tutta franchezza, mi sento di definire delle pure illusioni – ma persino
nella gestione dei BLOB. Lo stesso driver Informix, che rimane comunque
una delle implementazioni più solida ed affidabile, pecca fortemente
in questo senso fermandosi ancora alle API JDBC 1.22 e segnalandosi per
diversi e gravi bug nella gestione di questo tipo di colonne.
Le stampe
Le
applicazioni gestionali sono per tradizione grandi produttrici di stampe
cartacee, e – per quanto sia ragionevole pensare di eliminarne una buona
parte avvalendosi degli strumenti di condivisione di informazioni disponibili
negli attuali sistemi informativi distribuiti – alcuni documenti, per loro
stessa natura e a cause di precise motivazioni legali e fiscali, non possono
prescindere dalla semplice realtà fisica di un foglio di carta stampata.
Dunque anche la nostra applicazione si trovava nella necessità di
dover gestire un vasto bagaglio di stampe, alcune delle quali avrebbero
dovuto essere eventualmente consultabili on-line. Forti di una prassi consolidata
nella loro gestione, messa prima d’allora alla prova in diverse applicazioni
native, ed essendo a conoscenza di una generica, intrinseca “debolezza”
nelle gestione delle stampe propria del linguaggio di Sun, decidemmo sin
dall’inizio di non lavorare direttamente sulle classi del package java.awt.print
– tra l’altro estese e rinnovate nel JDK 1.2 – ma di avvalerci di un buon
tool di reporting, e cominciammo quindi le nostre ricerche in tal senso.
Il mercato offriva un vasto insieme di scelte, più o meno portabili,
alcune delle quali dotate di tool visuali per la definizione delle stampe,
e molti di questi ambienti di gestione affiancavano alla versione tradizionale
del loro prodotto una versione server-based, nella quale le stampe venivano
inviate dai client ad un server, e da questo processate e riversate fisicamente
in stampa. Il prodotto che ci sembrò più completo e ci colpì
in virtù dell’utilizzo per la memorizzazione dei report non di un
formato proprietario, ma di un linguaggio XML – cosa che ci faceva ben
sperare nell’ottica di future possibili integrazioni - fu il printing tool
ReportStyle Pro [1]. Come se ciò non bastasse questo prodotto consisteva
in realtà di un insieme di componenti ed API Java, per cui i report
potevano venire generati sia a partire da un file XML sia in modalità
code-driven, gestendone in sostanza il contenuto in maniera affine ad una
sorgente di dati JDBC. Si trattava di una soluzione di un’eleganza e di
una purezza estreme, che permetteva di implementare in maniera assolutamente
trasparente l’export delle stampe nei più diversi formati, quali
i linguaggi HTML e PDF.
Ebbene,
alla prova dei fatti la realtà quotidiana dell’utilizzo di questo
prodotto si rivelò tristemente negativa. Trattandosi di uno strumento
multipiattaforma e Pure Java il suo tool visuale era interamente realizzato
in tale linguaggio, ed era di una lentezza e di una instabilità
tali da costringerci ben presto a metterlo da parte ed a scrivere direttamente
i file di definizione dei report in XML – buon per noi che avessimo scelto
un prodotto “aperto” in questo senso. Ma i guai non finirono qui: il modello
di reporting di ReportStyle Pro era infatti semanticamente incompleto,
e semplici operazioni come la definizione di un header o di un footer standard
in report multi-pagina non erano possibili, sia passando attraverso i template
XML sia realizzando le stampe avvalendosi delle API Java, e contatti diretti
con il servizio di supporto confermarono questa situazione grottesca. In
sostanza, ci trovavamo con un tool di reporting che non era in grado di
generare una fattura. Se i problemi fossero stati “solo” questi, avremmo
semplicemente potuto optare per un prodotto concorrente, ma alcuni altri
segnali ci spinsero a rivedere le nostre convinzioni in maniera più
radicale. In effetti persino le stampe più semplici si rivelavano
nella pratica di una esosità, in termini di risorse, e di una fragilità
disarmanti. Stampe di due, tre pagine al massimo provocavano l’invio allo
spooler di non meno di 20-30 MB di dati, sufficienti a mettere in ginocchio
il client da cui venivano eseguiti, mandando in crash il servizio di spooling
se non addirittura l’intera macchina. Quando ciò non accadeva il
tempo necessario per l’esecuzione della stampa era ovviamente così
elevato da indurre l’utente nella convinzione che, per qualche motivo,
la stampa non fosse stata accettata; lo stesso utente, quindi, provvedeva
solertemente a rilanciarla, con i risultati che i lettori potranno facilmente
immaginare. Purtroppo il responsabile di questa situazione non era ReportStyle
Pro, ma lo stesso JDK 1.2, che – in omaggio alla portabilità – si
trova costretto a gestire ogni stampa come un’immensa bitmap. Il nostro
tool di reporting forniva in realtà dei driver ottimizzati per un
certo insieme di stampanti, che riuscivano a ridurre della metà
le dimensioni delle richieste inviate allo spooler. Tuttavia essi non potevano
rappresentare in alcun modo una soluzione a questi problemi, dal momento
che, anche con il loro aiuto, una stampa di 10 pagine superava comunque
abbondantemente i 100 MB. Inoltre essi erano disponibili solo per un insieme
limitato di stampanti, e comunque solo per il sistema operativo Windows:
le workstation Linux avrebbero dovuto continuare ad usare i driver standard
del JDK.
In
sostanza quest’esperienza ci insegnò che tentare di trasporre in
Java la gestione delle stampe, eminentemente client-based, cui ci aveva
abituati la piattaforma Windows era semplicemente impossibile. Una nuova
fase di analisi e di design ci spinse ad identificare come un paradigma
più adatto al linguaggio di Sun quello delle stampe centralizzate,
non a caso già implementato da diversi produttori di tool di questo
genere. Per evitare brutte sorprese decidemmo di implementare in-house
un server di stampa di prova, basato – per guadagnare tempo - sul protocollo
RMI e limitato inizialmente alla gestione di stampe in modalità
testo. Tale scelta si rivelo sorprendentemente felice, e ci spinse a riprogettare
in tale ottica tutto il printing system della nostra applicazione. Oggi
Irmgard, secondo una prassi consolidata nel mondo Unix, genera internamente
le stampe in formato PostScript che invia in RMI al server di stampa, generalmente
una macchina Linux, la quale redirige tale stampa alla coda specificata:
il modello di stampante associato, se necessario, converte il codice PostScript
[2] nel linguaggio nativo della stampante, tipicamente il PCL – in ambito
Unix questo passo viene effettuato dal Ghostscript [3]. Questo approccio,
completamente rivoluzionario rispetto a quanto accade nel mondo Windows,
ha reso il printing system di Irmgard di una solidità, efficienza,
manutenibilità e sicurezza adamantine:
-
La configurazione
delle stampanti va eseguita in un unico punto di rete, e non più
replicata e tenuta aggiornata presso ogni client;
-
il sistema
di stampa di un server Unix è per sua natura altamente flessibile
e scalabile, e permette di associare delle code virtuali a dei gruppi di
stampanti – usualmente omogenee - gestendo così in maniera assolutamente
trasparente per Irmgard il bilanciamento del carico di lavoro tra un insieme
di dispositivi di stampa. Esso è inoltre in grado di gestire in
maniera intelligente, sempre senza richiedere passi espliciti da parte
di Irmgard, i periodi di inattività o di fermo forzato di tali dispositivi,
provvedendo a sospendere, trasferire e riavviare le stampe in funzione
della loro disponibilità;
-
il passaggio
di tutte le richieste di stampa attraverso uno snodo centrale consente
di effettuare un accounting ed un’archiviazione precisa e puntuale delle
stampe stesse, esigenza molto sentita ma non risolvibile in un ambito completamente
decentralizzato come quello di Windows. Ed è proprio questo sistema
di archiviazione centrale a risolvere brillantemente il problema della
consultazione on-line delle stampe: esse vengono memorizzate in uno o più
repository, che l’utente può in ogni momento consultare e visualizzare
comodamente dall’interno di Irmgard (la visualizzazione di file PostScript
è un altro dei servizi offerti dal programma GhostScript, già
menzionato in precedenza);
In
definitiva, io credo che ognuno di noi abbia sentito dire, più e
più volte, che “in Java le stampe non funzionano”. Ebbene, oggi
mi sento di poter dire che le cose non stanno veramente così. Semplicemente,
la verità è che in Java le stampe non funzionano come in
Windows.
Scenario di deployment
Dopo
mesi passati a discutere, modellare, sviluppare, testare ed ottimizzare,
giunse finalmente il momento di far uscire Irmgard dal proprio mondo ovattato.
Come si sarebbe comportato il nostro programma alla prova dei fatti ?
Penso
sia facilmente immaginabile l’atmosfera dei giorni immediatamente precedenti
il rilascio della prima beta: in quei momenti venne fuori, in maniera chiara
come mai era accaduto prima di allora, il carattere di ognuno di noi, ed
i pareri sul futuro e sul senso dei nostri sforzi, all’interno del mio
stesso team, erano ampiamente discordanti. Tuttavia il solo fatto che io
stia scrivendo questo articolo testimonia come, in realtà, Irmgard
abbia saputo dare fino ad ora ottima prova di sé.
In
un progetto innovativo come il nostro, ed in cui solo in minima parte potevamo
avvalerci di esperienze passate, decidemmo di curare particolarmente anche
gli scenari di deployment: l’approccio tipico nel mondo Windows, dove l’installazione
di un’applicazione costituisce un’altra applicazione a sé stante
– per la quale esistono diversi tool, dotati addirittura di specifici linguaggi
di scripting – era per noi assolutamente inaccettabile, dal momento che
trasforma la gestione degli aggiornamenti del software, in reti di dimensione
medio-grande, in un vero e proprio incubo. Dunque, nonostante il mercato
offrisse soluzioni di questo genere, in grado di realizzare procedure di
installazione multipiattaforma in Java efficienti, esteticamente gradevoli
e caratterizzate da una operatività assolutamente analoga a quella
cui siamo abituati sotto Windows – cito ad esempio Zero G InstallAnywhere
[4] ed InstallShield [5] – scartammo questa possibilità sin dall’inizio,
e stabilimmo i seguenti criteri per la nostra piattaforma di deployment:
-
Punto
di aggiornamento unico: esiste uno ed un solo repository per l’applicazione,
le sue librerie e tutti i suoi file di supporto. L’aggiornamento di Irmgard,
presso tutte le workstation, non deve richiedere nessun’altra operazione
oltre l’aggiornamento di detto repository; eventuali operazioni di post-configurazione
necessarie, sia presso i client che presso i server, devono venire eseguite
automaticamente, senza richiedere passi espliciti agli utenti. Tipicamente,
queste operazioni verranno eseguite alla prima esecuzione di una nuova
versione di Irmgard;
-
aggiornamenti
remoti incrementali: l’aggiornamento del programma richiede la modifica
del repository relativamente alle sole parti dell’applicazione o dei suoi
dati di supporto che abbiano subito delle effettive modifiche;
-
sicurezza
e stabilità dell’ambiente di esecuzione: per impedire agli utenti
di manomettere l’applicazione, o di comprometterne in qualsiasi modo il
corretto funzionamento, tutti i dati di controllo e di configurazione devono
essere protetti ed aggiornabili solamente dal personale autorizzato.
Questi
tre punti, che rappresentano un’assoluta utopia per chiunque sia uso operare
in ambienti Microsoft – e di questo si è resa conto la stessa casa
di Redmond, che ha a più riprese tentato di trovare un rimedio ai
limiti manifestati da Windows in questo senso, pur senza riuscire a trovare
soluzioni veramente efficaci per via della stessa impostazione del suo
sistema operativo – si sono rivelati al contrario perfettamente realizzabili
in Java. Si noti, en passant, che l’aggiornamento incrementale rappresentava
per noi un requisito fondamentale solamente in remoto: vale a dire, esso
doveva essere possibile dal sistema del cliente al nostro repository centrale,
così da permettere al cliente stesso di effettuare rapidamente aggiornamenti
via Internet, mentre non veniva richiesto per l’aggiornamento dal server
del cliente alle workstation dei suoi utenti. Infatti quest’ultimo passo,
verificandosi in un ambito di rete locale, non richiedeva una gestione
egualmente attenta della banda di trasmissione dati. In effetti, durante
le nostre accalorate discussioni sui fini e sulle modalità del deployment,
una parte del mio team giunse addirittura ad eliminare del tutto il concetto
di aggiornamento in locale dell’applicazione dal server alle workstation,
affermando che queste ultime avrebbero semplicemente potuto caricare dinamicamente
il programma dal server ad ogni esecuzione. Alla luce di questa osservazione,
ci trovammo in sostanza a dover valutare due soluzioni al nostro problema:
-
Deployment
server: queste applicazioni, tra le quali possiamo citare il tool di deployment
– ormai defunto – realizzato da Borland/Inprise per le sue precedenti versioni
di JBuilder ed il nuovissimo Web Start [6] di Sun, gestiscono un repository
archiviato in un server di rete. Ogni client che desideri eseguire un’applicazione
contatta il deployment server, il quale verifica che il client possegga
una copia aggiornata dell’applicazione e di tutti i componenti richiesti
al suo funzionamento. Se così è, il client esegue direttamente
la propria copia locale dell’applicazione, mentre nel caso contrario esso
riceve dal server la versione aggiornata del programma – limitatamente
alle sole componenti che necessitano effettivamente dell’aggiornamento
– la memorizza nella sua cache locale, e la esegue. Questo approccio, completamente
centralizzato, rende possibili una distribuzione ed un aggiornamento assolutamente
trasparenti delle applicazioni, e facilita l’esecuzione delle eventuali
procedure di post-configurazione, grazie alla presenza di un sistema “intelligente”
come il deployment server e di un vero e proprio dialogo con il client.
Si noti come in questo scenario persino il runtime Java possa venire considerato
alla stregua di un componente dell’applicazione, e quindi assoggettato
alla logica dell’aggiornamento automatico, e come ne consegua che sia possibile
associare a diverse applicazioni runtime Java specifici, gestendo nella
maniera più semplice quelle situazioni ingarbugliate che vengono
a crearsi in presenza di software realizzati in momenti differenti e sfruttando
ambienti Java non omogenei. Infine questa scelta permette di risolvere
con la massima naturalezza i problemi di sicurezza e stabilità,
dal momento che il deployment server può controllare la coerenza
dell’ambiente di esecuzione del client, forzandolo eventualmente ad un
aggiornamento delle procedure o dei dati di configurazione;
-
file server:
l’applicazione, e tutti i dati ad essa associati, sono esportati dal server
all’interno della rete locale, ed eseguiti in maniera trasparente dai client.
Quest’approccio, enormemente più semplice, comporta tuttavia un
tempo di caricamento delle classi Java e dei dati di configurazione maggiori,
dal momento che essi vanno sempre cercati in rete, e complica la gestione
delle procedure di post-configurazione e di controllo della sicurezza.
Venendo infatti a mancare il “componente attivo” rappresentato nella precedente
soluzione dal deployment server, tali procedure vanno forzatamente spostate
all’interno dell’applicazione.
La
soluzione basata su un deployment server, per quanto funzionalmente superiore,
presentava il non indifferente ostacolo dei tempi di codifica: infatti
nessuna delle soluzioni offerte dal mercato soddisfaceva i nostri bisogni.
D’altro canto tutti i test di usabilità che avevamo condotto sin
dalle primissime fasi di sviluppo della nostra applicazione, e che continuammo
sempre ad eseguire parallelamente al design ed alla codifica, ci avevano
fornito dei dati su cui meditare, e che riassumo brevemente:
-
Un normalissimo
Celeron a 366 MHz, con 64 MB Ram ed hard-disk EIDE (una macchina “standard”
al tempo del rilascio della prima beta di Irmgard) è in grado di
offrire un’esperienza di utilizzo più che accettabile;
-
la memoria,
più ancora che la CPU, mantiene soddisfacente la velocità
del programma anche in periodi di prolungato utilizzo (oltre 6 ore consecutive),
vero tallone d’achille di tutte le applicazioni basate su Java;
-
la velocità
di risposta di Irmgard si mantiene positiva anche installando un’unica
copia dell’applicazione, e di tutti i dati ad essa associati, presso un
server di rete;
-
di più,
le performance non decadono anche se le classi costituenti l’applicazione
ed il nostro framework vengono archiviate, nel server, non in un file .jar
ma in un albero di file .class.
Questi
fattori, e specialmente l’ultimo – che in sostanza risolve il problema
dell’aggiornamento remoto incrementale - ci convinsero ad utilizzare l’approccio
basato su file server per il deployment di Irmgard, perlomeno nelle fasi
iniziali. Arrivati ad un sufficiente punto di maturazione dell’applicazione
sarebbe stato possibile liberare una parte del team dal lavoro diretto
sul programma, ed assegnargli il compito di sviluppare il nostro deployment
server.
Per
la cronaca, i nostri server sono basati su Linux, ed esportano l’applicazione
Irmgard verso tutti i client di rete utilizzando il server Samba [7], soluzione
ideale in reti locali eterogenee, e che si fa preferire al classico NFS
sia dal punto di vista delle performance che della portabilità.
Infine,
non si può parlare di deployment senza citare le numerose implementazioni
di JVM disponibili, le cui performance sono state studiate ed analizzate
in maniera approfondita ed in funzione delle diverse casistiche di utilizzo.
Chi scrive, così come chi ha collaborato e collabora con lui al
progetto Irmgard, crede fermamente nel software open-source; logico quindi
che la scelta della JVM da utilizzare in questo progetto si sia effettuata
tra le implementazioni liberamente disponibili – vale a dire, prive di
qualsiasi costo di licensing o royalty – e prediligendo tra queste quelle
che si segnalavano per uniformità di comportamento nelle diverse
piattaforme, stabilità e velocità. Alla luce di queste considerazioni,
le implementazioni di JVM che ci hanno soddisfatto di più sono state
quelle di IBM e Sun. La scelta finale è ricaduta su Sun, semplicemente
perché delle due è l’unica ad avere implementato la versione
1.2 dell’ambiente Java sulle nostre due piattaforme di riferimento, vale
a dire Linux e Windows.
Il punto della
situazione
Concluso
lo studio dell’architettura della nostra applicazione non ci rimane che
discutere di alcuni particolari scenari di ottimizzazione del linguaggio
Java, e dare infine uno sguardo d’insieme a tutto il progetto Irmgard,
accennando a quelle che ne saranno le future linee di sviluppo: questi
punti verranno tratti nella terza, ed ultima, parte di questo articolo.
Bibliografia
[1]
- ReportStyle Pro: http://www.inetsoftcorp.com
[2]
- PostScript: http://www.cs.indiana.edu/docproject/programming/postscript/postscript.html,
http://www.tronche.com/web-directory/science-and-technology/computer/languages/postscript
[3]
- Ghostscript: http://www.cs.wisc.edu/~ghost
[4]
- Zero G InstallAnywhere: http://www.zerog.com/products/products.html
[5]
- InstallShield: http://www.installshield.com/java
[6]
- Web Start: http://java.sun.com/products/javawebstart
[7]
- Samba: http://www.samba.org
Lavinio
Cerquetti si occupa di modellazione e sviluppo del software (prevalentemente
in Java, C/C++ e Delphi) in ambienti Windows e Unix, concentrandosi sulla
programmazione distribuita e di rete. Può essere contattato all’indirizzo
di e-mail lavinio@poboxes.com
|