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.
|