Quando si scrivono i test, la corretta gestione delle dipendenze (Dependency Injection) è uno degli aspetti più rilevanti e molte volte le best practices per l’utilizzo di un Dependency injector ed una libreria di Mocking sono le stesse.
In questa presentazione si cerca di capire quando un Dependency injector rappresenta un Anti-pattern e quando invece diventa un valido strumento professionale per risparmiare tempo, ridurre gli errori, scrivere meno codice e rendere l’applicazione molto flessibile.
Tutto questo però senza sacrificare il design dei nostri oggetti e legarci in modo indissolubile ad un framework.
1. Dependency Injection:
The Good Parts
@massimogroppel1
groppedev
MASSIMO GROPPELLI
Senior Software Developer at
Zucchetti
TDD Milano (Test Driven Development)
Mikamai/LinkMe - Milano 16/04/2018
2. Di cosa parleremo?
1. Cos’è la Dependency Injection e quali sono i suoi vantaggi
2. Caso di studio “Correttore Ortografico” (Tutti gli esempi mostrati riguarderanno questo
caso di studio)
3. Inversion of Control e Dependency Injector
4. Injection points / Forme di Dependency Injection
5. Scope dei componenti e problematiche associate
6. Test Unitari
7. Test di Integrazione
8. Profili
3. Dependency Injection
La Dependency Injection (DI) è una naturale tecnica di composizione degli oggetti in
Object Oriented Programming (OOP).
La DI è un design pattern talmente semplice che molti sviluppatori lo utilizzano per anni
senza sapere come si chiama.
Ogni oggetto (dipendente) può avere la necessità di collaborare con altri oggetti
(dipendenze/collaboratori) per svolgere il suo unico compito (SRP Single Responsibility
Principle).
Nell’ambito della DI è molto importante considerare gli oggetti come dei servizi. In questo
modo si evidenzia la relazione di dipendenza (transitiva) tra un oggetto ed i servizi che
utilizza per svolgere la sua funzione (A sua volta un altro servizio).
Il sistema composto da tutte le dipendenze è comunemente chiamato Object Graph e
funzionalmente svolge il suo compito come se fosse un singolo componente.
ORIGINI D.I. (2004): https://martinfowler.com/articles/injection.html
4. Vantaggi della Dependency Injection
Incoraggia l’utilizzo della composizione di oggetti e scoraggia l’uso dell’ereditarietà.
Porta ad un basso accoppiamento tra i componenti, aumentando chiaramente la
flessibilità e favorendo il riuso del software.
Aumenta la testabilità dei componenti. I collaboratori che rappresentano delle dipendenze,
vengono sostituiti dai test doubles.
Favorisce l’applicazione di alcuni dei principi SOLIDi soprattutto se utilizzata in
combinazione con IOP (Interface-Oriented Programming).
● OCP (Open/Close Principle)
● DIP (Dependency Inversion Principle)
OCP Un'entità software dovrebbe essere aperta alle estensioni, ma chiusa alle modifiche
DIP I moduli di alto livello non devono dipendere da quelli di basso livello. Entrambi devono dipendere da astrazioni; Le
astrazioni non devono dipendere dai dettagli; sono i dettagli che dipendono dalle astrazioni. INFRASTRUTTURA usa DOMINIO e
non viceversa
5. Libro del 2009.
Ottimo riferimento sia per principianti che
per esperti di DI.
Mostra i design pattern e le best practices
per utilizzare al meglio la DI in ambito
professionale.
Non si concentra su uno specifico
framework per DI ma si focalizza sui
concetti importanti con il supporto di ottimi
esempi.
Gli esempi sono in Java e vengono utilizzati i
framework:
● Spring IoC
● Google Guice
● PicoContainer
6. Caso di studio “Correttore Ortografico”
Il nostro “correttore ortografico” è molto semplice, e fornisce due funzionalità:
● Data una parola ci dice se è VALIDA per il lessico impostato.
● Data una parola errata prova a darci dei SUGGERIMENTI per correggerla cercando
parole simili nel lessico impostato sfruttando una specifica strategia di correzione.
Lessici installati:
● SPORT ITALIANI: calcio, pallavolo, pallacanestro
● CIBI ITALIANI : pizza, lasagne, pasta
Strategie di correzione disponibili:
● INIZIA PER: Se si digita “p” ed è installato il dizionario CIBI ITALIANI le possibili
correzioni saranno “pizza” e “pasta” visto che entrambe iniziano per il typo “p”.
● FINISCE PER : Se si digita “o” ed è installato il dizionario SPORT ITALIANI le possibili
correzioni saranno “calcio” , “pallavolo” e “pallacanestro” visto che tutte terminano per
il typo.
7. Caso di studio “Correttore Ortografico”
Il caso di studio è sviluppato in Java (1.8) ed utilizza Spring IoC Container
(5.0.5.RELEASE) come Dependency Injector.
Il progetto utilizza Maven ed il plug-in di Eclipse Spring Tool Suite (STS 3.9.4.RELEASE)
I test utilizzano le librerie JUnit (4.12), AssertJ (3.9.1) e Mockito (1.10.19)
Nel caso di studio il container di Spring viene configurato solo con files XML.
URL Github: https://github.com/groppedev/di-thegoodparts-tdd-milano
9. DI senza IoC
Dependency Injection manuale con l’aiuto del Factory Pattern
● Si deve scrivere molto codice che non fornisce alcun valore aggiunto (Factory)
● I componenti sono testabili ma con difficoltà. Si modificano le factory
appositamente per fare i test.
● Lo scope dei componenti è definito nel codice sorgente e non può essere
modificato dall’esterno. La flessibilità dell’applicazione è ridotta.
● I client devono chiedere le loro dipendenze mentre la DI con IoC enfatizza il
concetto che i client non devono avere conoscenza esplicita delle loro dipendenze.
10. DI senza IoC (Client)
public class SpellCheckerNoIoC
{
// Si utilizza il lessico dei cibi italiani.
private final Lexicon lexicon = LexiconFactory.getInstance().newItalianFoodLexicon();
public boolean check(Word wordToCheck)
{
return lexicon.hasWord(wordToCheck);
}
}
11. DI senza IoC (Factory)
public class LexiconFactory
{
// SCOPE Singleton nel sorgente e non determinato dal contesto.
private static final LexiconFactory INSTANCE;
static {
INSTANCE = new LexiconFactory();
INSTANCE.init();
}
private volatile LexiconRepository lexiconREPO = new LexiconFileRepository();
private volatile LexiconQueryExecutor lexiconQE = new LexiconQueryExecutor(lexiconREPO);
public static LexiconFactory getInstance() {
return INSTANCE;
}
private LexiconFactory() {}
// Factory method
public Lexicon newItalianFoodLexicon() {
// Composizione manuale del grafo, nessuna flessibilità
return new LexiconItalianFood(lexiconREPO, lexiconQE);
}
}
12. DI senza IoC (Test)
@RunWith(JUnit4.class)
public class SpellCheckerNoIoCTest {
@Before
public void setup() {
// Nei test si vuole utilizzare l'implementazione in memoria.
LexiconInMemoryRepository lexiconREPO = new LexiconInMemoryRepository();
LexiconFactory.getInstance().setLexiconREPO(lexiconREPO);
}
@After
public void teardown(){
// Al termine del test va impostata la vecchia implementazione.
LexiconRepository lexiconREPO = new LexiconFileRepository();
LexiconFactory.getInstance().setLexiconREPO(lexiconREPO);
}
@Test
public void test() {
// Run Test
SpellCheckerNoIoC spellChecker = new SpellCheckerNoIoC();
boolean isValid = spellChecker.check(Word.aWord("pizza"));
// Assertion
Assert.assertTrue(isValid);
}
}
13. IoC (Inversion of Control)
Hollywood Principle: Un oggetto dipendente, non deve
preoccuparsi di richiedere le sue dipendenze, gli verranno
fornite in automatico.
Dependency Injector: Framework o libreria che
implementa l’Hollywood Principle
Esempi di IoC:
● Un metodo di test invocato in automatico da un framework (vedi DI senza IoC (Test))
● Un event handler richiamato in seguito al click del mouse
● Una dipendenza impostata in un oggetto dipendente da un dependency injector
14. Dependency Injector
Un framework di Dependency Injection (Injector o Container) è uno strumento molto
potente che gestisce aspetti che vanno oltre la pura Dependency Injection:
● Gestione degli SCOPE dei componenti, garantendo a seconda dell’utilizzo la possibilità
di cambiare scope senza modificare il codice sorgente.
● Gestione dell’intero ciclo di vita degli oggetti, dalla creazione alla distruzione.
● Metodologie standardizzate per risolvere problemi comuni quando si utilizza la
Dependency Injection, su tutte la possibilità di impostare una dipendenza con scope più
limitato rispetto a quello dell’oggetto dipendente.
● Possibilità di gestire dei profili per rendere l’applicazione completamente flessibile
senza la necessità di modificare la codebase.
Nei successivi esempi verranno utilizzate le annotation JSR-330, anche se la
configurazione XML è da considerarsi migliore per applicazioni complesse.
15. Dependency Injector per quali oggetti?
Quando si inizia ad utilizzare un framework di DI si tende a gestire OGNI oggetto con
l’injector, ma è sbagliato!
Lo stesso sintomo si verifica quando si inizia ad utilizzare una libreria di mocking nei test,
si tende a sostituire ogni oggetto con un test doubles.
● I Value Object e le Entity non vanno gestite con l’injector così come non devono mai
essere sostituiti con dei mock nei test.
● Le classi di utilità che non hanno dipendenze, *Utils non devono essere gestite con
l’injector così come non devono mai essere sostituite con dei mock nei test.
Vanno create apposite classi statiche senza stato.
16. Dependency Injector per quali oggetti?
/**
* Value Object
*/
@Immutable
public final class Word {
private final String text;
private Word(String text) {
this.text = text;
}
public static Word aWord(String text) {
return new Word(text);
}
public boolean startsWith(Word word) {
return this.text.startsWith(word.text);
}
@Override
public boolean equals(Object obj) { ... }
}
17. Dependency Injector per quali oggetti?
/**
* Classe di utilità che non ha dipendenze esterne
*/
public class LexiconUtils {
public static Collection<Word> toWord(LexiconType type, Collection<LexiconWord> lexicondWords) {
Collection<LexiconWord> currentTypeWords = select(lexicondWords, new Predicate<LexiconWord>() {
public boolean evaluate(LexiconWord lexiconWord) {
return lexiconWord.matchesType(type);
}
});
return collect(currentTypeWords, new Transformer<LexiconWord, Word>() {
@Override
public Word transform(LexiconWord lexiconWord) {
return lexiconWord.toWord();
}
});
}
}
18. Dependency Injector - Best practices
Qualificare i componenti utilizzando i namespace
Soprattutto quando si devono gestire applicazioni complesse è buona prassi qualificare
sempre i componenti gestiti dall’injector con un identificativo univoco utilizzando i
namespace.
@Named // Non Qualificato
public class SuggestionService
@Named(value="suggestionService") // Senza Contesto
public class SuggestionService
@Named(value="spellcheck.suggestions.service") // OK
public class SuggestionService
19. Dependency Injector - Best practices
Non legare il codice di dominio al codice del framework di injection
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import groppedev.dithegoodparts.domain.Word;
import groppedev.dithegoodparts.domain.lexicon.LexiconItalianFood;
public class WordsRepository implements ApplicationContextAware
{
private ApplicationContext applicationContext;
public Collection<Word> words() {
return applicationContext.getBean(LexiconItalianFood.class).words();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
20. Injection Point - Field Injection
// Annotations JSR-330.
@Named
public class FieldInjection implements InjectionPoint
{
@Inject private Provider<Example> provider;
public void execute() {
Objects.requireNonNull(provider);
System.out.println(provider.get());
}
public void executeBis() {
Objects.requireNonNull(provider);
System.out.println(provider.get());
}
}
21. Injection Point - Field Injection
● Forma di DI concisa.
● Si creano oggetti NON testabili.
● Si creano oggetti dipendenti dal framework di Dependency Injection.
● Va aggiunta la validazione in tutti i metodi.
● Tende a nascondere le dipendenze.
● Favorisce il crescere dei collaboratori senza accorgersi del problema fino a violare il
principio SRP
DA NON UTILIZZARE
La Field Injection è un ANTI-PATTERN
22. // Annotations JSR-330.
@Named
public class SetterInjection implements InjectionPoint
{
private Provider<Example> provider;
public void execute()
{
Objects.requireNonNull(provider);
System.out.println(provider.get());
}
public void executeBis() {
Objects.requireNonNull(provider);
System.out.println(provider.get());
}
// Setter non esposto sull'interfaccia
@Inject public void setProvider(Provider<Example> provider) {
this.provider = provider;
}
}
Injection Point - Setter/Method Injection
23. Injection Point - Setter/Method Injection
● Vanno bene per esplicitare dipendenze opzionali.
● Sono utili per gestire casistiche particolari e NON di dominio (ES:
ComponentProvider nel caso di studio)
● Ottima soluzione per risolvere le dipendenze circolari, che però normalmente sono
un segnale di cattivo design
● Funziona bene con l’ereditarietà, ma noi vorremmo lavorare con la
composizione.
24. Injection Point - Setter/Method Injection
● Si creano oggetti testabili ma fixture di test poco manutenibili.
● Va aggiunta la validazione in tutti i metodi.
● Tende a nascondere le dipendenze se utilizzato con dipendenze obbligatorie.
● Incoraggia a creare oggetti MUTABILI e potenzialmente INCOMPLETI.
● Favorisce il crescere dei collaboratori senza accorgersi del problema fino a violare il
principio SRP.
● Può causare più facilmente errori a RUNTIME
DA UTILIZZARE SOLO PER MODELLARE LE DIPENDENZE OPZIONALI
La Setter Injection si può utilizzare ma prestando molta attenzione!
I setter devono essere metodi delle implementazioni NON delle interfacce
25. Injection Point - Constructor Injection
// Annotations JSR-330.
@Immutable
@Named
public final class ConstructorInjection implements InjectionPoint {
private final Provider<Example> provider;
// Ogni componente dovrebbe avere uno solo costruttore!
@Inject
public ConstructorInjection(Provider<Example> provider){
// Validazione solo nel costruttore.
Objects.requireNonNull(provider);
this.provider = provider;
}
public void execute() {
System.out.println(provider.get());
}
public void executeBis() {
System.out.println(provider.get());
}
}
26. Injection Point - Constructor Injection
● Per le dipendenze opzionali utilizzare setter/method injection
● Va bene per esplicitare dipendenze obbligatorie.
● Si creano oggetti testabili e fixture di test manutenibili
● E’ richiesta la validazione solamente nell’unico costruttore (Più costruttori molte volte
indicano la violazione del principio SRP)
● Limita gli errori a RUNTIME
● Evidenzia in modo chiaro quando i collaboratori aumentano troppo e favorisce la
sistemazione del design prima di violare il principio SRP
● Incoraggia a creare oggetti IMMUTABILI e COMPLETI eliminando il problema della
concorrenza e favorendo notevolmente la diminuzione degli errori.
● Evidenzia le dipendenze al posto di nasconderle creando oggetti utilizzabili facilmente
anche senza il framework di dependency injection
27. Injection Point - Conclusioni
La FIELD INJECTION non va mai utilizzata!
Utilizzare la SETTER INJECTION solo per i collaboratori OPZIONALI e la CONSTRUCTOR
INJECTION per i collaboratori OBBLIGATORI.
I componenti devono avere un solo COSTRUTTORE e non nascondere mai le DIPENDENZE.
Anche i più grandi critici dei framework dei Dependency Injection, ammettono che la
CONSTRUCTOR INJECTION è un ottima tecnica per creare oggetti.
Vedi http://www.yegor256.com/2014/10/03/di-containers-are-evil.html
28. Scope
Lo scope di un oggetto indica la durata del ciclo di vita dell’oggetto stesso in un determinato
contesto che nel nostro ambito sarà una istanza del container di dependency injection.
Unscoped / Prototype : Viene creato un nuovo oggetto ad ogni richiesta.
Singleton: Viene creato un nuovo oggetto solo alla prima richiesta e poi viene sempre
restituita l’istanza creata la prima volta.
Lo scope è fornito dal contesto e non deve essere esplicitato nel codice sorgente!
Dovrebbe essere possibile cambiare scope senza modificare il codice
Esistono altri scope (Request, Session e Thread), ma ci concentreremo su Prototype e
Singleton che sono quelli di base ed i più importanti.
29. Scope - Qual è il migliore
Lo scope di default, incredibilmente è diverso a seconda del framework di dependency
injection utilizzato:
● Google Guice: Unscoped / Prototype
● Standard JSR-330: Unscoped / Prototype
● Spring IoC Container: Singleton
Se si parte dal presupposto che il container di dependency injection dovrebbe gestire
principalmente dei servizi per lo più immutabili, sicuramente l’approccio di Spring IoC
Container è il più corretto.
30. Scope - Problematiche
La principale problematica da risolvere quando si lavora con gli scope è la gestione di una
dipendenza di tipo Unscoped / Prototype in un oggetto dipendente di tipo Singleton.
In sostanza quando si ha una dipendenza di scope più limitato rispetto allo scope del
dipendente.
Quello sopra presentato, è un problema talmente comune, che anche lo standard JSR-330
ha proposto una soluzione:
javax.inject.Provider<T>
Questa interfaccia deve essere implementata da tutti i framework di dependency injection
che aderiscono allo standard JSR-330
31. Come fare i Test
TEST UNITARI: Rigorosamente senza l’injector, si istanziano gli oggetti con la new e si passano dei test
doubles al posto delle dipendenze.
@RunWith(JUnit4.class)
public class SpellCheckerTest
{
@Test
public void isValidTest()
{
// Fixture.
Lexicon lexicon = Mockito.mock(Lexicon.class);
SuggestionService suggestionService = Mockito.mock(SuggestionService.class);
// Stubbing
Mockito.when(lexicon.hasWord(Word.aWord("massimo"))).thenReturn(true);
// Run Test.
SpellChecker spellChecker = new SpellChecker(lexicon, suggestionService);
boolean isValid = spellChecker.check(Word.aWord("massimo"));
// Assertions.
Assert.assertTrue(isValid);
}
}
32. Come fare i Test
TEST DI INTEGRAZIONE: Test di accettazione sempre completi di tutto il ciclo di vita dell’injector, deve
essere caricata tutta la configurazione.
@RunWith(JUnit4.class)
public class ApplicationStaticTest {
private static ApplicationStaticAPI app;
@BeforeClass
public static void startApplication(){
System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, "TEST,STATIC_APP");
app = Application.startStaticApplication();
}
@AfterClass
public static void stopApplication(){
app.close();
System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, "");
}
@Test
public void validWordTest(){
// Run Test.
boolean validWord = app.checkWord(Word.aWord("pizza"));
// Assertion.
Assert.assertTrue(validWord);
}
}