Untitled Document
   
 
Esaminiamo la validità di IOC (Inversion of Control)
di Sony Mathew, traduzione di Francesco Saliola

L'Inversion of Control (IOC, "inversione di controllo") con l'avvio delle dipendenze, conosciuto anche come Injection IOC non è mai stato un pattern molto facile da usare per il progettista o il programmatore. Molti mettono in dubbio la sua validità, e quella dell'IOC in generale. L'inversione di controllo sembra essere una contraddizione al concetto fondamentale dell'incapsulamento degli oggetti. Il Context IOC può rappresentare una maniera nuova per tentare di racchiudere l'Inversion of Control sotto forma di design pattern puro, dimostrare così che l'IOC è un concetto molto potente.

 

Panoramica su IOC
Inversion of Control (IOC) è un pattern nuovo che ha guadagnato popolarità di recente. Tale pattern è basato su pochi semplici concetti che producono un codice base fortemente disaccoppiato, leggero, mobile e adatto per essere sottoposto ai test di unità. I concetti cruciali alla base dell'inversione di controllo consistono nella esposizione da parte di un oggetto delle proprie dipendenze attraverso un qualche contratto. Le dipendenze comprendono entità quali implementazioni di oggetti, risorse del sistema e così via: sostanzialmente tutto ciò di cui un oggetto necessita per eseguire la sua funzione designata ma che non riguarda direttamente la sua implementazione. In un grafo di oggetti annidati, ciascun oggetto nella catena di chiamata espone le sue dipendenze all'oggetto chiamante al suo esterno che lo usa, e quest'ultimo, a sua volta, espone queste dipendenze -- comprese anche le proprie -- al suo oggetto invocante e così via, finché tutte le dipendenze si saranno manifestate in cima alla gerarchia di annidamento. L'oggetto al top level della gerarchia assembla poi il grafo di dipendenza prima di attivare gli oggetti. Tale oggetto in cima all'annidamento è di solito un punto di entrata nel proprio sistema, come il main di un'applicazione, una Servlet o anche un EJB.

 

Validità di IOC
IOC sembra fare a pugni con il paradigma fondamentale dell'incapsulamento degli oggetti. Il concetto di propagazione verso l'alto delle dipendenze è effettivamente una contraddizione rispetto all'incapsulamento. L'incapsulamento imporrebbe che un oggetto chiamante non sappia nulla dell'oggetto helper e del modo in cui esso funziona. Quello di cui ha bisogno e si occupa l'oggetto helper è solamente di svolgere la propria funzione, a patto che vengano rispettati gli standard di aderenza all'interfaccia, indipendentemente che si tratti di ricercare EJB, connessioni a database, connessioni in coda o di aprire socket, file, e così via. Ovviamente, ci accingiamo a scoprire che questa interpretazione rigida dell'incapsulamento così come è non è valida. Ci costringe a soluzioni forzose per gestire funzionalità e risorse attraverso i "confini" degli oggetti, imponendoci di affrontare dei salti mortali per gestire thread local per le transazioni, la sicurezza, le informazioni sugli utenti, il caching e così via. Che poi questi salti mortali siano eseguiti in vece nostra da parte di un container EJB o effettuati all'interno dell'applicazione conta relativamente… dobbiamo comunque fare i conti con il fatto che il paradigma dell'incapsulamento deve essere un po' semplificato. L'incapsulamento deve essere facilitato al punto in cui ogni dipendenza che venga gestita o incorra nella possibilità di essere gestita attraversando i boundaries degli oggetti deve essere esposta per una gestione di livello più elevato, in maniera non troppo diversa da quanto avviene in una grande azienda, in cui la richiesta di acquisto di un PC da parte di un impiegato si propaga ai livelli amministrativi superiori. Ciò che dovrebbe essere esposto non è solo limitato all'uso delle risorse gestite ma include anche l'impiego delle funzionalità gestite. Pertanto, la propagazione verso l'alto delle dipendenze a un oggetto di livello superiore attraverso IOC è sicuramente valida. L'oggetto di livello superiore conosce il suo ambiente: il modo in cui ottenere e gestire risorse condivise, configurazioni e funzionalità per un caso d'uso. In sostanza, abbiamo bisogno di uno "slittamento" del paradigma, da un incapsulamento rigido all'esposizione degli oggetti gestiti attraverso l'Inversion of Control. L'incapsulamento rimane un concetto importante: semplificarlo un po' non significa assolutamente buttarlo via dalla finestra… Oltre al discorso della gestione degli oggetti potendone attraversare i limiti, c'è un'altra spinta propulsiva verso IOC, vale a dire l'isolamento delle responsabilità. Si tratta di un vecchio mantra della programmazione object-oriented che viene sviluppato in tutte le sue potenzialità con IOC: gli oggetti possono concentrarsi sulle loro funzionalità precipue e, tramite l'Inversion of Control, lasciare le implementazioni di responsabilità non strettamente indispensabili ad autorità di livello più elevato.

 

