Home

Blog

Articles

Books

About Me

Contact Me

ThoughtWorks

Container ad Inversione di Controllo e il pattern dell'Iniezione di Dipendenza

Aritcolo Originale

Martin Fowler

C'è stato un grandissimo interesse nella comunità Java riguardo i lightweight container che permettono di assemblare componenti di differenti progetti dentro ad un'applicazione unica. Alla base di questi container c'è un pattern comune su come essi implementano il collegamento, un concetto espresso sotto il termine generico di "Inversione di Controllo". In questo articolo approfondirò il funzionamento di questo pattern, sotto il nome più specifico di "Iniezione di Dipendenza", e lo metterò in confronto coll'alternativa del Service Locator. La scelta tra questi due approcci è meno importante del principio di tenere separati la configurazione dall'uso.

Ultimo aggiornamento significativo: 23 gennaio 04

Traduzione del: 4 ottobre 06

| Cinese | Portoghese | Francese | Italiano

Indice


Una delle cose più affascinanti del mondo Java per l'enterprise è l'ingente numero di attività che cercano di produrre delle alternative alle tecnologie J2EE ufficiali, la maggior parte delle quali avvengono nel mondo open-source. Gran parte di ciò è una reazione alla complessità eccessiva nel mondo ufficiale J2EE, ma molto è dovuto anche all'esplorazione di alternative e alla nascita di idee creative. Un problema comune da affrontare è come collegare assieme elementi diversi: come si può fare cooperare questa architettura di controllo web con quell'interfaccia di supporto a database quando questi sono stati realizzati da gruppi diversi con scarse conoscenze reciproche. Un certo numero di framework hanno preso in considerazione questo problema, e molti si stanno adoperando per trovare una metodologia generica per assemblare componenti di diverse provenienze. Ci si riferisce ad essi come a lightweight container, alcuni esempi sono PicoContainer e Spring.

Alla base di questi container ci sono un certo numero di principi progettuali interessanti, cose che prescindono sia dei container stessi che della piattaforma Java. In questo articolo voglio iniziare ad esplorare alcuni di questi principi. Gli esempi adottati sono in Java, ma, come nella maggior parte dei miei scritti, i principi sono equivalentemente applicabili ad altri ambienti OO, in particolare a .NET.


Componenti e Servizi

Il tema di collegare insieme elementi mi porta immediatamente ai nodosi problemi di terminologia che riguardano i termini servizio e componente. Si possono con facilità trovare articoli lunghi e contraddittori sulla definizione di questi due concetti. Ecco l'uso che faccio per i miei fini di questi termini super utilizzati.

Indico con componente un pezzo di software che è inteso per essere usato, senza modifiche, da un'applicazione che è fuori dal controllo di coloro che hanno scritto il componente. Con 'senza controllo' intendo che chi scrive l'applicazione NON modifica il codice sorgente dei componenti, anche se può alterarne il comportamento estendendolo nei modi consentiti dagli autori del componente.

Un servizio è simile ad un componente nel senso che viene usato da applicazioni esterne. La differenza principale è che mi aspetto che un componente venga usato localmente (si pensi ad un file jar, un assembly, una dll o all'inclusione di codice sorgente). Un servizio verrà usato in modo remoto attraverso qualche interfaccia remota, sia sincrono che asincrono (per es. web service, sistemi di messaggistica, RPC o socket).

In questo articolo farò riferimento principalmente a servizi, ma molte delle stesse logiche possono essere applicate anche a componenti locali. D'altronde spesso si ha bisogno di un framework di componenti locali per accedere ad un servizio remoto. Ma scrivere e leggere "componente e servizio" è noioso, ed i servizi sono molto più di moda oggi.


Un semplice esempio

Per rendere il tutto più concreto mi baserò su un esempio reale per parlare di tutto ciò. Come tutti i miei esempi è uno di quegli esempi super-semplici; così piccolo da sembrare irreale, ma spero sia abbastanza affinché si possa visualizzare cosa sta succedendo senza affrontare la complessità di un esempio reale.

In questo esempio scrivo un componente che fornisce una lista di film di un certo regista. Questa stupefacente funzione è implementata da un singolo metodo

class MovieLister...

 public Movie[] moviesDirectedBy(String arg) {

 List allMovies = finder.findAll();

 for (Iterator it = allMovies.iterator(); it.hasNext();) {

 Movie movie = (Movie) it.next();

 if (!movie.getDirector().equals(arg)) it.remove();

 }

 return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);

 }

L'implementazione di questa funzione è estremamente banale, chiede ad un oggetto finder (di cui parleremo tra breve) di restituire ogni film che conosce. Quindi va a caccia in questa lista in modo da restituire solo quelli di un particolare regista. Non mi voglio soffermare su questa parte molto banale, in quanto rappresenta solo l'impalcatura dell'obiettivo di questo articolo.

L'obiettivo primo di questo articolo è l'oggetto finder, o meglio come colleghiamo l'oggetto lister con un particolare oggetto finder. La ragione per cui ciò è interessante è che io voglio che il mio meraviglioso metodo moviesDirectedBy sia completamente indipendente dal modo in cui tutti i film sono memorizzati. Così tutto ciò di cui il metodo ha bisogno è fare riferimento ad un finder, e tutto ciò che il finder fa è sapere rispondere al metodo findAll. Posso spostarlo fuori definendo un'interfaccia per il finder.

