Articoli Manifesto Tools Links Canali Libri Contatti ?
Programmazione

Creare prototipi con Novocaine

Abstract
Nel mondo di oggi è sempre più frequente la necessità di sviluppare rapidamente prototipi per demo. Vediamo come è possibile farlo usando il buon vecchio (e ingessato) Java.
Data di stesura: 23/07/2005
Data di pubblicazione: 29/07/2005
Ultima modifica: 04/04/2006
di Giovanni Giorgi Discuti sul forum   Stampa

Uno dei problemi tipici quando si ha a che fare con progetti di grande respiro (oltre 180000 linee di codice java) è tenere sempre sotto controllo quello che si fa.

Quando il progetto diventa grande, anche l'analista ha difficoltà a ricordare il perché di determinate scelte. In qualità di software architect in progetti del genere, mi sono trovato più di una volta nella situazione di dover ridurre i tempi morti, e massimizzare il lavoro degli sviluppatori, senza inutili frustrazioni.
Difatti nelle prime fasi di progetto i ripensamenti sono frequenti, e quindi gli sviluppatori più abili e veloci si vedono costretti a reimplementare sempre tutto da zero, poiché le specifiche cambiano sotto i loro occhi in modo subitaneo.
Questo scoraggia il team, e anche gli analisti che si sentono spinti a fare meno iterazioni, anticipando cose che non sono loro chiare, per tentare di massimizzare il lavoro degli sviluppatori.
Inutile dire che quest approccio è alla lunga pericoloso (anche se funziona e alza i costi).
Oltre a ciò spesso i progetti di queste dimensioni richiedono un database di test non banale, e scriverlo al più presto serve a dare a tutti un "linguaggio comune" su cui basarsi: ma chi aveva tempo da perdere in una cosa del genere?

Novocaine nasce quindi da questa esperienza, ed è un tool che consente di scrivere prototipi semi funzionanti a partire da interfacce java pure.
Con Novocaine, l'analista dà allo sviluppatore un set di interfacce di analisi. È poi sufficente dare in pasto queste interfacce a novocaine.

Piccoli esempi

Nel codice incluso in novocaine [1] sono presenti diversi esempi. Partiamo dal più semplice, contenuto nella classe NProxySimpleExample:
  1. /** Esempio di semplice uso del novocaine proxy 
  2.  * Abbiamo 2 istanze cocnrete PrinterImpl1 e PrinterImpl2  
  3.  */ 
  4. // Wrap the concrete 
  5. ArrayList instances=new ArrayList(); 
  6. instances.add(PrinterImpl1.class); 
  7. instances.add(PrinterImpl2.class); 
  8.  
  9. InvocationHandler handler = new NProxy( instances); 
  10. Printer f = (Printer)  
  11.   NProxy.bless(new Class[] { Printer.class, ProxyServices.class  }, 
  12.         handler);    
  13.  
  14. // Esempio di uso del proxy:     
  15. f.print("AAA"); 
  16. f.setPrinterName("pipz"); 
Nell'esempio vi sono 3 entità in gioco: l'interfaccia Printer, che espone parecchi metodi tra cui print() e setPrinterName(). Abbiamo poi due implementazioni parziali (PrinterImpl1 e PrinterImpl2) la prima delle quali implementa print mentre la seconda implementa il setPrinterName().
Per prima cosa creiamo un Novocaine Proxy (NProxy) passandogli quello che abbiamo, cioè una lista delle implementazioni parziali già fatte. L'NProxy allocherà una istanza per ognuna delle classi nella lista, e le terrà al suo interno come riferimento. L'istruzione
  1. NProxy.bless(new Class[] { Printer.class, ProxyServices.class  }, 
  2.     handler); 
crea un proxy che è in grado di rispondere all'interfaccia Printer, ed anche a qualcosa in più. Da questo momento il codice chiamante ha in mano un proxy che ridirigerà le chiamate alla implementazione più corretta. Per esempio, quando Java esegue la linea setPrinterName, l'NProxy si accorge che PrinterImpl1 non può rispondere al messaggio e prova ad inviarlo a PrinterImpl2.
Se il metodo non è presente, NProxy cerca di ignorarlo, ritornando se necessario un particolare proxy ('EaterProxy') che ignora a sua volta ogni richiesta. Per es:
  1. // Esempio di metodo non completo che pero' non da' errore: 
  2. Printer eater=f.nobodyHasMe(); 
  3. eater.print("eat me"); 