Dimensioni di IOC
Sono due le dimensioni lungo le quali IOC può essere applicato all'interno della propria applicazione. La prima dimensione è quella orizzontale, o "in larghezza", dove a un estremo si avrà ciascun oggetto disaccoppiato dall'altro e il top object che assembla un grafo di oggetti molto complesso e gestisce il ciclo di vita di tutti gli oggetti. La seconda dimensione è quella verticale, o "in profondità", dove a un estremo si avranno oggetti fortemente accoppiati, eccezion fatta per la maggior parte del "carico pesante" di uso delle risorse che sarà assemblato dall'oggetto di gerarchia più elevato. Come accade spesso, il design migliore si trova un po' nel mezzo, laddove la maggior parte degli oggetti sono disaccoppiati, ma poi ci sono anche oggetti intermedi che assemblano una parte del grafo degli oggetti per fornire unità concrete e incapsulate: un po' come una grande azienda con svariati livelli gestionali che conducono fino all'ufficio del direttore generale.

 

Vantaggi di IOC
I vantaggi di IOC stanno nel fatto che gli oggetti si concentrano fortemente sulle loro funzionalità proprie, diventano estremamente riusabili e, cosa maggiormente importante, molto adatti ai test di unità. La gestione delle risorse e delle funzionalità superando i confini di ciascun oggetto diventa piuttosto diretta. Gli oggetti non sono accoppiati direttamente a risorse d'ambiente o ad altre implementazioni non strettamente pertinenti.
Un test di unità per un oggetto può facilmente creare delle implementazioni di comodo per le entità dipendenti ed effettuare il test di unità per la sua logica, in maniera esplicita e in isolamento. Questi test sono più facili da scrivere, e anche velocissimi. La maggior parte dei test di unità NON richiederà di creare implementazioni fasulle di connessioni a database e alberi JNDI, il che può dare origine a test lenti e pesanti. Basta ricordarsi che implementazioni di comodo per le connessioni al database hanno la loro importanza nei test di integrazione, ma non per il 90% della logica da verificare con i test di unità.

 

Injection IOC
Injection IOC è una maniera piuttosto diffusa per affrontare IOC, in cui si ritiene che un oggetto debba esporre le sue dipendenze attraverso le proprietà del suo costruttore o del Java bean. Questo popolare uso di Injection IOC prevede anche un container generico che gestisca il grafo degli oggetti nel complesso, e l'avvio delle dipendenze quando comincia il ciclo di vita dell'oggetto. Parlo di container "generico" poiché può essere usato con ogni applicazione, dal momento che legge il proprio grafo degli oggetti a partire da un file descrittore e successivamente procede nella sua gestione.

 

Problemi di Injection IOC
Il problema di esporre le dipendenze attraverso costruttori o proprietà di Java bean in generale sta nel fatto che si complica un oggetto e si sovraccaricano i reali scopi funzionali del costruttore e delle proprietà del Java bean. Si costringe inoltre chi effettua l'invocazione a un grande lavoro di assemblaggio, ovvero a "usare molta colla", con la conseguenza che il risultato non può essere facilmente catturato, riusato ed esteso. I container generici cercano di catturare il lavoro di incollaggio all'interno di descrittori, tipo file XML, che vengono caricati ed assemblati dal container. Spingono l'applicazione verso una estremizzazione della dimensione orizzontale di IOC, con un disaccoppiamento superfluo e grafi degli oggetti complessi esposti come file descrittori non tipizzati la cui estensione non risulta facile e che non forniscono nessuna verifica al momento della compilazione. Ovviamente l'esternalizzazione di dipendenze proprie dell'ambiente, quali il binding di risorse e altro, all'interno di descrittori di deploy XML come avviene con i container J2EE, è un'altra storia -- questa tipologia di esternalizzazione è necessaria -- ma esternalizzare un intero grafo di oggetti sembra eccessivamente ambizioso e controproducente. Questo tipo di lavoro "di incollaggio" viene considerata una funzionalità fondamentale che può essere soggetta a requisiti della logica dell'applicazione e a procedure di test: esporla come file descrittore non tipizzato potrebbe rendere l'applicazioni molto instabile. Infine, questo lavoro di incollaggio, in quanto funzionalità fondamentale, dovrebbe risultare riutilizzabile ed estensibile, proprio come qualsiasi altra parte dell'applicazione che rappresenti il business domain.

 

Context IOC
L'IOC di tipo Injection, sebbene rappresenti un passo in avanti nel design, non rende completa giustizia all'Inversion of Control, per le ragioni appena illustrate. Il concetto di un container generico che gestisce il funzionamento del proprio caso d'uso fondamentale sminuisce IOC come pattern di progettazione, facendo diminuire la sua accessibilità per un uso di utilità comune: dopo tutto, i design pattern dovrebbero necessitare solo di espressione nel linguaggio. Oltre a ciò, le persone vengono frenate a usare tale pattern dalla complicazione insita nell'avvio delle dipendenze e la maggior parte di esse ha la "percezione" che IOC implichi incapsulamento pari a zero.
L'IOC di tipo Context, invece, è Inversion of Control nella sua forma più pura: un semplice pattern per la progettazione, privo di pesanti bagagli. Elimina le complicazioni e favorisce l'incapsulamento. Fornisce un modo pulito per organizzare le dipendenze del contesto di un oggetto nella forma di un'interfaccia Context dichiarata nell'ambito della sua classe. L'inversione di controllo viene poi ulteriormente espressa attraverso l'estensione di Context.

 