public interface MovieFinder {

 List findAll();

}

Ora tutto è molto disaccoppiato, ma ad un certo punto io devo tirare fuori una classe concreta per ottenere effettivamente i film. In questo caso metto il questo codice nel costruttore della mia classe lister.

class MovieLister...

 private MovieFinder finder;

 public MovieLister() {

 finder = new ColonDelimitedMovieFinder("movies1.txt");

 }

Il nome della classe che implementa il finder deriva dal fatto che sto prelevando la mia lista da un file delimitato da punto e virgola. Vi risparmio i dettagli, dopo tutto il punto è che c'è una qualche implementazione.

Ora, se sto usando questa classe solo per me stesso, è tutto corretto ed elegante. ma cosa succede se i miei amici sono sopraffatti da un desiderio per questa fantastica funzionalità e desiderassero una copia del mio programma? Se anche essi memorizzassero i loro elenchi di film in file separati da punto e virgola chiamati "movies.txt" allora tutto sarebbe magnifico. Se loro avessero un nome diverso per i loro file di film, sarebbe facile mettere quel nome in un properties file. Ma cosa succederebbe se avessero un modo completamente diverso di memorizzare i loro elenchi di film: un database SQL, un file XML, un web service o semplicemente un altro formato di file di testo? In tal caso ci sarebbe bisogno di una classe diversa per recuperare quei dati. Ora, siccome ho definito un'interfaccia MovieFinder, questo non modificherebbe il mio metodo moviesDirectedBy. Ma ho ancora bisogno di avere un modo per oteenere un'istanza dell'implementazione corretta del finder.

Figura 1: Le dipendenze usando una semplice creazione della classe lister

La Figura 1 mostra le dipendenze in questa situazione. La classe MovieLister dipende sia dall'interfaccia MovieFinder che dalla sua implementazione. Preferiremmo se dipendesse solo dall'interfaccia, ma allora come possiamo fare affinché essa lavori su un'istanza?

Nel mio libro P of EAA, descriviamo questa situazione come una Plugin. La classe implementazione del finder non è collegata al programma al momento della compilazione, in quanto non so quale i miei amici intendano usare. Invece vogliamo che il mio lister lavori con qualsiasi implementazione, e che tale implementazione sia inserita in un momento successivo, fuori dal mio controllo. Il problema è come posso costruire il collegamento in modo che la mia classe lister non conosca la classe implementazione, ma possa ancora parlare con una sua istanza per fare il proprio lavoro.

Espandendo questo concetto ad un sistema reale, potremmo avere decine di componenti e servizi simili. In ogni modo possiamo astrarne l'uso parlando loro tramite interfacce (ed utilizzando un adapter se il componente non è stato progettato pensando ad un'interfaccia). Ma se desideriamo mettere in linea questo sistema in diversi modi, dobbiamo usare le plugin per gestire l'interazione di questi servizi così da poter usare diverse implementazioni in diverse configurazioni.

Quindi il cuore del problema è: come assemblare queste plugin in una applicazione? Questo è uno dei problemi principali che questa nuova razza di lightewight container deve affrontare ed universalmente lo affrontano tutti usando l'Inversione di Controllo.


Inversione di Controllo

Quando mi spiegano che questi container sono così utili perché implementano l'"Inversione di Controllo" finisco sempre per essere confuso. L'Inversione di Controllo è una caratteristica comune ai framework, così, dire che questi container sono speciali perché usano l'Inversione di Controllo, è come dire che la mia macchina è speciale perché ha le ruote.

La domanda è: quale aspetto del controllo stanno invertendo? Quando ho visto per la prima volta l'Inversione di Controllo, fu nel controllo principale di un interfaccia utente. Le prime interfacce utente erano controllate dal programma dell'applicazione. Erano disponibili una sequenza di comandi come "Inserire nome", "Inserire indirizzo"; il programma presentava le richieste e raccoglieva le risposte ad ognuna di esse. Con le UI grafiche (o anche screen based), il framework della UI conteneva il loop principale ed il programma forniva gestori di eventi per i diversi campi sullo schermo. Il controllo principale del programma era invertito, spostato da voi verso il framework.

Per questa nuova famiglia di container, l'inversione sta nel come cercano l'implementazione di una plugin. Nel mio esempio banale il lister cercava il finder creandolo direttamente. Questo impedisce al finder di essere una plugin. L'approccio che questi container usano è di assicurare che tutti gli utenti di una plugin seguano alcune convenzioni che permettono ad un modulo assemblato separato di iniettare l'implementazione nel lister.

Come risultato, credo che ci sia il bisogno di un nuovo nome per questo pattern. Il termine Inversione di Controllo è troppo generico e perciò la gente si confonde. Dopo molte discussioni con vari avvocati dell'IdC, ci siamo accordati sul nome Iniezione di Dipendenza.

