Approfondimento
Dependency Injection
Sandro Pedrazzini
Dependency Injection
1
Dependency Injection (DI)
• Modalità di configurazione di un oggetto, in cui le
dipendenze dell’oggetto vengono specificate da entità
esterne
• L’alternativa è che l’oggetto stesso si definisca da solo, al
suo interno, le dipendenze
• Anche chiamata Inversion of Control (IoC), che però ha un
significato più esteso (principio del framework in generale)
Sandro Pedrazzini
Dependency Injection
2
Esempio
• Classe che gestisce l’esecuzione del pagamento di un certo
ordine (BillingService)
• Sia la classe responsabile dell’elaborazione dei dati della
carta di credito (CreditCardProcessor), sia la classe
responsabile di mantenere le informazioni di log
(TransactionLog) del pagamento devono poter essere
modificate
Sandro Pedrazzini
Dependency Injection
3
Esempio (2)
Sandro Pedrazzini
Dependency Injection
4
Esempio (2)
• BillingService carica l’ordine sulla carta di credito. La
transazione verrà registrata (log) sia in caso di riuscita, sia
in caso di insuccesso
public interface IBillingService {
Receipt chargeOrder(Order order, CreditCard creditCard);
}
Sandro Pedrazzini
Dependency Injection
5
Esempio (3)
• Schema di elaborazione
public Receipt chargeOrder(Order order, CreditCard creditCard) {
…
ChargeResult result =
processor.process(order.getAmount(), creditCard);
transactionLog.logChargeResult(result);
…
}
Sandro Pedrazzini
Dependency Injection
6
Esempio (4)
public class BillingService implements IBillingService {
public Receipt chargeOrder(Order order, CreditCard creditCard) {
ICreditCardProcessor processor = new PaypalCreditCardProcessor();
ITransactionLog transactionLog = new DatabaseTransactionLog();
try {
ChargeResult result =
processor. process(order.getAmount(), creditCard);
transactionLog.logChargeResult(result);
return ...
} catch (UnreachableException e) {
...
}
}
}
Sandro Pedrazzini
Dependency Injection
7
Esempio (4)
Situazione attuale
Sandro Pedrazzini
Dependency Injection
8
Commenti
• Il codice precedente pone problemi di modularità e di test
• La dipendenza diretta, in compilazione, all’elaboratore di
carta di credito significa che ogni chiamata al codice,
anche durante il test, esegue una transazione reale sulla
carta di credito
ICreditCardProcessor processor = new PaypalCreditCardProcessor();
Sandro Pedrazzini
Dependency Injection
9
Obiettivo
Sandro Pedrazzini
Dependency Injection
10
Factory
• Una factory permette di separare la classe client
(BillingService) dalla classe che implementa un servizio
(classi concrete di CreditCardProcessor e TransactionLog)
• Una factory semplice utilizza alcuni metodi static per
stabilire quale implementazione collegare a una data
interface
Sandro Pedrazzini
Dependency Injection
11
Factory (2)
public class CreditCardProcessorFactory {
private static ICreditCardProcessor instance;
public static void setInstance(ICreditCardProcessor processor) {
instance = processor;
}
public static ICreditCardProcessor getInstance() {
if (instance == null) {
throw new IllegalStateException("Factory not initialized”);
}
return instance;
}
}
Sandro Pedrazzini
Dependency Injection
12
Factory (3)
public class RealBillingService implements IBillingService {
public Receipt chargeOrder(Order order, CreditCard creditCard) {
ICreditCardProcessor processor =
CreditCardProcessorFactory.getInstance();
ITransactionLog transactionLog =
TransactionLogFactory.getInstance();
try {
ChargeResult result =
processor.process(order.getAmount(), creditCard);
transactionLog.logChargeResult(result);
return ...
} catch (UnreachableException e) {
...
}
}
}
Sandro Pedrazzini
Dependency Injection
13
Factory (4)
CreditCardFactory
TransactionLogFactory
Sandro Pedrazzini
Dependency Injection
14
Factory (5)
• La scelta dell’implementazione di ICreditCardProcessor e
ITransactionLog viene fatta attraverso le factory
• In questo modo si diminuisce la dipendenza esistente tra
BillingService e queste classi
• L’implementazione dei test diventa più semplice
Sandro Pedrazzini
Dependency Injection
15
Unit Test
public class BillingServiceTest extends TestCase {
private ITransactionLog transactionLog = new InMemoryTransactionLog();
private ICreditCardProcessor processor = new FakeCreditCardProcessor();
public void setUp() {
TransactionLogFactory.setInstance(transactionLog);
CreditCardProcessorFactory.setInstance(processor);
}
@Test
public void testBilling() {
...
}
public void tearDown() {
TransactionLogFactory.setInstance(null);
CreditCardProcessorFactory.setInstance(null);
}
Sandro Pedrazzini
Dependency Injection
16
Unit Test (2)
...
@Test
public void testBilling() {
IBillingService billingService = new BillingService();
Order order = new PizzaOrder(100);
CreditCard creditCard = new CreditCard("1234", 11, 2010);
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertTrue(transactionLog.successfullyLogged());
}
}
Sandro Pedrazzini
Dependency Injection
17
Commenti
• Codice problematico: le implementazioni vengono
praticamente gestite in variabili globali (static)
• Se il tearDown() dovesse venir interrotto per qualche
motivo, avremmo l’implementazione di test che rimane
nella factory e potrebbe creare problemi ad altri test
• Non è possibile eseguire più test in parallelo (variabili
static)
Sandro Pedrazzini
Dependency Injection
18
Commenti (2)
• La dipendenza è nascosta nel codice
• Se per un motivo qualsiasi la factory venisse inizializzata in
modo sbagliato, bisognerebbe attendere il primo
pagamento per accorgersi del problema
• La cosa si complicherebbe se il numero di factory dovesse
crescere
Sandro Pedrazzini
Dependency Injection
19
Dependency Injection
• Con questo pattern si fa un passo ulteriore verso la
separazione tra comportamento e risoluzione della
dipendenza
• BillingService non è più responsabile della risoluzione,
perché gli oggetti ICreditCardProcessor e ITransactionLog
vengono passati come parametri
Sandro Pedrazzini
Dependency Injection
20
Dependency Injection (2)
• Gli oggetti dipendenti vengono forniti a BillingService
dall’esterno, attraverso il costruttore (o attraverso un metodo
set())
public BillingService(ICreditCardProcessor creditCardProcessor,
ITransactionLog transactionLog) {
fCreditCardProcessor = creditCardProcessor;
fTransactionLog = transactionLog;
}
Sandro Pedrazzini
Dependency Injection
21
Dependency Injection (3)
public class BillingService implements IBillingService {
private ICreditCardProcessor fCreditCardProcessor;
private ITransactionLog fTransactionLog;
public BillingService(ICreditCardProcessor creditCardProcessor,
ITransactionLog transactionLog) {
fCreditCardProcessor = creditCardProcessor;
fTransactionLog = transactionLog;
}
public Receipt chargeOrder(Order order, CreditCard creditCard) {
try {
ChargeResult result=
fCreditCardProcessor.process(order.getAmount(), creditCard);
fTransactionLog.logChargeResult(result);
return ...
} catch (Exception e) {
...
}
}
}
Sandro Pedrazzini
Dependency Injection
22
Dependency Injection (4)
Injector
Sandro Pedrazzini
Dependency Injection
23
Commenti
• Non vengono più usate factory, quindi eliminata la
dipendenza tra BilingService e factory
• Si possono modificare i test, eliminando setUp() e
tearDown()
Sandro Pedrazzini
Dependency Injection
24
Unit test
public class BillingServiceTest {
private ICreditCardProcessor processor = new FakeCreditCardProcessor();
private ITransactionLog transactionLog = new InMemoryTransactionLog();
public void testSuccessfulCharge() {
Order order = new PizzaOrder(100);
CreditCard creditCard = new CreditCard("1234", 11, 2010);
IBillingService billingService =
new BillingService(processor, transactionLog);
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertTrue(transactionLog.successfullyLogged());
}
}
Sandro Pedrazzini
Dependency Injection
25
Commenti
• Con il passaggio delle dipendenze al costruttore, per ogni
nuova dipendenza si deve aggiungere un parametro. In
questo modo il compilatore ci può avvertire se nel test ci
sono dipendenze da inserire: le dipendenze vengono
esposte via API
• Ora le classi client di IBillingService devono gestire loro le
dipendenze
Sandro Pedrazzini
Dependency Injection
26
Generalizzazione
• Si può andare indietro nella catena di dipendenze (classi che
dipendono da un’implementazione di IBillingService)
• Queste classi dovranno accettare IBillingService come
parametro
• Il punto di fermata saranno le classi “top-level”
Sandro Pedrazzini
Dependency Injection
27
Generalizzazione (2)
• Per gestire le dipendenze nelle classi “top-level” è utile
usare un framework, che aiuti a ricostruire la gerarchia di
dipendenze
• Ne esistono diversi
–
–
–
–
Sandro Pedrazzini
Bean Container di Spring
Guice
PicoContainer
…
Dependency Injection
28
DI con Guice
• Il pattern DI permette di scrivere codice più modulare e più
facilmente testabile
• Framework come Guice ne facilitano l’implementazione
• Permettono di associare le interface alle implementazioni
necessarie nei vari contesti
Sandro Pedrazzini
Dependency Injection
29
DI con Guice (2)
• La configurazione è specificata in un modulo Guice, una
classe che implementa l’interfaccia Module
public class BillingModule extends AbstractModule {
protected void configure() {
bind(ITransactionLog.class).to(DatabaseTransactionLog.class);
bind(ICreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(IBillingService.class).to(BillingService.class);
}
}
Sandro Pedrazzini
Dependency Injection
30
DI con Guice (3)
• Inserendo @Inject nel costruttore di BillingService, si
indica a Guice di passare le dipendenze come specificate
nel modulo
• Nota: non è necessario che le dipendenze vengano
specificate con interface. La relazione classe-sottoclasse
basta
Sandro Pedrazzini
Dependency Injection
31
DI con Guice (4)
public class BillingService implements IBillingService {
private ICreditCardProcessor fCreditCardProcessor;
private ITransactionLog fTransactionLog;
@Inject
public BillingService(ICreditCardProcessor creditCardProcessor,
ITransactionLog transactionLog) {
fCreditCardProcessor = creditCardProcessor;
fTransactionLog = transactionLog;
}
public Receipt chargeOrder(Order order, CreditCard creditCard) {
...
}
}
Sandro Pedrazzini
Dependency Injection
32
DI con Guice (5)
• Injectable properties: non è necessario prevedere il costruttore
per l’inizializzazione
public class BillingService implements IBillingService {
@Inject
private ICreditCardProcessor fCreditCardProcessor;
@Inject
private ITransactionLog fTransactionLog;
public Receipt chargeOrder(Order order, CreditCard creditCard) {
...
}
}
Sandro Pedrazzini
Dependency Injection
33
Utilizzo
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
//ricevel’oggetto top level
IBillingService billingService = injector.getInstance(IBillingService.class);
Receipt receipt = billingService.chargeOrder(
new PizzaOrder(100), new CreditCard("1234", 6, 2012));
if (receipt.hasSuccessfulCharge()) {
System.out.println("Receipt value: " + receipt.getAmountOfCharge());
} else {
System.out.println("Receipt value: " + receipt.getErrorMessage());
}
}
Sandro Pedrazzini
Dependency Injection
34
Altri punti di injection (2)
• Anche un metodo può essere segnalato con @Inject.
• Guice chiama questo metodo con l’oggetto associato subito
dopo aver chiamato il costruttore
• Non dev’essere necessariamente un metodo set(), può avere
più parametri e non dev’essere per forza pubblico
@Inject
public void setTransactionLog(ITransactionLog transactionLog){
fTransactionLog = transactionLog;
}
Sandro Pedrazzini
Dependency Injection
35
Utilizzo di Guice per test (1)
public class TestModule extends AbstractModule {
protected void configure() {
bind(ITransactionLog.class).to(InMemoryTransactionLog.class);
bind(ICreditCardProcessor.class).to(FakeCreditCardProcessor.class);
bind(IBillingService.class).to(BillingService.class);
}
}
Sandro Pedrazzini
Dependency Injection
36
Utilizzo di Guice per test (2)
public class BillingServiceWithGuiceTest {
private Injector injector = Guice.createInjector(new TestModule());
@Test
public void testSuccessfulCharge() {
Order order = new PizzaOrder(100);
CreditCard creditCard = new CreditCard("1234", 11, 2010);
IBillingService billingService =
injector.getInstance(IBillingService.class);
Receipt receipt = billingService.chargeOrder(order, creditCard);
assertTrue(receipt.hasSuccessfulCharge());
assertEquals(100, receipt.getAmountOfCharge());
assertTrue(billingService.getTransactionLog().successfullyLogged());
}
}
Sandro Pedrazzini
Dependency Injection
37
Provider
• Capita che per la creazione di un oggetto sia necessario più
codice che la semplice chiamata al costruttore
• Normalmente in questi casi scriveremmo un metodo factory,
che contenga la chiamata al costruttore, più il codice
necessario
• In Guice il metodo factory viene specificato nel modulo di
binding e viene chiamato @Provides method, dal nome
dell’annotation da applicare
• Il binding in configure() va tolto
Sandro Pedrazzini
Dependency Injection
38
Metodo @Provides
public class BillingModule extends AbstractModule {
protected void configure() {
// bind(ITransactionLog.class).to(DatabaseTransactionLog.class);
...
}
@Provides
ITransactionLog provideTransactionLog() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setJdbcUrl("jdbc:mysql://localhost/trans");
transactionLog.setThreadPoolSize(30);
return transactionLog;
}
}
Sandro Pedrazzini
Dependency Injection
39
Classe Provider
• Quando un metodo @Provides inizia ad essere troppo
complesso, si può pensare di creare una classe Provider
dedicata
• Guice prevede a questo scopo un’interfaccia Provider che va
implementata
public interface Provider<T> {
T get();
}
Sandro Pedrazzini
Dependency Injection
40
Classe Provider (2)
Classe responsabile della creazione dell’oggetto ITransactionLog
public class DBTransactionProvider implements Provider<ITransactionLog> {
public ITransactionLog get() {
DatabaseTransactionLog transactionLog =
new DatabaseTransactionLog();
transactionLog.setJdbcUrl("jdbc:mysql://localhost/trans");
transactionLog.setThreadPoolSize(30);
return transactionLog;
}
}
Sandro Pedrazzini
Dependency Injection
41
Classe Provider (3)
• Binding nella classe di modulo
public class BillingModule extends AbstractModule {
protected void configure() {
bind(ITransactionLog.class).
toProvider(DBTransactionProvider.class);
...
}
...
}
Sandro Pedrazzini
Dependency Injection
42
Request Injector
• Necessario quando all’interno di configure si deve instanziare
un oggetto da usare in modo particolare: si crea e si forza il
passaggio delle dipendenze ai campi @Inject
public class BillingModule extends AbstractModule {
protected void configure() {
bind(ITransactionLog.class).to(DatabaseTransactionLog.class);
bind(ICreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
IBillingService billingService = new BillingService();
//necessario forzare injection
requestInjection(billingService);
…
}
Sandro Pedrazzini
Dependency Injection
43
Utilizzo contemporaneo di più moduli
• Più moduli possono essere usati contemporaneamente, se è
necessario usare oggetti della stessa classe, ma inizializzati
diversamente
• L’utilizzo di più moduli può essere utile anche a cascata: se un
oggetto di un modulo dev’essere creato manualmente
all’interno di un provider, se ha dipendenze, queste potrebbero
essere specificate all’interno di un secondo modulo.
Sandro Pedrazzini
Dependency Injection
44
Utilizzo contemporaneo di più moduli (2)
public class DBTransactionProvider implements Provider<ITransactionLog> {
public ITransactionLog get() {
Injector injector = Guice.createInjector(new DBProductionModule());
ITransactionLog transactionLog =
injector.getInstance(ITransactionLog.class);
…
return transactionLog;
}
}
Sandro Pedrazzini
Dependency Injection
45
Scope
• Per default, Guice restituisce una nuova istanza ogni volta che
un oggetto viene richiesto
• Esempio
– Ogni colta che si richiede un oggetto ITransactionLog a questo modulo, viene
creata una nuova istanza di InMemoryTransactionLog
bind(ITransactionLog.class).
to(InMemoryTransactionLog.class);
Sandro Pedrazzini
Dependency Injection
46
Scope (2)
• Esistono altri scope, a scelta:
– per l’intero ciclo di vita dell’applicazione (@Singleton)
– per la durata di una sessione (@SessionScoped)
– per una singola richiesta (@RequestScoped)
• Esempio
– Scope configurato durante il binding:
bind(ITransactionLog.class).
to(InMemoryTransactionLog.class).
in(Singleton.class);
Sandro Pedrazzini
Dependency Injection
47
Scope (3)
• Eager singleton
– Guice prevede la possibilità di specificare oggetti singleton da costruire
subito, in anticipo (eagerly)
bind(ITransactionLog.class).
to(InMemoryTransactionLog.class).
asEagerSingleton();
Sandro Pedrazzini
Dependency Injection
48
DI con Spring
• Il Bean Container di Spring risolve le dipendenze durante il
caricamento dei singoli oggetti
• Le dipendenze vengono specificate in un file di
configurazione
• Le dipendenze possono essere specificate, con
l’annotation @Autowired
• Le dipendenze possono essere passate via costruttore o
via metodi “set”
Sandro Pedrazzini
Dependency Injection
49
Definizione di Bean
• Le definizioni dei Bean all’interno di una variante di
BeanFactory sono rappresentate con oggetti
BeanDefinition, contenenti almeno (anche solo
implicitamente):
– nome o id
– nome della classe
– elementi di configurazione del comportamento (prototype o singleton,
inizializzazione, etc.)
– argomenti di costruttore e property values da assegnare al bean creato
– altri bean necessari al bean considerato per eseguire il suo lavoro
(dipendenze)
<bean id="exampleBean”
class="examples.ExampleBean"/>
Sandro Pedrazzini
Dependency Injection
50
Singleton
• I Bean sono definiti per essere installati e attivati in due
modalità: singleton o non-singleton (prototype)
• Altre modalità sono disponibili per ApplicationContext di
applicazioni Web (request, session, global session
(portlet))
• Quando un bean è un singleton, un’unica instanza (shared)
del bean viene gestita e creata (per bean e container)
• Per default i bean sono attivati in modalità singleton
Sandro Pedrazzini
Dependency Injection
51
Singleton vs. Prototype
Singleton (per bean e contesto)
Sandro Pedrazzini
Dependency Injection
52
Singleton vs. Prototype
• Prototype (istanza)
Sandro Pedrazzini
Dependency Injection
53
Proprietà e Dipendenze
• Le dipendenze possono essere specificate con costruttore
o con metodi set
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne” ref="anotherExampleBean"/></property>
<property name="beanTwo” ref="yetAnotherBean"/></property>
<property name="integerProperty"><value>1</value></property>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
Sandro Pedrazzini
Dependency Injection
54
Spring DI (3)
Esempio
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne” ref=.../></property>
<property name="beanTwo” ref=.../></property>
<property name="integerProperty">
<value>1</value>
</property>
</bean>
public class ExampleBean {
private AnotherBean fBeanOne;
private YetAnotherBean fBeanTwo;
private int fValue;
public void setBeanOne(AnotherBean beanOne) {
fBeanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
fBeanTwo = beanTwo;
}
public void setIntegerProperty(int value) {
fValue = value;
}
}
Sandro Pedrazzini
Dependency Injection
55
BillingService con Spring
• Specificare metodi set per RealBillingService
public class BillingService implements IBillingService {
private ICreditCardProcessor fCreditCardProcessor;
private ITransactionLog fTransactionLog;
...
public void setCreditCardProcessor(ICreditCardProcessor processor) {
fCreditCardProcessor = processor;
}
public void setTransactionLog(ITransactionLog transactionLog) {
fTransactionLog = transactionLog;
}
...
}
Sandro Pedrazzini
Dependency Injection
56
BillingService con Spring (2)
<bean id=”billingServiceBean"
class="examples.BillingService>
<property name=”creditCardProcessor” ref="creditCardProcessor"/>
<property name=”transactionLog” ref="transactionLog"/>
</bean>
<bean id=”creditCardProcessor"
class="examples.PaypalCrediCardProcessor”/>
<bean id=”transactionLog"
class="examples.DatabaseTransactionLog”/>
Sandro Pedrazzini
Dependency Injection
57
Confronto: Guice e Spring
• Spring rappresenta un intero stack di elementi per applicazioni
enterprise, di cui DI è un tassello
• Si può comunque usare Spring anche solo per DI (Spring non è
un framework “all or nothing”)
• Guice si concentra invece puramente sugli aspetti di DI (e
AOP)
Sandro Pedrazzini
Dependency Injection
58
Confronto: Guice e Spring (2)
• La configurazione Spring (esplicita) avviene in XML, quindi
scomoda e poco concisa (tool di sviluppo che offrono una
buona integrazione tra Java e XML aiutano comunque a
rendere mantenibili le applicazioni)
• Esiste in Spring una modalità di configurazione “autowired”
• Guice utilizza annotations, una buona via di mezzo: una
modalità concisa, esplicita, mantenibile e supportata dal
linguaggio di programmazione
Sandro Pedrazzini
Dependency Injection
59
Scarica

Dependency Injection