Esempio 1: PoolGame (gioco del biliardo)
La classe PoolGame esprime le sue dipendenze attraverso una classe interna denominata Context e prende poi un'istanza di Context nel suo costruttore. La classe esegue la sua logica incapsulata per giocare una partita di biliardo, ma delega alla sua interfaccia Context quelle funzionalità, tipo draw(), le cui implementazioni non riguardano la PoolGame. Va notato che in una fase avanzata del refactoring dell'interfaccia Context, si possono spostare alcuni dei metodi di Context ad altre interfacce: e questo è un altro vantaggio dell'uso di Context IOC, come vedremo più avanti.

public class PoolGame {

public interface Context {
public String getApplicationProperty(String key);
public void draw(Point point, Drawable drawable);
public void savePoolGame(String name, PoolGameState poolGameState);
public PoolGameState findPoolGame(String name);
}

public PoolGame(Context cxt) {
this.context = cxt;
}

public void play() {
//comincia a giocare
}

public void endPlay() {
//finisce di giocare
}

public void savePlay(String name) {
PoolGameState saveGameState = new PoolGameState(this.gameState);
context.savePoolGame(name, saveGameState);
}

public void play(String savedPoolGameName) {
PoolGameState savedGameState = context.findPoolGame(savedPoolGameName);
gameState = new PoolGameState(savedGameState);
play();
}

private final Context context;
private PoolGameState gameState;

}

L'interfaccia Context rimuove la complicazione derivante dall'esposizione delle dipendenze attraverso costruttori o proprietà di Java bean (eccetto, ovviamente, per il solo argomento context). Fornisce una maggiore chiarezza organizzando le dipendenze di un oggetto in una singola interfaccia personalizzata unica per quella classe. L'interfaccia PoolGame.Context è unica per la classe PoolGame. Ora gli argomenti e le proprietà del costruttore possono essere riservate per ciò per cui sono veramente pensate: variare le funzionalità usabili di un oggetto, come per esempio una proprietà gameType che potrebbe controllare il tipo di gioco che viene effettuato, per esempio se si tratta di una partita a 9 biglie o di uno straight pool.

 