Sto per iniziare a parlare di diverse forme di Iniezione di Dipendenza, ma sottolineo adesso che non è il solo modo di rimuovere le dipendenze dalla classe dell'applicazione all'implementazione delle plugin. l'altro pattern che si può usare è il Service Locator, e lo discuterò dopo aver finito di spiegare l'Iniezione di Dipendenza.


Le forme di Iniezione di Dipendenza

L'idea di base dell'Iniezione di Dipendenza è di avere un oggetto separato, un assemblatore, che popoli un campo nella classe lister con l'appropriata implementazione per l'interfaccia finder, il cui risultato segue quello di Figura 2

Figura 2: Le dipendenze per l'Iniezione di Dipendenza

Ci sono 3 stili principali di Iniezione di Dipendenza. I nomi che userò sono Iniezione sul Costruttore, Iniezione sul Setter e Iniezione sull'Interfaccia. Se si legge su questo argomento nell'attuale discussione sull'Inversione di Controllo se ne sente parlare come di IdC di tipo 1 (Iniezione sull'Interfaccia), tipo 2 (Iniezione sul Setter) e tipo 3 (Iniezione sul Costruttore). Trovo i nomi numerici piuttosto difficili da ricordare, ecco perché ho usato questi altri nomi.

Iniezione sul Costruttore con PicoContainer

Incomincerò mostrando come questa iniezione sia fatta usando un lightweight container chiamato PicoContainer. Inizio da qui soprattutto perché molti dei miei colleghi alla ThoughtWorks sono molto attivi nello sviluppo di PicoContainer (sì, è una specie di campanilismo aziendale).

PicoContainer usa un costruttore per decidere come iniettare l'implementazione di un finder nella classe lister. Perché questo funzioni, bisogna che la classe lister di film dichiari un costruttore che riceva tutto ciò che deve essere iniettato.

class MovieLister...

 public MovieLister(MovieFinder finder) {

 this.finder = finder;

 }

Anche lo stesso finder sarà gestito dal pico container e come tale gli sarà iniettato dal container il nome del file di testo.

class ColonMovieFinder...

 public ColonMovieFinder(String filename) {

 this.filename = filename;

 }

Il pico container ha quindi bisogno di sapere quale classe implementazione associare ad ogni interfaccia, e e quale stringa iniettare nel finder.

 private MutablePicoContainer configureContainer() {

 MutablePicoContainer pico = new DefaultPicoContainer();

 Parameter[] finderParams = {new ConstantParameter("movies1.txt")};

 pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);

 pico.registerComponentImplementation(MovieLister.class);

 return pico;

 }

Questo codice di configurazione viene tipicamente messo nel file di una classe separata. Per il nostro esempio, ogni amico che usa il mio lister può scrivere la configurazione appropriata in qualche sua classe di setup. Naturalmente è cosa comune tenere questo tipo di informazione di configurazione su file separati. Si può scrivere una classe per leggere un file di configurazione e preparare il container di conseguenza. Sebbene PicoContainer non possieda questa funzionalità, c'è un progetto strettamente collegato che si chiama NanoContainer che fornisce i mezzi necessari per permettere di usare un file di configurazione XML. Questo nano container legge il file XML e configura il sottostante pico container. La filosofia del progetto è di separare il formato del file di configurazione dal meccanismo sottostante.

Per usare il container bisogna scrivere del codice come questo.

 public void testWithPico() {

 MutablePicoContainer pico = configureContainer();

 MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);

 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");

 assertEquals("Once Upon a Time in the West", movies[0].getTitle());

 }

Sebbene in questo esempio io abbia usato l'iniezione sul costruttore, PicoContainer supporta anche l'iniezione sul setter, anche se i suoi sviluppatori preferiscono l'iniezione sul costruttore.

Iniezione sul Setter con Spring

Il framework Spring è un framework ad ampio raggio per lo sviluppo Java enterprise. Include livelli di astrazione per transazioni, persistenza, sviluppo di applicazioni web e JDBC. Come il PicoContainer supporta sia l'iniezione sul costruttore che sul setter, ma i suoi sviluppatori preferiscono quella sul setter - che è anche una scelta appropriata per questo esempio.

Per far sì che il lister di film accetti l'iniezione definisco un metodo setter per quel servizio

class MovieLister...

 private MovieFinder finder;

 public void setFinder(MovieFinder finder) {

 this.finder = finder;

 }

In modo simile definisco un setter per la stringa del finder.

class ColonMovieFinder...

 public void setFilename(String filename) {

 this.filename = filename;

 }

Il terzo passo è preparare la configurazione per i file. Spring supporta una configurazione basta su file XML ed anche sul codice, ma l'XML è il metodologia più appropriata.

 <beans>

 <bean id="MovieLister" class="spring.MovieLister">

 <property name="finder">

 <ref local="MovieFinder"/>

 </property>

 </bean>

 <bean id="MovieFinder" class="spring.ColonMovieFinder">

 <property name="filename">

 <value>movies1.txt</value>

 </property>

 </bean>

 </beans>

Il test sarà quindi così.

 public void testWithSpring() throws Exception {

 ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");

 MovieLister lister = (MovieLister) ctx.getBean("MovieLister");

 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");

 assertEquals("Once Upon a Time in the West", movies[0].getTitle());

 }

