Ant, la formica operosa che beve caffè

III parte: meccanismi di ereditarietà tra build filedi

Dopo aver visto il mese scorso un esempio di build file con le operazioni più comuni per i progetti JavaEE, proseguiamo in questo articolo parlando di come affrontare la build dei progetti di grandi dimensioni. In particolare verrà mostrato un esempio di come realizzare meccanismi di ereditarietà tra build file per facilitare la manutenibilità ed estendibilità della build con ant nei contesti "enterprise".

Monolithic buildfile vs Cascading buildfile

Quando si ha a che fare con la build di progetti di grandi dimensioni composti da diversi sottoprogetti si possono seguire due strade.

La prima considera il progetto come un blocco monolitico e prevede l‘uso di un solo buildfile (monolithic buidfile) per eseguire tutta la build. Il file di build unico definisce tutti i target necessari ai vari sottoprogetti, permettendo di centralizzare la gestione delle loro dipendenze e allo stesso tempo consentendo la definizione per ciascun sottoprogetto di target specifici per le operazioni di packaging e di deployment.

Questa soluzione facilita le attività  di manutenzione sul build file, ma comporta un forte accoppiamento in quanto ogni volta si effettua la build di tutti i moduli.

Per questo motivo l‘uso di un monolithic buidfile è consigliabile nei casi in cui i progetti sono fortemente correlati e presentano complesse dipendenze.

L‘altra strada consiste nel ricorrere a un set di build file (cascading buildfile), uno per ogni sottoprogetto. Si ribalta la prospettiva rispetto a prima, ovvero è possibile effettuare la build di una sola applicazione, senza preoccuparsi delle altre.

Il design prevede, infatti, un master build file nel direttorio radice che invoca i target dei vari build presenti nelle sottodirectory: gli sviluppatori che vogliono fare la build di un singolo progetto si limitano ad usare il build file nella directory relativa.

Questo tipo di soluzione è, quindi, preferibile per i grandi progetti ben definiti e composti da una serie di moduli a sà© stanti.

Approccio tradizionale

La build strutturata con cascading build file è molto diffusa nei progetti e nei framework del mondo Java. Bisogna precisare, tuttavia, che tale approccio deriva dai grandi progetti Unix, primo fra tutti il kernel di Linux, che è un esempio di cascading build realizzata con dei make file.

La tecnica consiste nello strutturare i sottoprogetti in una serie di directory, definendo un master build file nella directory radice che invoca gli altri usando il task .

Di seguito è riportato un esempio di master build file che esegue la build di 5 sottoprogetti:










In pratica nel master sono definiti dei target "proxy" che modellano le dipendenze nei vari sottoprogetti.

Ciò produce l‘indubbio vantaggio di poter riusare dei build file contenenti target per operazioni standard (si pensi ad esempio alle operazioni del ciclo di sviluppo compile, build, dist, deploy), consentendo la definizione di librerie di build file.

Un‘altra importante funzionalità  offerta dal task è il controllo da parte del master build file delle proporties dei build file contenuti nei vari sottoprogetti. Ciò è possibile con l‘attributo inheritall:

  • se inheritall vale true, tutte le properties impostate nel master build file sono ereditate dai file di build dei sottoprogetti;
  • se inheritall vale true e ci sono delle properties ridefinite nel task , queste hanno la meglio su quelle del master build file;
  • se inheritall vale false, solo eventuali properties definite nel task del master build file sono ereditate dai file di build dei sottoprogetti;
  • indipendentemente dal valore di inheritall, le proprietà  definite dalla riga di comando (p.e. ant â??Dnome.proprietà ="..." nomeTask) vengono sempre passate ai sottoprogetti e non possono essere sovrascritte dalle properties definite nel task .

Ã? importante sottolineare che il valore di default dell‘attributo inheritall è true. Quindi i sottoprogetti devono usare nomi univoci per le proprietà , altrimenti il comportamento di default prevede, in caso di omonimie, l‘overriding con i valori definiti nel master build file.

Un simile discorso vale per i riferimenti ai percorsi in un progetto. Un build file master può definire attraverso il task un classpath di esecuzione o compilazione e, se l‘attributo del task inheritRefs vale true, i riferimenti a questi path vengono ereditati dai sottoprogetti.

In questo caso però il valore di default di inheritRefs è false, per cui a meno di una sua definizione a true nel task , i sottoprogetti non si ritrovano i valori dei path settati a quelli del master build.

L‘overriding delle properties e la possibilità  di definire delle librerie di build file rendono i cascading build file estremamente flessibili, ma problematici dal punto di vista dell‘estendibilità  e della manutenibilità .

Una modifica sulle operazioni di processo, come ad esempio l‘inserimento di target per il controllo della qualità  del codice, o semplicemente la modifica dell‘ordine di precedenza, si traduce, infatti nella modifica del build file di ciascun progetto.

Ereditarietà  tra buildfile

La comparsa del task , disponibile a partire dalla versione 1.6, ha introdotto in ant l‘overriding a livello di task. Questa funzionalità  è molto importante e adeguatemente sfruttata offre la possibilità  di strutturare una build "object oriented", che risponda alle esigenze di flessibilità  e manuntenibilità  dei grandi progetti.