Context Extension
L'Inversion of Control viene espressa ulteriormente attraverso la Context Extension: l'interfaccia Context può estendere altre Context. L'oggetto invocante deve implementare la Context della propria helper o passare il contratto ulteriormente verso l'alto fornendo una propria interfaccia Context che estende la Context della propria helper. Chi effettua l'invocazione poi aggiunge le proprie dipendenze del contesto alla sua Context. In sostanza chi effettua l'invocazione afferma che le sue necessità di contesto includono tutte quelle del suo helper oltre ad alcune proprie. Questo modo di procedere estendendo le Context per propagare le dipendenze è perfettamente valido tra oggetti che collaborano tra loro, per esempio dei service e i loro helper. L'approccio alternativo potrebbe essere che l'oggetto invocante dichiara una dipendenza all'interfaccia dell'invocato entro la propria Context. Questo disaccoppia il chiamante dall'implementazione del chiamato. L'assemblatore di livello superiore poi implementa le Context sia del chiamante che del chiamato e sceglie l'implementazione corretta del chiamato per il chiamante. Questo approccio potrebbe essere usato per disaccoppiare gli uni dagli altri oggetti a grana grossa e consumatori di risorse, come nel caso di diversi tipi di servizi. In sostanza, la Context Extension viene usata da assemblatori intermedi che rappresentano un'unità funzionale concreta incapsulata a grana grossa. Essi incollano implementazioni concrete di oggetti helper per ottenere queste funzionalità a grana grossa e poi propagano verso l'alto tutte le dipendenze dei propri helper attraverso la Context Extension. Queste unità a grana grossa sono sottoponibili a test di unità proprio come i loro helper e dovrebbero avere i loro particolareggiati test di unità che dimostrino che la loro funzionalità da assemblati è in ordine. È importante anche notare che, oltre ad assemblare helper concreti, queste unità a grana grossa spesso aggiungeranno ulteriore logica applicativa dentro e intorno al lavoro di assemblaggio.
Propagare le dipendenze con l'Injection IOC attraverso proprietà di costruttori o Java bean costringe a modifiche in ogni classe della/e catena/e di chiamate per dare spazio alle nuove dipendenze, a meno che non si implementi una dimensione estremamente orizzontale di IOC, la quale a sua volta farebbe aumentare la complicazione dentro ciascun oggetto. Introdurre nuove dipendenze o modificare quelle esistenti con la Context Extension avrà un impatto solamente sulla classe di assemblaggio di livello più elevato che assembla gli oggetti a grana grossa, dal momento che sono solo essi a implementare le Contexts. Modifiche alla Context di un oggetto si propagheranno verso l'alto attraverso la Context Extension, senza influire sulle classi intermedie nella/e catena/e di chiamata.
Le interfacce Context forniscono un'ottima modalità per astrarre le dipendenze all'interno della gerarchia e del design, consentendo riuso e possibilità di estensione. I metodi presenti in una Context che affrontano problemi simili possono a loro volta essere sottoposti a refactoring nella loro interfaccia: la Contest, a quel punto, può estendere questa nuova interfaccia o fornire ad essa un metodo get. Fatto ancor più importante, le implementazioni di Context, ovvero il "codice collante", possono essere generalizzate e astratte in vista del riuso e dell'estensione. Le implementazioni di Context sono generalmente fornite da classi top level, vale a dire punti di entrata di casi d'uso, e possono essere astratte, totalmente o in parte, e condivise tra molti casi di uso che abbiano ambienti o requisiti simili. Inoltre, il refactoring di implementazioni di Context porterà a identificare delle proprietà configurabili che dovrebbero effettivamente risiedere esternamente, sotto forma di proprietà di applicazioni o di deploy: le prime saranno tipicamente immagazzinate in un database per applicazioni di amministrazione, le seconde staranno in un file XML o di proprietà, a disposizione di chi effettua il deploy.
Alla fine, tutto il "collante" applicativo viene catturato in Context estensibili che vengono verificate a tempo di compilazione. Introdurre o modificare una dipendenza in una Context di un oggetto porterà a una verifica in compilazione di tutti i casi d'uso che utilizzano quell'oggetto e di conseguenza all'implementazione delle sue dipendenze contestuali.
D'altro canto, i container generici IOC che leggono grafi di oggetti a partire da un descrittore e affermano di possedere la massima possibilità di configurazione non forniscono un vero e proprio ROI: hanno catturato il "collante" essenziale dell'applicazione in un file descrittore non tipizzato il quale non può essere compreso o modificato dagli amministratori dell'applicazione o da chi ne effettua il deploy. Inoltre, cambiamenti nei descrittori "collanti" possono modificare l'andamento dei casi d'uso e dovrebbero attraversare una adeguata fase di test prima di essere pronti per la release: non si tratterebbe quindi, di qualcosa di facilmente modificabile all'esterno dell'applicazione.

 

Esempio 2: far corrispondere il lavoro
Il caso d'uso in cui si cerca di far corrispondere un lavoro al candidato dimostra l'uso del Context IOC, la sua integrazione con pattern esistenti per soddisfare le funzionalità e, infine, il modo in cui viene applicato nel deploy di tipo enterprise.
Il pattern Service insieme al Context IOC
Il pattern Service viene usato per rappresentare e raggruppare casi d'uso. Ciascun metodo definito in un'interfaccia Service rappresenta un caso d'uso attivato dall'utente o dal sistema. Generalmente rappresenta anche una transazione di una o più risorse. La funzionalità applicativa di Service viene implementata come semplice oggetto Java e rappresenta una unità funzionale a grana grossa. Propaga verso l'alto le necessità di contesto dei suoi helper attraverso Context Extension. Va notato che questa unità aggiunge funzionalità applicativa dentro e intorno all'assemblaggio dei suoi helper, e dichiara pertanto alcune necessità di contesto sue proprie.

/**
* Interfaccia Service per la corrispondenza dei lavori.
*/
public interface JobMatchService {

/**
* Rappresenta il caso d'uso per far corrispondere un lavoro a un candidato
* @return Set : set di oggetti Job.
*/
public Set match(CandidateKey candidateKey);

//...altri metodi del caso d'uso
}


/**
* La business logic di JobMatchService in un oggetto Java.
*/
public class JobMatchServiceImpl
implements JobMatchService
{
/**
* Dichiara ciò di cui questa implementazione necessita nella sua interfaccia Context.
* Oltre a definire le sue necessità nella sua Context, estende
* le Contexts delle classi helper che utilizza, in maniera da propagare tutte
* le necessità di contesto verso l'alto.
*/
public interface Context
extends MatchStrategyFactory.Context
{
public CandidateStore getCandidateStore();
... //altri metodi
}

/**
* Notare come l'argomento di Context può essere passato così come è
* a tutti gli oggetti helper, dal momento che Context implementa anche tutte le loro Contexts
*/
public JobMatchServiceImpl(Context cxt) {
this.context = cxt;
this.matchStrategyFactory = new MatchStrategyFactory(cxt);
}

/**
* Contiene tutta la business logic per il caso d'uso di corrispondenza dei lavori.
*/
public Set match(CandidateKey candidateKey)
throws CandidateNotValidException
{
Candidate candidate =
context.getCandidateStore().findCandidate(candidateKey);

if (candidate == null || candidate.isRegistrationExpired()) {
throw new CandidateNotValidException(candidateKey);
}

MatchStrategy strategy = matchStrategyFactory.createStrategy(candidate);
Set jobs = strategy.match(candidate);

candidate.setLastMatchedDate(new Date());
candidate.setMatchStrategyUsed(strategy.getName());
context.getCandidateStore().update(candidate);

return jobs;
}

//...implementazioni degli altri metodi del caso d'uso

private final Context context;
private final MatchStrategyFactory matchStrategyFactory;
}

 