È facile pensare ad un proxy che capisca al volo i setter e getter di un metodo java e li "simuli" senza bisogno che noi li implementiamo. L'esempio DBExample mostra questo, e si spinge oltre: memorizza questi oggetti in un semplice database in memoria. Nell'esempio, la classe Person.class non è mai implementata! L'idea è quella di creare una sottoclasse di NProxy di nome DBProxy. DBProxy è in grado di lavorare senza implementazioni, e tenta di capire cosa deve fare dalla segnatura del metodo. Per es con il seg frammento di codice (preso da DBProxy) gestiamo i getter e setter:
  1. protected Object doRemoteCall(Method method, Object[] args) { 
  2.   // Hum hum 
  3.   String name=method.getName(); 
  4.   if(name.startsWith("get")|| name.startsWith("set")){ 
  5.     return processGetterAndSetters(method, args, name); 
  6.   }else{        
  7.     // Try to see if it is equals: 
  8.     if(name.equals("equals")){ 
  9.       // You can try this: 
  10.       DBObject objToCompare=(DBObject) args[0]; 
  11.       return new Boolean( 
  12.           (getNakedProperty(SimpleDB.OID_SPECIAL_PROPERTY)).equals(objToCompare.getId())); 
  13.     }else{ 
  14.       System.out.println("WARN "+this+" FAILED TO PROCESS:"+method); 
  15.       return EaterProxy.getEater(method); 
A questo punto lo sviluppatore è in grado di scrivere un insieme di dati di test, e di memorizzarli in un database demo senza scrivere una sola linea di implementazione delle interfacce!

Come secondo passo, il responsabile di design può iniziare a scrivere uno use case, usando i metodi delle interfacce. A runtime Novocaine è in grado di implementare da solo i setter e i getter delle interfacce, e di ignorare tutti i metodi non ancora definiti.

A questo punto è possibile suddividere il lavoro su un insieme anche vasto di sviluppatori, che possono iniziare a scrivere una implementazione parziale delle interfacce (tipicamente dei soli metodi di business).
Al termine di questa prima iterazione il sistema è funzionante (anche se solo in parte) e il db serializzabile.
L'analista può quindi vedere il lavoro fatto, e interagire attraverso un linguaggio di scripting (come per es BeanShell).

Quanto prodotto è tranquillamente vendibile come "demo" ad un cliente, ed in effetti fa il lavoro in modo parziale ma definito.
Quando tutti i metodi di business sono implementati, è sufficiente sostituire ai proxy virtuali di novocaine le implementazioni reali, e tutto funzionerà a dovere.

Tecnologia elegante

Per funzionare novocaine si serve della interfaccia proxy di java, e funziona soltanto con interfacce pure. Come vedremo è possibile estenderlo in ottica OOP.
In novocaine un NProxy è un InvocationHandler java, a cui sono assegnate k implementazioni chiamate ImplementationCluster (ove k può anche valere zero). All'NProxy è assegnata anche una interfaccia, che è tenuto a rispettare.
Quando si chiede di eseguire il metodo X all'NProxy, esso cerca un'istanza nell'Implementation cluster che possa rispondere. Se non ne trova nessuna prova a vedere se l'interfaccia è dichiarata come "Remotizzabile" e in tal caso chiama il metodo doRemoteCall(..). Tale metodo in NProxy non fa nulla e "ingoia" il messaggio, ma le sottoclassi di NProxy possono ridefinirlo. Il database ad oggetti è solo un esempio embrionale delle potenzialità nascoste di Novocaine.
Nel database ogni nuovo oggetto-interfaccia ha uno speciale NProxy con k=0. Questo speciale proxy però ha una tabella chiave/valore dove memorizza i valori dei setter/getter che vengono invocati.
Quando il db viene salvato, vengono serializzate queste istanze con la tabella chiave/valore.
È possibile togliere/aggiungere campi e migrare il vecchio db con poco sforzo: infatti la struttura di memorizzazione è "flat". Si si accede ai dati con delle interfacce, quindi è sempre possibile tracciare con un IDE come e dove si usa il particolare metodo InterfacciaLetale.getValorePericoloso()
Poiché non è consentito mischiare classi e interfaccie in modo arbirario, il modello è elegante e ben controllabile.
Esistendo la tipizzazione, è ben definito chi usa cosa e come, quindi si evitano i tipici problemi di coordinamento di linguaggi di scripting troppo laschi da questo punto di vista.

Estensioni

È possibile estendere i proxy per supportare il concetto di MockObject
È possibile creare il concetto di mixim, mischiando k implementazioni parziali in una sola interfaccia completa.
In teoria, rispetto a linguaggi dinamici come SmallTalk o Self è possibile implementare proxy ad hoc per ogni istanza, migliorando le performance del sistema in modo significativo.
È anche possibile implementare il concetto di invio parallelo e dinamico dello stesso messaggio ad un cluster di implemenetazioni: in questo modo novocaine può scalare linearmente.
Sempre su questa linea,è concepibile che ogni implementazione proxata sia eseguita da un processore differente, consentendo un livello di scalabilità che arriva fino alla singola istanza. Quest'ultima area di sviluppo è completamente sperimentale, ma promette di dare i suoi frutti se adeguatamente studiata.

Informazioni sull'autore

Giovanni Giorgi, classe 1974. Dopo il diploma di liceo Classico, si è laureato in Informatica nel febbraio 2000, e attualmente lavora nel campo del software finanziario (trading on line, soluzioni web).
Appassionato di linguaggi di programmazione, si interessa anche di politica e letteratura.

È possibile consultare l'elenco degli articoli scritti da Giovanni Giorgi.

Altri articoli sul tema Programmazione.

Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.