Iniezione sull'Interfaccia

La terza tecnica di iniezione è di definire ed usare delle interfacce per l'iniezione. Avalon è un esempio di framework che usa questa tecnica. Ne parlerò un po' di più dopo, ma per ora lo uso con un po' di semplice codice.

Con questa tecnica si inizia definendo un interfaccia che verrà usata per realizzare l'iniezione. Ecco l'interfaccia per iniettare un finder di film dentro un oggetto.

public interface InjectFinder {

 void injectFinder(MovieFinder finder);

}

Questa interfaccia verrebbe definita da chiunque fornisse l'interfaccia MovieFinder. Bisogna che sia implementata da ogni classe che voglia usare un finder, come lo è il lister.

class MovieLister implements InjectFinder...

 public void injectFinder(MovieFinder finder) {

 this.finder = finder;

 }

Si usa un approccio simile per iniettare il nome del file nell'implementazione del finder.

public interface InjectFinderFilename {

 void injectFilename (String filename);

}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename......

 public void injectFilename(String filename) {

 this.filename = filename;

 }

Poi, come al solito, c'è bisogno di un po' di configurazione per collegare insieme le implementazioni. Volendo semplificare, lo facciamo nel codice.

class Tester...

 private Container container;

 private void configureContainer() {

 container = new Container();

 registerComponents();

 registerInjectors();

 container.start();

 }

Questa configurazione ha due parti, la registrazione dei componenti attraverso chiavi di lookup è piuttosto simile agli altri esempi.

class Tester...

 private void registerComponents() {

 container.registerComponent("MovieLister", MovieLister.class);

 container.registerComponent("MovieFinder", ColonMovieFinder.class);

 }

Un nuovo passo consiste nel registrare gli iniettori che inietteranno i componenti dipendenti. Ogni interfaccia di iniezione ha bisogno di un po' di codice per iniettare gli oggetti dipendenti. Di seguito lo faccio registrando l'oggetto iniettore con il container. Ogni oggetto iniettore implementa l'interfaccia iniettore.

class Tester...

 private void registerInjectors() {

 container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));

 container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());

 }

public interface Injector {

 public void inject(Object target);

}

Quando la classe dipendente è una classe scritta per questo container, ha senso che il componente stesso implementi l'interfaccia dell'iniettore, come faccio di seguito con il finder di film. Per le classi generiche, come le stringhe, uso una classe interna dentro il codice di configurazione.

class ColonMovieFinder implements Injector......

 public void inject(Object target) {

 ((InjectFinder) target).injectFinder(this);

 }

class Tester...

 public static class FinderFilenameInjector implements Injector {

 public void inject(Object target) {

 ((InjectFinderFilename)target).injectFilename("movies1.txt");

 }

 }

Così i test usano il container.

class IfaceTester...

 public void testIface() {

 configureContainer();

 MovieLister lister = (MovieLister)container.lookup("MovieLister");

 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");

 assertEquals("Once Upon a Time in the West", movies[0].getTitle());

 }

Il container usa le interfacce di iniezione dichiarate per derivare le dipendenze e gli iniettori per iniettare le dipendenze corrette. (L'implementazione specifica del container che ho fatto qui non è importante per capire la tecnica e quindi non la mostro per non far ridere.)


Usare un Service Locator

La caratteristica chiave di un Iniettore di Dipendenza è rimuovere la dipendenza che la classe MovieLister ha sull'implementazione concreta MovieFinder. Questo mi permette di dare agli amici i lister e a loro di connetterli in un'implementazione adatta al loro ambiente. L'Iniezione non è il solo modo per rompere questa dipendenza, un altro è usare un service locator.

L'idea di base dietro ad un service locator è di avere un oggetto che sa come tenere conto di tutti i servizi necessari ad un'applicazione. Così un service locator per questa applicazione avrebbe un metodo che ritorna un movie finder quando ne serve uno. Naturalmente non cambia di molto il problema; dobbiamo ancora fare che il lister riceva il locator, generando le dipendenze di Figura 3

Figura 3: Le dipendenze per un Service Locator

In questo caso uso il ServiceLocator come un Registry singleton. Il lister può quindi usarlo per trovare il finder quando viene creato.

class MovieLister...

 MovieFinder finder = ServiceLocator.movieFinder();

class ServiceLocator...

 public static MovieFinder movieFinder() {

 return soleInstance.movieFinder;

 }

 private static ServiceLocator soleInstance;

 private MovieFinder movieFinder;

Come nell'approccio dell'iniezione, dobbiamo configurare il service locator. Qui lo scrivo nel codice, ma non è complesso usare un meccanismo che leggesse i dati corretti da un file di configurazione.

class Tester...

 private void configure() {

 ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));

 }

class ServiceLocator...

 public static void load(ServiceLocator arg) {

 soleInstance = arg;

 }

 public ServiceLocator(MovieFinder movieFinder) {

 this.movieFinder = movieFinder;

 }

Ecco il codice di test.

class Tester...

 public void testSimple() {

 configure();

 MovieLister lister = new MovieLister();

 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");

 assertEquals("Once Upon a Time in the West", movies[0].getTitle());

 }