Pattern Strategy insieme al Context IOC (continuazione dell'Esempio 2)
Il pattern Strategy viene usato per determinare il corretto algoritmo per la corrispondenza che deve essere applicato a seconda delle diverse circostanze. Un factory crea la strategia adeguata e usa la sua Context e il Context Extension per propagare i requisiti delle singole strategie. In questo esempio, abbiamo un numero predeterminato di strategie e pertanto ciascuna strategia può fornire la sua Context che viene combinata e propagata verso l'alto attraverso la Context del factory. Tuttavia, nei casi in cui le strategie devono essere aggiunte in maniera dinamica, è possibile definire una singola Context in una classe base di strategia per le necessità di tutte le sottoclassi.

/**
* Questa factory crea l'adeguata strategia di corrispondenza per un dato candidato.
*/
public class MatchStrategyFactory {

/**
* Trattandosi di una factory per tutte le strategie, propaga tutte le
* necessità di contesto delle Strategy sottostanti che essa creerà.
*/
public interface Context
extends JobMatchConfig.Context, QuickMatchStrategy.Context,
SinglePhaseMatchStrategy.Context, TwoPhaseMatchStrategy.Context,
MaximumPhaseMatchStrategy.Context
{
//...nessun metodo, dal momento che non ha necessità contestuali sue proprie.
}

public MatchStrategyFactory(Context cxt) {
this.context = cxt;
config = new JobMatchConfig(cxt);
}

/**
* Crea la strategia adeguata per il dato candidato.
* Notare come l'argomento di Context può essere passato così come è
* a tutti gli oggetti Strategy, dal momento che Context implementa anche tutte le loro Contexts
*/
public MatchStrategy createStrategy(Candidate candidate) {
//nota: config è stato configurato nel costruttore sopra...

if (candidate.daysOnSearch() < config.getDaysOnSearchLow()) {
strategy = new QuickMatchStrategy(context);
}
else if (candidate.daysOnSearch() < config.getDaysOnSearchMedium()) {
strategy = new SinglePhaseMatchStrategy(context);
}
else if (candidate.daysOnSearch() < config.getDaysOnSearchHigh()) {
strategy = new TwoPhaseMatchStrategy(context);
}
else{
strategy = new MaximumPhaseMatchStrategy(context);
}

return strategy;
}

private final Context context;
private final JobMatchConfig config;
}


/**
* Rappresenta una strategia per far corrispondere I lavori a un candidato.
*/
public interface MatchStrategy {
public String getName();
public Set match(Candidate candidate);
}


/**
* Implementazione veloce e sporca di una MatchStrategy.
*/
public class QuickMatchStrategy
implements MatchStrategy
{
/**
* Context che dichiara di aver bisogno della versione Read Only
* del Job Persistent Store.
*
* Context vs. Constructor
* Il vantaggio di collocare le cose nel Context piuttosto che nel Constructor
* sta nel fatto che chi userà questo oggetto non necessita di essere influenzato dalle necessità
* che l'oggetto ha al fine di implementarsi.
*/
public interface Context {
public JobStore getJobStoreReadable();
}

/**
* Risiende nello stesso package di MatchStrategyFactory e pertanto fornisce solamente
* visibilità di package per il costruttore.
*/
QuickMatchStrategy(Context cxt) {
this.context = cxt;
}

/**
* Algoritmo veloce e sporco…
* Trova I lavori solamente in base alla capacità principale.
*/
public Set match(Candidate candidate) {
Set jobs = context.getJobStoreReadable().findJobsBySkill(candidate.getTopSkill());

Iterator jobsIter = jobs.iterator();
while(jobsIter.hasNext()) {
Job job = (Job)jobsIter.next();
if (job.yearsOfExperience() > candidate.yearsOfExperience()) {
if (job.salary() < candidate.salary()) {
jobsIter.remove();
}
}
}

return jobs;
}

private final Context context;
}

 


Pattern Config insieme al Context IOC (continuazione dell'Esempio 2)
Un pattern semplice per accedere alla configurazione all'interno di un'applicazione. La configurazione viene raggruppata secondo l'interesse in classi le quali poi forniscono un'interfaccia incapsulata e ben tipizzata alle proprietà individuali. Il pattern Config usa il Context IOC per accedere alla proprietà dell'applicazione.

/**
* Un helper per leggere e tradurre le proprietà usate per JobMatch.
*/
public class JobMatchConfig {

/**
* Context dichiara la sua necessità di una proprietà di applicazione
* che potrebbe trovarsi in un Database o in un URL o nel file-system locale
* come determinato dall'assemblatore di alto livello del caso d'uso.
*
* Context vs. Constructor
* Il vantaggio di collocare le cose nel Context rispetto al Constructor
* sta nel fatto che chi invoca Context può facilmente passare I requisiti di implementazione
* attraverso il Context Extension.
*/
public interface Context {
public String getApplicationProperty(String key);
}

public JobMatchConfig(Context cxt) {
this.context = cxt;
}

public int getDaysOnSearchLow() {
String key = "job.match.days.on.search.low";
String value = context.getApplicationProperty(key);

try {
return Integer.parseInt(value)
}catch(NumberFormatException x){
throw new IllegalConfigException(key, value, x);
}
}

public int getDaysOnSearchMedium() {
//...simili ad altri
}

public int getDaysOnSearchHigh() {
//...simili ad altri
}

... //altri metodi

private final Context context;
}

 


Pattern Store insieme al Context IOC (continuazione dell'Esempio 2)
Il pattern Store viene usato per accedere a dati persistenti. Un uso sbagliato di IOC molto comune è il caso in cui oggetti business accedono a un DataSource per loro specifiche necessità di dati. Possono avere usato IOC per accedere DataSource, il che va parzialmente bene, ma perde di vista la giusta prospettiva. Questi oggetti business non sono riutilizzabili e non vengono facilmente sottoposti a test di unità (poiché sottoporli a test di unità significa creare delle implementazioni fasulle di DataSource per essi). Questi oggetti dovrebbero invece dichiarare ed esporre la loro dipendenza ai dati di cui necessitano, permettendo all'assemblatore top-level del caso d'uso di decifrare da dove proviene. Un buon design consisterebbe nel far dichiarare a questi oggetti una dipendenza a una interfaccia Store che gli fornisce un tipo specifico di entità dati persistente di cui gli oggetti hanno bisogno. Ciò consente test di unità semplici per questi business object dal momento che creare una implementazione di comodo per una Store e i suoi dati fasulli diventa più facile e, soprattutto, estremamente veloce. In generale, la facilità per un oggetto di essere sottoposto a test di unità è un buon indicatore per misurare un buon design IOC.
CandidateStore segue un pattern simile, come mostrato.

/**
* Interfaccia che identifica una lettura / scrittura completa
* alla Store persistente per I dati di Job.
*/
public interface JobStore
extends JobStoreReadable
{
public Job add(Job job);
public Job update(Job job);
public void addSkills(JobKey jobKey, Set skills);
... //altri metodi
}


/**
* Interfaccia che identifica solo l'interfaccia Readable al
* Job Persistent Store.
*
* Consente ai top-object di ottimizzare le transazioni se l'andamento di
* un caso d'uso richiede solo Readable Store.
*/
public interface JobStoreReadable {
public Job findJob(JobKey key);
public Set findJobsBySkills(Set skills);
... //altri metodi
}


/**
* Implementazione di lettura e scrittura verso Job Persistent Store.
*/
public class JobStoreImpl
implements JobStore
{
/**
* Context dichiara che necessita di JDBC Connection al Database.
* Questo approccio elimina responsabilità/errori della chiusura di Connection da parte di tutti gli oggetti.
* La responsabilità rimane solamente all'assemblatore del caso d'uso top-level che deve fornire e poi
* chiudere la connessione per un dato caso d'uso. Permette all'assemblatore del caso d'uso top-level
* di riusare Connection etc. (Vedi ulteriori alternative).
*
* Alternativa: può dichiarare DataSource invece di Connection,
* per consentire un uso di connessioni a grana più fine, ma responsabilità/errori
* della chiusura di Connection vengono lasciati alla gesrione di ciascuna StoreImpl.
*
* Alternativa: può dichiarare HibernateSession invece di Connection.
* Questo approccio elimina responsabilità/errori della chiusura di Connection e HibernateSession da parte di tutti gli oggetti.
* La responsabilità rimane solamente all'assemblatore del caso d'uso top-level che deve fornire HibernateSession e poi
* chiudere la Session e la Connection connessione per un dato caso d'uso.
* Permette all'assemblatore del caso d'uso top-level il caching/riuso di Session, il riuso di Connections etc..
*
* Alternativa: Può accettare DataSource/Connection/HibernateSession via Constructor.
* Non è molto estensibile. Con una Context, gli oggetti invocanti possono facilmente estendere
* e passare la responsabilità vero l'alto, senza rimanere sostanzialmente influenzati dalle
*necessità di StoreImpl. Inoltre, il top-object deve implementare solamente questo metodo una sola volta
* per tutti gli StoreImpl usati nel caso d'uso e poi creare la connessione.
*
* Facoltativo: se esistono molti DataSource nell'applicazione o si desidera
* prevedere tale ipotesi si può fare in modo che getConnection() specifichi un nome logico di datasource:
*come getConnection(String datasource)
* permettendo all'assemblatore del caso d'uso top-level di assemblare la Connection corretta
* dalla corretta DataSource.
*/
public interface Context {
public java.sql.Connection getConnection();
}

public JobStoreImpl(Context cxt) {
this.context = cxt;
}

public Job findJob(JobKey key) {
... //usare la Connection dal contesto per trovare
}

public Set findJobsBySkills(Set skillset) {
... // usare la Connection dal contesto per trovare
}

public Job add(Job job) {
... // usare la Connection dal contesto per aggiungere
}

public Job update(Job job) {
... // usare la Connection dal contesto per aggiornare
}

public void addSkills(JobKey jobKey, Set skills) {
... // usare la Connection dal contesto per aggiungere skill
}

... //altri metodi

private final Context context;
}

 

Deploy Enterprise insieme al Context IOC (continuazione dell'Esempio 2)
Una volta che sono state costruite Service e la/e sua/e ServiceImpl con i propri metodi e la propria business logic, usando il Context IOC, è possibile inserirle in qualsiasi ambiente. Se occorre effettuare il deploy su un server J2EE, si può scegliere di inglobare il proprio Service dentro un EJB o una Servlet a seconda delle proprie necessità di gestione per quanto attiene a sicurezza, transazioni, risorse e così via. Se si sceglie di effettuare il deploy come parte di un main() standard o come applicazione di tipo batch, è allora possibile inglobare il proprio Service in una classe Application. Mi piace pensare agli EJB, alle Servlet e così via semplicemente come a dei punti di ingresso in un sistema che funge da assemblatore del caso d'uso top-level. Sostanzialmente il sistema prende un Service e adatta le sue necessità di contesto all'ambiente rappresentato dal punto di ingresso. Gli assemblatori si limitano semplicemente a soddisfare la Context di Service e poi delegano al metodo del caso d'uso di Service l'esecuzione di tutte le funzionalità associate al caso d'uso.

/**
* Un deploy di JobMatchService tramite Stateless SessionBean.
* Questo EJB impiega sicurezza e transazioni gestite dal container.
*/
public class JobMatchServiceEJB
implements SessionBean, JobMatchService
{
/**
* Assembla la Context di JobMatchService affinché esegua il caso d'uso della corrispondenza dei lavori.
* Trattato semplicemente come un punto di ingresso per un tipo di ambiente: esso sa come fornire
* sicurezza, gestire le transazioni e accedere alle risorse necessarie per il servizio.
*/
public Set match(CandidateKey candidateKey)
throws CandidateNotValidException
{
ServiceContext context = new ServiceContext(); //vedere più avanti la classe privata


try {
JobMatchService service = new JobMatchServiceImpl(context);
return service.match(candidateKey);
}finally{
context.destroy();
}
}

/**
* Un'implementazione private della Context di Service usata solamente da questo EJB.
* Se diversi EJB necessitano di condividere questa Context o parti di essa, è possibile estrarla
* ed effettuarne il refactoring per il riuso.
*
* Sicurezza del thread:
* ServiceContext mantiene lo stato (per l'inizializzazione lazy etc.) ma la sincronizzazione
* del thread non è necessaria dal momento che esso è creato ed eseguito
* all'interno del meto dell'EJB.
*/
private class ServiceContext
implements JobMatchServiceImpl.Context, CandidateStoreImpl.Context,
JobStoreImpl.Context, AppConfigStoreImpl.Context
{
public CandidateStore getCandidateStore() {
return new CandidateStoreImpl(this); //può essere ottimizzata sotto forma di unica istanza
}

public JobStoreReadable getJobStoreReadable() {
return new JobStoreImpl(this); //può essere ottimizzata sotto forma di unica istanza
}

/**
* Ottiene proprietà di applicazione dal database.
* Può essere modificata affinché indichi di leggere da un URL.
*/
public String getApplicationProperty(String key) {
//può essere sottoposta a refactoring per avere un caching a scadenza più lunga.
if (appConfig == null) {
AppConfigStore configStore = new AppConfigStoreImpl(this);
appConfig = configStore.findAppConfig("JobMatch");
}
return appConfig.getProperty(key);
}

/**
* Usata da tutte le classi StoreImpl che fanno parte di un caso d'uso.
*
* Solamente una istanza della Connection create e usata per la singola
* esecuzione di un caso d'uso. Questo approccio fornisce una gestione facile in un solo posto
* di una Connection per un intero caso d'uso ed è sufficiente per la maggior parte degli scenari.
* La singola StoreImpl non si deve preoccupare di chiudere Connection.
* Perdite nelle risorse vengono evitate tramite questo approccio.
*
* Alternativa: se il caso d'uso prevede molte elaborazioni tra l'uso di Connection
* la sua Context dovrebbe richiedere invece un DataSource e gestire il ciclo di vita della sua Connection
*/
public Connection getConnection() {
try {

if (conn == null) {
DataSource ds = (DataSource)(new InitialConext()).lookup("jdbc/job");
conn = ds.getConnection();
}
return conn;

}catch(Exception x){
throw new InvalidStateException(x);
}
}

/**
* Ripulisce le risorse usate da questa Context.
*/
public void destroy() {
try {
if (conn != null) {
conn.close();
}
}catch(Exception x){
log.warn(x);
}
}

private Connection conn = null;
private AppConfig appConfig = null;
}

}


Test di unità insieme al Context IOC (continuazione dell'Esempio 2)
Molto semplicemente, i test di unità sono un altro punto di ingresso in un sistema, più specificamente un sistema di test, e come tali adattano le necessità di contesto di una Service al sistema di test attraverso la creazione di implementazioni fasulle di comodo. JUnit riesce non solo a implementare la Context di Service ma anche le Context di qualsiasi sua helper o utilità. Preferisco creare implementazioni concrete che implementano le varie Context o Store direttamente, piuttosto che usare le librerie di implementazioni di comodo disponibili liberamente, ma entrambe le soluzioni funzionano. Le implementazioni di comodo concrete assomigliano a classi vere e proprie che possono a loro volta essere generalizzate, analizzate e sottoposte a refactoring per massimizzare il riuso dei propri dati di test all'interno del proprio sistema di test.

public class JobMatchServiceImplTest
extends TestCase
{
/**
* Un test semplice per il servizio di corrispondenza dei lavori.
*/
public void testMatch() {
TestContext testContext = new TestContext(); //vedere più avanti la classe privata
JobMatchServiceImpl jobMatchService = new JobMatchServiceImpl(testContext);
CandidateKey candidateKey = new TestCandidateKey("Sony");
Set jobs = jobMatchService.match(candidateKey);
assertNotNull(jobs);
assertEquals(5, jobs.size());
assertEquals(1, testContext.candidateStore.updateCount());
//...altre assert.
}

/**
* Una implementazione privata di context usata solamente in questo test.
* Può essere modificata in refactoring per il riutilizzo con molti test.
*/
private class TestContext
implements JobMatchServiceImpl.Context
{
final TestCandidateStore candidateStore = new TestCandidateStore();
final TestJobStore jobStore = new TestJobStore();

public CandidateStore getCandidateStore() {
return candidateStore;
}

public JobStoreReadable getJobStoreReadable() {
return jobStore;
}

public String getApplicationProperty(String key) {
if (key.equals("job.match.days.on.search.low") {
return "7";
}else if (key.equals("job.match.days.on.search.medium") {
return "14";
}else if (key.equals("job.match.days.on.search.high") {
return "25";
}else{
throw new IllegalStateException("Bad application property requested " + key);
}
}

public Connection getConnection() {
throw new IllegalStateException("Connection request invalid in a test case");
}
}
}

/**
* Un esempio di implementazione di comodo per Store usata da molti test.
*
* Un test case può scegliere se creare la sua Store e le sue assert di test
* direttamente nei suoi metodi OPPURE usare una Store di test condivisa
* che può successivamente essere ispezionata.
*/
public class TestCandidateStore implements CandidateStore {
private final Collection candidates = new ArrayList(100);

public TestCandidateStore() {
add(new Candidate("Sony"));
add(new Candidate("Roney"));
add(new Candidate("James"));
//...ulteriori dati.
}

public void add(Candidate candidate) {
candidates.add(candidate);
//...aggiunge a varie mappe indicizzate
//...per I metodi di ricerca.
}

public Job findCandidate(CanidateKey key) {
//lookup an indexed map to find.
}

public void update(Candidate candidate) {
//ricerca una mappa indicizzata
//poi aggiorna l'oggetto o lancia l'eccezione se non viene trovato
//tiene traccia degli aggiornamenti per una successiv ispezione
}

/**
* Esempio di metodo che permette all'ispezione di Store di effettuare l'assert.
*/
public int updateCount() {
//return count from update list.
}

//...altri metodi
}

 

In conclusione
Inversion of Control(IOC) è di certo un concetto valido e i vecchi parrucconi della progettazione OO devono venire a patti con questa naturale evoluzione verso IOC. Il concetto rigido di incapsulamento deve essere allentato per consentire la gestione di risorse e funzionalità oltre i confini degli ogetti, attraverso IOC. La possibilità estesa di test di unità per i vari aspetti isolati, ottenuta con IOC, rappresenta un altro grande passo avanti nel fornire software di qualità. Injection IOC, sebbene sia comunque un passo avanti nel design, non sviluppa completamente il potenziale di IOC. Context IOC cerca di oltrepassare il limite di IOC, per diventare un design pattern di uso generale.

Sony Mathew (Sony Mathew, Lead Architect alla Prime Therapeutics smathew@primetherapeutics.com) è capo architetto alla Prime Therapeutics, in Minnesota, e si occupa di dirigere la realizzazione di applicazioni enterprise volte in gran parte alla distribuzione di prodotti e servizi farmaceutici, attraverso siti web e un'integrazione diretta di B2B e web service. Ha circa sei anni di esperienza nello sviluppo e nella progettazione di applicazioni J2EE.