In questo articolo vediamo come realizzare un proxy generico utilizzando i concetti esposti in articoli precedenti riguardanti il disaccoppiamento tra classi (che consiglio di rileggere attentamente).
Pensiamo ad un applicativo web in Java capace di interfacciarsi lato back-end a numerosi EJB sfruttando un proxy per le chiamate. Avremo un form che invierà i dati (dopo il submit) ad una classe Delegate
che effettua la chiamata in remoto all'EJB, restituendo un output lato view. Insomma la classica modalità di interazione client/server con il proxy che riveste il ruolo di intermediario.
Interzione con il proxy e problemi di dominio
L'idea iniziale è quella di incorporare al suo interno i riferimenti (cablati) agli ejb secondo questa modalità:
public class MyProxy {
private EJBService1RemoteInterface service1;
private EJBService2RemoteInterface service1;
private EJBService2RemoteInterface service1;
..
}
Il ciclo di sviluppo di un software senza linee guida documentate porta spesso ad una forma di "anarchia" in termini di implementazione, ciò significa che in termini di codice potremmo avere diverse variazioni:
MyProxy
restituisce al chiamante il riferimento all'interfaccia remota dell'ejb;MyProxy
espone dei metodi specifici dell'ejb comportandosi quindi da Adapter piuttosto che da proxy;
Tenendo conto in particolare del secondo punto, MyProxy
ha diversi fattori da considerare come per esempio un forte accoppiamento con gli ejb da richiamare. Se si parla infatti di un'interfaccia e di utilizzare l'Inversion Of Control via Dependency Iniection per gli ejb 3.0, essa apporta sicuramente dei miglioramenti, ma c'è un fattore da considerare: MyProxy è comunque accoppiato ad una entità interfaccia quindi è "responsabile" della sua gestione. Motivo per cui, dovrà fornire come minimo l'interfaccia al chiamante.
Ora supponiamo che MyProxy
sia un componente condiviso, una situazione sicuramente "reale" non riferita a Pattern Singleton ma alla possibilità di utilizzare MyProxy
da parte di più chiamanti:
In questo schema di massima consideriamo che MyProxy "collezioni" riferimenti a servizi remoti (in questo caso ejb) indipendentemente dal chiamante, per poter essere utilizzato da qualsiasi chiamante che dovrà impiegare un servizio (in istanza 1:1 come nel grafico o in modalità Singleton). Il ruolo di MyProxy
è quello di intermediario, quindi finirà prima o poi per collezionare tutti i riferimenti agli ejb inducendo in maniera implicitia un problema di "dominio", dove un client che utilizza Myproxy
per richiamare un ejb caricherà dalla classe di MyProxy
tutti gli ejb che internamente dichiara.
DynamicProxy come soluzione ai problemi di dominio
Quello precedentemente proposto è in effetti un paradosso: l'OOP promuove come strumento l'interfaccia per il disaccoppiamento ma in questo caso non funziona; ad esempio, nell'adozione di MyProxy per utilizzare un ejb, si fa riferimento a questi attraverso l'interfaccia remota via Myproxy
che significa "interfaccia" quindi disaccoppiamento. Ma cosa accadrebbe se, ad un certo punto, il team di sviluppo dovesse apportare modifiche alla firma di un metodo di un loro ejb? MyProxy dovrà essere modificato, ecco il riferimento al problema di "dominio".
Relativamente a quanto appena detto, le domande da porsi dovrebbero essere le seguenti:
- perché MyProxy deve conoscere il servizio (e la sua tipologia) da richiamare?
- Perché deve incapsulare al suo interno dei riferimenti ai servizi da richiamare? (Cosa che porta al meccanismo di "conoscenza" cioè il ciclo di vita nascita-morte tra MyProxy e il servizio è fortemente legato).
- Perché deve caricare servizi che potrebbero non essere utilizzati?
La soluzione proposta si chiama DynamicProxy, essa infatti dovrebbe garantire:
- nessun riferimento specifico contenuto;
- servizi caricati in Lazy-Loading (Run-Time);
- problema dell'invasione di dominio inesistente;
- disaccoppiamento reale ed effettivo;
- altissima scalabilità senza nessun impatto in termini di codice per ogni servizio aggiunto;
- disaccoppiamento effettivo: DynamicProxy infatti non conosce la natura del servizio da richiamare.
Esiste quindi un modo per generalizzare un servizio indipendentemente dalla sua natura (ejb, MDBean, classe Java ecc.)? Cioè, è possibile "vederlo" esternamente sempre nella stessa forma indipendentemente dalla tipologia, dal numero di metodi e di firme implementate? Lo vedremo nella seconda parte dell'articolo.
Generalizzazione dei servizi e perdita della specificità
Per proporre un esempio pratico su quanto detto fino ad ora è possibile creare due classi base generiche per l'input e l'ouput di qualsiasi metodo:
public class InputContainer implements Serializable {
private static final long serialVersionUID = 1L;
private ArrayList<Object> paramsIn;
// Constructor
public InputContainer() {
super();
paramsIn = new ArrayList<Object>();
}
// insert Param
public void insert(Object value) {
paramsIn.add(value);
}
// get Param
public Object getParam(int position) {
return paramsIn.get(position);
}
public ArrayList<Object> getParamsIn() {
return paramsIn;
}
public void setParamsIn(ArrayList<Object> paramsIn) {
this.paramsIn = paramsIn;
}
}
E per l'output:
public class OutputContainer implements Serializable {
private static final long serialVersionUID = 1L;
private Object outputParam;
public OutputContainer() {
super();
}
public Object getOutputParam() {
return outputParam;
}
public void setOutputParam(Object outputparam) {
this.outputParam = outputparam;
}
}
Le due classi funzionano da wrapper e InputContainer
conterrà i parametri di input da passare al metodo che li recupererà in maniera posizionale, cioè se il metodo normalmente avesse la firma foo(String p1, int p2)
allora p1 ha posizione 0 mentre p2 posizione 1; sempre InputContainer colleziona i parametri in un ArrayList di Object quindi non è accoppiato alla loro tipologia. OutputContainer
restituisce invece un unico valore che è un Object; entrambi però hanno metodi getteser/setter (metodi Helper) e, ovviamente, implementano java.io.Serializable
.
Proseguendo, potremmo creare la seguente interfaccia dove il parametro activity
è in sostanza il metodo da eseguire:
public interface GenericServiceInterface {
public String getServiceName();
public OutputContainer execute(String activity, InputContainer paramsIn);
}
L'interfaccia proposta potrà quindi essere implementata attraverso il seguente servizio:
public class ServiceA implements GenericServiceInterface {
protected Hashtable msgOut = new Hashtable();
public String serviceName = null;
public ServiceA() {
super();
}
// ######## BUSINESS METHODS
private String getMessage1() {
return "message 1 from ServiceA";
}
private String getChangedMessage2(String msg2) {
return "message 2 from ServiceA " + msg2;
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
// ########### SELECTOR METHOD DISPATCHER
@Override
public Hashtable execute(String activity, Hashtable paramsIn) {
System.out.println("called execution method " + activity);
if (activity.equals("getMessage1")) {
msgOut.put("paramOut1", getMessage1());
}
else
if (activity.equals("getChangedMessage2")) {
msgOut.put("paramOut1", getChangedMessage2((String)paramsIn.get("paramIn1")));
}
return msgOut;
}
}
Ogni metodo viene eseguito fornendo una stringa che ne rappresenta l'identità unito alle due classi container di Input ed Output, quindi, per eseguire ad esempio il metodo getMessage1
basterà richiamare il metodo execute
passandogli la stringa getMessage1
e i due container di I/O con eventuale contenuto se l'activity accetta dei parametri di input.
Verrà così perduta l'identità dell'oggetto tramite un approccio basato sul wrapping generico funzionale, ma saranno consentite ulteriori implementazioni che analizzeremo nel prossimo capitolo.
Il DynamicProxy
Per introdurre il discorso relativo al DynamicProxy si potrà creare la seguente interfaccia:
public interface ProxyInterface {
public OutputContainer execute(String serviceName, String activity, InputContainer paramsIn);
}
Questo sarà invece il DynamicProxy:
ublic class DynamicProxy implements ProxyInterface {
private Properties servicePropertiesFile = new Properties();
public DynamicProxy() {
super();
init();
}
// inizializza il delegate
private void init() {
try {
// carica il file di properties
servicePropertiesFile.load(new FileInputStream("./services.properties"));
} catch (IOException ex) {
ex.printStackTrace();
}
}
// ################## dato un servizio esegue una activity
@Override
public OutputContainer execute(String serviceName, String activity,InputContainer paramsIn) {
GenericServiceInterface service = null;
String serviceClassname = servicePropertiesFile.getProperty(serviceName);
if (serviceClassname!=null) {
try {
Class serviceClass = Class.forName(serviceClassname);
// lo recupera via interfaccia
service = (GenericServiceInterface)serviceClass.newInstance(); return service.execute(activity, paramsIn);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
} else {
service = (GenericServiceInterface) serviceCache.get(serviceName);
return service.execute(activity, paramsIn);
}
return null;
}
}
DynamicProxy carica nel costruttore il nome dei servizi da un file di properties:
servicePropertiesFile.load(new FileInputStream("./services.properties"));
Questo file contiene il nome dei servizi associati al nome qualificato della classe:
serviceA=common.delegate.ServiceA
serviceB=common.delegate.ServiceB
Nel codice in allegato troverete anche una classe da utilizzare al posto del file di properties (ServiceNames) al lettore la scelta di cablare o meno nel codice i nomi dei servizi; la parte core è invece la seguente:
Class serviceClass = Class.forName(serviceClassname);
// lo recupera via interfaccia
service = (GenericServiceInterface)serviceClass.newInstance();
return service.execute(activity, paramsIn);
In essa si utilizza il meccanismo di Reflection di Java per istanziare il servizio come GenericServiceInterface
e quindi anche l'oggetto senza conoscerne l'implementazione; il polimorfismo verrà applicato a Run-Time. Sarà quindi buona norma analizzare il codice in allegato in modo da capire anche il meccanismo di caching (qui asssente) che risulta comunque semplice utilizzando un hashtable dove memorizza la coppia "nome servizio, istanza".
Nella classe Test modifichiamo il metodo main()
in questo modo (da notare la doppia chiamata a getMessage1
):
public static void main(String[] args) {
DynamicProxy proxy = new DynamicProxy();
InputContainer ic = new InputContainer();
OutputContainer oc = proxy.execute("serviceA", "getMessage1", ic);
System.out.println(oc.getOutputParam());
proxy.execute("serviceA", "getMessage1", ic);
System.out.println(oc.getOutputParam());
}
Questo sarà invece il metodo main()
per richiamare anche il secondo metodo activity:
public static void main(String[] args) {
DynamicProxy proxy = new DynamicProxy();
InputContainer ic = new InputContainer();
OutputContainer oc = proxy.execute("serviceA", "getMessage1", ic);
System.out.println(oc.getOutputParam());
// chiamo il metodo
ic.insert("parametro input");
oc = proxy.execute("serviceA", "getChangedMessage2", ic);
System.out.println(oc.getOutputParam());
}
Creando un nuovo servizio DynamicProxy non verrà assolutamente modificato, il codice come numero di righe rimarrà sempre lo stesso; DynamicProxy non si chiederà se state sviluppando un servizio EJB o Message Driven Bean, più semplicemente utilizzerà una interfaccia per accedere ad un servizio e sarà disaccoppiato rispetto ai servizi a livello di firma di metodo.
Chi utilizza DynamicProxy è a sua volta non accoppiato a questo utilizzando l'interfaccia che implementa, volendo potremo prevedere diverse versioni: una che utilizza il caching e l'altra no; viene richiesto uno sforzo aggiuntivo per il dispatching interno dei metodi e la gestione dei parametri di I/O tramite classi contenitore prevedendo anche il Casting Locale.
Alcune considerazioni finali
Se pensiamo di fare implementare al proxy generico e ai servizi la medesima interfaccia, allora potremo anche garantire l'interscambiabilità dei componenti proxy/servizi; un client potrà utilizzare nello stesso modo il servizio tramite delegate (indirettamente) o il servizio in maniera diretta (senza delegate) con nessun tipo di accoppiamento tra i componenti, quindi con delegate e servizio interscambiabili senza nessuna modifica.
Come rendere fattibile tutto questo visto che l'interfaccia non è la medesima? Ad esempio con un'interfaccia come la seguente dove fullQualifiedActivity
sarà un valore stringa di tipo notazione ("mioservizio.activityscelta").
public interface GenericServiceInterface {
public String getServiceName();
public OutputContainer execute(String fullQualifiedActivity, InputContainer paramsIn);
}
Con una stringa individueremo sia il servizio che l'activity, basterà spacchettare la stringa in due parti magari implementando un metodo in una superclasse estesa dai servizi (la superclasse implementerà ovviamente anche l'interfaccia). Diversi client possono utilizzare una loro istanza del proxy e centralizzare alcuni aspetti elaborativi per renderli disponibili ai client, ad esempio, il log per la chiamata di un servizio verrà scritto una volta sola nel proxy e sarà disponibile a tutti i client.