Ho sempre sentito sostenere che questi service locator sono una brutta cosa perché non sono testabili in quanto non si possono sostituire le loro implementazioni. Si possono certamente progettare malamente finendo quindi in questo tipo di guai, ma non bisogna. In questo caso l'oggetto del service locator è un semplice container di dati. Si può creare semplicemente un locator con le implementazioni di test dei servizi.

In un approccio più sofisticato, si può derivare il service locator e passare questa classe alla variabile di registro delle classi. Posso cambiare i metodi statici in modo che chiamino un metodo dell'oggetto anziché leggere direttamente le variabili. Posso fornire service locator specifici per i thread utilizzando uno storage specifico per il thread. Tutto ciò si può fare senza modificare i client del service locator.

Un modo è pensare che il service locator è un registro, non un singleton. Un singleton fornisce un modo semplice di implementare un registro, ma si può cambiarne facilmente l'implementazione.

 Usare una Interfaccia Separata per il Locator

Uno dei problemi dell'approccio semplice visto sopra è che il MovieLister dipende dall'intera classe service locator, anche se usa solo un suo servizio. Possiamo minimizzarlo usando una segregated interface. In questo modo, invece di usare tutta l'interfaccia del service locator, il lister può dichiarare solo quel pezzetto di interfaccia di cui ha bisogno.

In questa situazione chi fornisce il lister fornirebbe anche una interfaccia di locator che serve a questo per ottenere il finder.

public interface MovieFinderLocator {

 public MovieFinder movieFinder();

Il locator deve quindi implementare questa interfaccia per fornire l'accesso ad un finder.

 MovieFinderLocator locator = ServiceLocator.locator();

 MovieFinder finder = locator.movieFinder();

 public static ServiceLocator locator() {

 return soleInstance;

 }

 public MovieFinder movieFinder() {

 return movieFinder;

 }

 private static ServiceLocator soleInstance;

 private MovieFinder movieFinder;

Qua si nota che, in quanto si vuole usare un'interfaccia, non si può semplicemente accedere ai servizi attraverso metodi statici. Bisogna usare la classe per ottenere un oggetto locator e poi usare quello per ottenere il servizio desiderato.

Un Service Locator dinamico

L'esempio visto era statico, in quanto la classe service locator ha i metodi per ogni servizio richiesto. Questo non è il solo modo per farlo, si può anche realizzare un service locator dinamico che permetta di accumulare qualsiasi servizio sia necessario e di sceglierlo poi runtime.

In questo caso, il service locator usa una mappa di servizi invece di un campo per ogni servizio, ed espone metodi generici per ottenere e caricare i servizi.

class ServiceLocator...

 private static ServiceLocator soleInstance;

 public static void load(ServiceLocator arg) {

 soleInstance = arg;

 }

 private Map services = new HashMap();

 public static Object getService(String key){

 return soleInstance.services.get(key);

 }

 public void loadService (String key, Object service) {

 services.put(key, service);

 }

Configurarlo consiste nel caricare un servizio con una chiave opportuna.

class Tester...

 private void configure() {

 ServiceLocator locator = new ServiceLocator();

 locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));

 ServiceLocator.load(locator);

 }

Si usa il servizio specificando la stessa stringa come chiave.

class MovieLister...

 MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

Tutto sommato non mi piace questo approccio. Sebbene sia certamente flessibile, non è molto esplicito. L'unico modo in cui si può raggiungere un servizio è attraverso chiavi testuali. Preferisco metodi espliciti perché è più semplice trovare dove sono guardando semplicemente le definizioni dell'interfaccia.

 Usare sia il locator che l'iniezione con Avalon

I concetti di Iniezione di Dipendenza e di Service Locator non sono per forza mutuamente esclusivi. Un buon esempio di un loro uso congiunto è nel framework Avalon. Avalon utilizza un service locator, ma usa l'iniezione per dire ai componenti dove trovare il locator.

Berin Loritsch mi ha mandato questa semplice versione funzionante del mio esempio per Avalon.

public class MyMovieLister implements MovieLister, Serviceable {

 private MovieFinder finder;