Con il task overriding si può, infatti, centralizzare tutta la gestione del processo all‘interno di un unico file, diverso dal build file di progetto: una scelta di questo tipo aumenta la mantenibilità  del build file, garantisce che tutti i progetti seguano le stesse politiche di processo, e crea una separazione netta tra le operazioni di carattere generale e quelle specifiche per i diversi progetti.

Da un punto di vista implementativo, la centralizzazione di tali operazioni è, inoltre, spesso dettata dalla similitudine delle varie applicazioni in termini infrastrutturali, tecnologici e architetturali: è possibile, infatti, definire un insieme di operazioni "generico", in cui la valorizzazione di opportuni parametri permette la specializzazione rispetto ad uno specifico progetto.

La soluzione che si vuole proporre prevede la definizione di un file "generico", chiamato MainBuild.xml, in cui sono definiti i target che implementano le diverse fasi del processo e le loro rispettive dipendenze. Questo file generico viene "specializzato" da un file specifico per il progetto, che avrà  il classico nome Build.xml.

Figura 1 - Struttura dei build file

Con una struttura di questo tipo si centralizza la definizione delle operazioni inerenti le fasi del processo all‘interno di un unico file, il MainBuild.xml, facilitandone l‘estensibilità  e la mantenibilità . Qualora si decidesse di cambiare le fasi di processo, ad esempio aggiungendo dei controlli aggiuntivi sulla qualità  del codice o imponendo un certo livello di documentazione, si dovrà  modificare un unico file per essere certi che tutti i progetti rispettino le nuove regole.

Attraverso il meccanismo di import, tutti i target definiti nel file importato (MainBuild.xml) sono disponibili all‘interno del file importante (Build.xml): in questo modo è possibile invocare i target come se fossero dichiarati localmente, evitando il riferimento esplicito come nel task . In più, il meccanismo di import permette di ridefinire nel file importante il contenuto dei target del file importato: i target ridefiniti sono disponibili per l‘invocazione sia esplicita da parte dell‘utente, sia implicita da parte di altri target ant. Le ridefinizione è particolarmente utile per estendere le funzionalità  di un target già  definito.

Si consideri ad esempio il seguente target definito nel MainBuild.xml:


... funzionalità  generali ...

Un esempio di codice per una estensione funzionale in un Build.xml è il seguente:

... task aggiuntivi ...

In questo modo, il target ridefinito avrà  tutte le funzionalità  del target precedente, che verrà  invocato dalla chiamata nel vincolo di dipendenza, ed in più eseguirà  tutti i task aggiuntivi inseriti nella nuova definizione. Cosଠfacendo si mantiene la separazione tra la definizione delle funzionalità  generali e funzionalità  specifiche, mantenendo l‘interfaccia comune.

Da un punto di vista implementativo tale tecnica si concretizza nella modifica di uno scheletro del file Build.xml già  opportunamente predisposto.

Le modifiche necessarie sono di tre tipi:

  • Assegnamento del corretto valore alle properties già  definite.
  • Definizione delle properties e dei target specifici per il progetto, che non possono essere definiti nel build file generico.
  • Estensione, tramite ridefinizione, di target già  definiti in MainBuild.xml.

Ovviamente le operazioni di modifica e customizzazione devono essere effettuate con una certa attenzione: la parametrizzazione deve riguardare solo il valore delle properties, e ovviamente non il nome, pena il mancato funzionamento della struttura.

Di seguito è mostrato un frammento di un Build.xml che segue questo tipo di design:

...


depends="MainBuild.init">






overwrite="true"/>
overwrite="true"/>



...

Conclusioni

Nell‘articolo si è parlato di come usare ant per affrontare la build dei progetti di grandi dimensioni. Partendo dalla soluzione tradizionale (cascading build file), che si appoggia al task , si è presentata l‘implementazione di un meccanismo di ereditarietà  tra file di build, basato sul task introdotto con la versione 1.6 di ant.

La soluzione proposta prevede un MainBuild.xml che centralizza le operazioni di processo e una serie di Build.xml che lo specializzano, aggiungendo le funzionalità  specifiche dei vari sottoprogetti.

Riferimenti

[ANT] Sito ufficiale del progetto Jakarta Ant
http://jakarta.apache.org/ant

[ADG] Jessy Tilly - Eric Burke, "Ant Definitive Guide", Ed. O‘Reilly

[JDA] Eric Hatcher - Steve Loughran, "Java Development with Ant", Ed. Manning

Condividi

Pubblicato nel numero
116 marzo 2007
Amedeo Cannone si è laureato in Ingegneria Informatica presso l‘università degli studi di Bologna. Dal 2003 lavora per il Gruppo Imola per cui svolge attività di consulenza su tematiche architetturali e di processo. Al momento sta seguendo alcuni progetti di integrazione SOA e si sta interessando di ESB e JBI.…
Articoli nella stessa serie
Ti potrebbe interessare anche