 public void service( ServiceManager manager ) throws ServiceException {

 finder = (MovieFinder)manager.lookup("finder");

 }  

Questo metodo di servizio è un esempio semplice di iniezione di interfaccia, e consente al container di iniettare un service manager nel MyMovieLister. Il service manager è un esempio di service locator. In questo esempio il lister non memorizza il manager in un campo, ma lo usa immediatamente per effettuare la ricerca del Finder e memorizzarlo.


Decidere quale opzione usare

Finora mi sono concentrato a spiegare come vedo questi pattern e le loro variazioni. Ora posso iniziare a parlare dei pro e contro per aiutare a farsi un'idea di quale usare e quando.

Service Locator contro Dependency Injection

La scelta fondamentale è tra il Service Locator e l'Iniezione di Dipendenza. Il primo punto è che entrambe le implementazioni forniscono il disaccoppiamento fondamentale che manca nell'esempio semplice – in entrambi i casi il codice dell'applicazione dipende dall'implementazione concreta dell'interfaccia del servizio. La differenza principale tra i due pattern è su come quell'implementazione è fornita alla classe dell'applicazione. Con il service locator la classe dell'applicazione la chiede esplicitamente con un messaggio al locator. Con l'iniezione non c'è una richiesta esplicita, il servizio appare nella classe applicazione – da qui l'inversione di controllo.

L'inversione di controllo è una caratteristica comune dei framework, ma è qualcosa che si ottiene ad un prezzo. Tende ad essere difficile da capire e porta a problemi quando si prova a fare debug. Così, tutto sommato, preferisco evitarla a meno che non ne abbia proprio bisogno. Ciò non significa che è una cosa sbagliata, solo che penso che debba essere giustificata rispetto ad alternative più dirette.

Le differenza base è che con un Service Locator ogni utente di un servizio ha una dipendenza dal locator. Il locator può nascondere le dipendenza alle altre implementazioni, ma bisogna vedere il locator. Così la decisione tra il locator e l'iniezione dipende se quella dipendenza è un problema.

L'uso dell'iniezione di dipendenza può aiutare a rendere più facile vedere quali sono le dipendenze del componente. Coll'iniezione di dipendenza, basta guardare il meccanismo di iniezione, come il costruttore, e vedere le dipendenze. Col service locator bisogna cercare nel codice sorgente le chiamate al locator. Gli IDE moderni con la capacità di trovare i riferimenti lo rendono più semplice, ma non è semplice come guardare un costruttore o un metodo setter.

Molto dipende dalla natura dell'utente del servizio. Se si sta costruendo un'applicazione con svariate classi che usano un servizio, allora una dipendenza dalla classe applicazione al locator non è un grosso problema. Nel mio esempio di dover passare il MovieLister ai miei amici, l'uso di un service locator funziona abbastanza bene. Tutto ciò che loro hanno bisogno di sapere è di configurare il locator ad agganciarsi alla giusta implementazione del servizio, sia attraverso del codice di configurazione o attraverso un file di configurazione. In questo tipo di scenario non vedo l'inversione dell'iniettore come qualcosa che dia un beneficio.

La differenza si vede se il lister è un componente che si sta fornendo ad un'applicazione che altre persone devono scrivere. In questo caso non so molto dell'API del service locator che i miei clienti useranno. Ogni cliente potrebbe avere i propri service locator incompatibili con quelli degli altri. Si può ovviare in qualche modo usando un'interfaccia separata. Ogni cliente può scrivere un adapter che coincida con la mia interfaccia verso il loro locator, ma in ogni caso c'è ancora bisogno di vedere il primo locator per cercare l'interfaccia specifica. E una volta che l'adapter fa la sua comparsa la semplicità della connessione diretta al locator inizia a traballare.

Siccome con un iniettore non c'è una dipendenza dal componente verso l'iniettore, il componente non può ottenere ulteriori servizi dall'iniettore una volta che è stato configurato.

Un motivo comune per cui la gente finisce per preferire l'iniezione di dipendenza è che rende più semplice il testing. Il punto è che per fare il testing, c'è bisogno di rimpiazzare le implementazioni reali del servizio con stub o mock. Comunque non c'è di fatto differenza tra l'iniezione di dipendenza e il service locator: entrambi sono molto soggetti ad diventare stub. Sospetto che questa osservazione arrivi dai progetti dove la gente non compie lo sforzo necessario a far sì che i loro service locator siano facilmente sostituibili. Qui è dove il test continuo aiuta, se non si può facilmente fare stub dei servizi per il test, questo implica che ci sono seri problemi di progetto.

Naturalmente il problema dei test viene esacerbato da quegli ambienti di componenti che sono molto invasivi, come i framework EJB di Java. La mia visione è che queste tipologie di framework dovrebbero minimizzare il loro impatto sul codice dell'applicazione, ed in particolare non dovrebbero fare cose che rallentino il ciclo modifica-esegui. Usare delle plugin per sostituire componenti heavyweight dà un grande aiuto a questo processo, che è vitale per pratiche come il Test Driven Development.

Così il problema principale riguarda coloro che scrivono codice che deve essere usato in applicazioni fuori dal loro controllo. In questi casi anche la più piccola assunzione su un Service Locator è un problema.

Iniezione sul costruttore contro iniezione sul setter

Per assemblare i servizi, bisogna sempre avere qualche convenzione per collegare le cose tra loro. Il vantaggio dell'iniezione è principale che richiede convenzioni molto semplici – almeno per l'iniezione sul costruttore e sul setter. Non bisogna fare niente di strano nei componenti ed è abbastanza lineare per un iniettore configurare tutto.

L'iniezione di interfaccia è più invaiava in quanto bisogna scrivere un sacco di interfacce per sistemare tutte le cose. Per un piccolo insieme di interfacce richieste dal container, come nell'approccio di Avalon, non è malissimo. Ma c'è un sacco di lavoro per assemblare i componenti e le dipendenze, motivo per cui il gruppo di lightwaight container di oggi funzionano con l'iniezione sul costruttore o sul setter.

La scelta tra iniezione sul costruttore o sul setter è interessante in quanto rispecchia un problema più generale della programmazione object-oriented – bisogna riempire i campi col costruttore o con i setter.

La mia lunga esperienza con gli oggetti è di creare il più possibile oggetti validi al momento della costruzione. Questo suggerimento fa riferimento allo Smalltalk Best Practice Patterns di Kent Beck: Construcotr Method and Constructor Parameter Method. i costruttori con parametri forniscono un chiara idea di cosa significhi creare un oggetto valido in un luogo ovvio. Se c'è più di un moto per farlo, basta creare più costruttori che mostrano le diverse combinazioni.

Un altro vantaggio con l'inizializzazione del costruttore è che permette di nascondere chiaramente qualsiasi campo che sia immutabile semplicemente non fornendo un setter. Penso che ciò sia importante – se qualcosa non dovrebbe cambiare, la mancanza di un setter lo comunica molto bene. Se si usano i setter per l'inizializzazione, allora può diventare doloroso. (Infatti in queste situazioni io preferisco evitare la solita convenzione di setting, preferisco un metodo tipo initFoo, per sottolineare che è qualcosa da fare alla nascita).

Ma per ogni situazione ci sono eccezioni. Se si hanno un sacco di parametri del costruttore le cose possono sembrare confuse, in particolare con linguaggi senza parametri con keyword. Resta vero che un costruttore lungo è spesso segno di un oggetto sovra impegnato che dovrebbe essere spezzato, ma ci sono casi in cui questo è ciò che serve.

Se ci sono più modi per costruire un oggetto valido, può essere difficile mostrarlo coni costruttori, in quanto i costruttori possono solo cambiare nel numero e tipo di parametri. Qui è dove i Factory Method entrano in gioco, essi possono usare una combinazione di costruttori privati e setter per fare il loro lavoro. Il problema con i Factory Method classici per assemblare componenti è che essi sono visti di solito con metodi statici, e non si possono avere sulle interfacce. Si possono fare classi Factory, ma poi quella diventa un altro oggetto service. Un servizio factory è spesso una buona tattica, ma bisogna ancora creare la factory usado una delle tecniche qui esposte.

I costruttori soffrono anche se si hanno parametri semplici come stringhe. Con l'iniezione sul setter si può dare ad ogni setter un nome per indicare cosa la stringa dovrebbe fare. Con i costruttori ci si basa solo sulla posizione, che è più difficile da seguire.

Se ci sono poi più costruttori e l'ereditarietà, le cose possono diventare ulteriormente ineleganti. Per inizializzare tutto bisogna fornire dei costruttori che invochino tutti i costruttori della classe superiore, e inoltre aggiungano i propri argomenti. Ciò può portare ad un esplosione ancora più grande di costruttori.

Nonostante questi svantaggi la mia preferenza è iniziare con l'iniezione sul costruttore, ma mi tengo pronto a passare all'iniezione sul setter appena i problemi che ho illustrato diventano un problema.

Questo argomento ha portato a molte discussioni tra i diversi team che forniscono l'iniezione di dipendenza come parte del loro framework. comunque sembra che la maggior parte della gente che crea questi framework abbia capito che è importante supportare entrambi i meccanismi, anche se hanno una preferenza per l'uno o per l'altro.

Codice e file di configurazione

Un argomento indipendente ma spesso conflittuale è se usare file o codice su un API di configurazione per collegare i servizi. Per la maggior parte delle applicazioni che sono tendenzialmente da approntare in molti posti, un file di configurazione separato ha generalmente più senso. Nella quasi totalità delle volte questo è un file XML e ciò ha senso. Comunque ci sono casi dove è più facile usare il codice del programma per fare l'assemblaggio. Un caso è dove si ha un'applicazione semplice che non viene approntata in tante varianti. Un questo caso un po' di codice risulta più chiaro che un file XML separato.

Un caso contrastante è dove l'assemblaggio è piuttosto complesso, in quanto coinvolge passi opzionali. Una volta che si incomincia ad avvicinarsi ad un linguaggio di programmazione allora l'XML inizia a fare acqua ed è meglio usare un vero linguaggio che ha la sintassi completa per scrivere programmi chiari. Quindi si scrive una classe builder che faccia l'assemblaggio. Se si hanno diversi scenari di build, si possono fornire diversi builder e usare un semplice file di configurazione per sceglierne uso tra essi.

Penso spesso che la gente sia più che diligente nel definire file di configurazione. Spesso un linguaggio di programmazione diventa un meccanismo di configurazione potente e diretto. I linguaggi moderni possono compilare facilmente piccoli assemblatori che possono essere usati per assemblare plugin per sistemi più grandi. Se la compilazione è una sofferenza allora ci sono linguaggi di scripting che funzionano altrettanto bene.

Si dice spesso che i file di configurazione non dovrebbero usare un linguaggio di programmazione perché hanno bisogno di essere modificati da non programmatori. Ma quando spesso? Ci si aspetta veramente che non programmatori cambino il livello di isolazmento delle transazioni fi un'applicazione server-side complessa? I file di configurazione non linguaggio funzionano bene solo finché restano semplici. Se diventano complessi allora è l'ora di pensare di usare un linguaggio di programmazione vero e proprio.

Una cosa che si nota nel mondo Java al momento è la cacofonia di file di configurazione, dove ogni componente ha il proprio file di configurazione che è diverso dagli altri. Se su usa una dozzina di questi componenti si finisce con facilità con una dozzina di file di configurazione da tenere aggiornati.

Il mio suggerimento è di fornire sempre un modo per fare tutta la configurazione facilmente con un'interfaccia peogrammatica e poi trattare un file di configurazione separato come un'aspetto opzionale. Si può fare realizzare facilmente un gestore di file i conigurazione che usi l'interfaccia programmatica. Se si sta scrivendo un componente è meglio lasciare all'utente la decisione se scegliere l'uso dell0interfaccai progrmmatica, il file di configurazione o scrivere un proprio formato di file di configurazione e collegarlo all0interfaccai programmatca.

Separare la configurazione dall'uso

L'argomento importante in tutto ciò è di assicurare che la configurazione dei servizi sia separata dal loro uso. Infatti questo è un principio di progetto fondamentale che siede con la separazione delle interfacce dall'implemetazione. È qualcosa che qualcosa che si vede in un progrmama object-oriented quando la logica condizionale decide quale classe creare e poi le future valutazioni di quella condizione vengono fatte attraverso il polimorfismo anziché attraverso codice condizionale duplicato.

Se questa separazione è utile all0interno di un singolo codice base, diventa particolarmente vitale quando si usano elementi esterni come componenti o servizi. La prima domanda è se si desidera differire la scelta della classe implementazione a particolari messe in opera. Se sì, c'è bisogno di usare qualche implementazione di plugin. una volta che si usano plugin è essenziale che l'assemblaggio delle plugin sia fatto separatamente dal resto dell'applicazione cosicché si possono sostituire diverse configurazioni inmodo facile per diverse messe in opera. Come ottenerlo è secondario. Questo meccanismo di configurazione può sia configurare un service locator o usare l'iniezione per configurare gli oggetti direttamente.


Alcuni ulteriori argomenti

In questo articolo mi sono concentrato sugli argomenti base della configurazione di servizi usando l'Iniezione di Dipendenza e il Service locator. Ci sono alcuni ulteriori argomenti che hanno un ruolo che meritano anche attenzione, ma non ho ancora abuto tempo di scavarci a fondo. in particolare ci sono problemi di comportamento del ciclo di vita. Alcuni componenti hanno eventi di ciclo di vita diversi: fine ed inizio per esempio. Un altro problema è il crescente interesse nell'usare idee aspect oriented con questi container. Sebbene io non abbia considerato questo materiale nel presente articolo, spero di scrivere di più se questo estendendo questo articolo o scrivendone un altro.

Si può trovare molto di più su queste idee guardando sui siti web votati ai lightweight container. Navigare a partire da i siti di picocontainer e spring vi porterà in molte più discussione di questi problemi e all'inizio di alcuni ulteriori problemi.


Pensieri conclusivi

Nell'attuale corsa sui lightweight container tutti hanno un pattern di base comune su come fare l'assemblaggio sei servizi – il pattern dell'iniezone di dipendenza. L'iniezione di Dipendenza è un'utile alternative al Service Locator. Quando si costruiscono classi di applicazione i due sono grosso modo equivalenti, ma Penso che il Service Locator abbia un piccolo vantaggio dovuto al suo comportamente più diretto. Comunque se si costruiscono classi che devono essere utilizzate in diverse applicazioni allora la mi glior scelta è l'Iniezione di Dipendenza.

Se si usa l'Iniezione di Dipendenza ci sono un certo numero di stili tra cui scegliere. Suggerisco di seguire l'iniezione sul costruttore fin quando si finisce in uno dei problemi specifici di questo approccio, nel qual caso spostarsi all'iniezione sul setter. Se si sceglie di costruire o utilizzare un container, cercatene uno che supporti entrambe le iniezioni.

La scelta tra Serivce Locator e Iniezione di Dipendenza è meno importante del principio di separare la configurazione del servizio dall'uso dei servizi all'interno di un'applicazione.



Ringraziamenti

I miei sinceri ringraziamenti alle molte persone che mi hanno aiutato con questo articolo. Rod Johnson, Paul Hammant, Joe Walnes, Aslak Hellesøy, Jon Tirsén and Bill Caputo mi hanno aiutato ad afferrare questi concetti e hanno commentato le prime bozze di questo articolo. Berin Loritsch and Hamilton Verissimo de Oliveira mi hanno dato alcuni suggerimenti molto utili su come andar d'accordo con Avalon. Dave W Smith ha insistito a far domande sulla mio codice di configurazione iniziale dell'iniezione di interfaccia, mettendomi davanti il fatto che era stupido

Revisioni significative

23 gennaio 04: Rifatto il codice di configurazione dell'esempio dell'iniezione sull'interfaccia.

16 gennaio 04: Aggiunto un breve esempio sia del locator che dell'iniezione con Avalon.

14 gennaio 04: Prima pubblicazione


Tradotto da Marco Perrando.

Nota sul Traduttore: Marco Perrando lavora nel campo dello sviluppo software come sviluppatore e progettista da oltre 10 anni. Attualmente lavora per la SIR - Soluzioni in Rete di Genova.



© Copyright Martin Fowler, tutti i diritti riservati