Liigu peamise sisu juurde

Sõltuvuste süstimine (Dependency Injection)

Sissejuhatus

Eelnevates peatükkides oleme kokku puutunud sellega, kuidas üks klass sõltub teistest klassidest. BookService klass kasutab BookRepository klassi, OrderProcessor Notifier-it jne. Need on sõltuvused ehk objektid, mida üks klass vajab selle nimel, et oma eesmärki täita.

Sellest tekib aga küsimus - kust need sõltuvused tulevad? Täpsemalt siis kes neid sõltuvusi tekitab?

Kui klass vastutab ise enda sõltuvuste tekitamise eest (klassi sees tehakse new ...), siis need komponendid muutuvad tihedalt seotuks omavahel. Lisaks sellele sõltub üks klass mingist väga spetsiifilisest implementatsioonist, mis teeb selle klassi testimise, laiendamise ja muutmise keerukamaks.

Seda annab lahendada sõltuvuste süstimisega (dependency injection, DI). DI tähendab, et need objektid luuakse valmis klassist väljaspool ning edastatakse klassile loomise hetkel edasi.

Probleemist lähemalt

Vaatleme BookService klassi, millel on andmebaasi ligipääs läbi BookRepository liidese.

public class BookService {
private final BookRepository repository = new DatabaseBookRepository();

public String getAllBooks() {
return repository.findAll().toString();
}
}

Antud juhul BookService loob ise endale DatabaseBookRepository objekti. Sellega kaasnevad järgmised probleemid:

  • Testimine muutub raskeks. Selleks, et testida BookService-t, on vaja reaalset andmebaasiühendust kuna puudub viis seda asendada.
  • Teostuse muutmiseks peab klassi muutma. Kui muuta andmehoidla andmebaasi pealt näiteks failipõhise hoidla vastu, siis peab muutma ka BookService klassi.
  • Klassil on peidetud sõltuvus. Väljast vaadates ei ole selge, et BookService kasutab BookRepository-t, see on teostusdetailide sees peidus.

Sõltuvuse süstimine

Selle asemel et klassis endas sõltuvust tekitada, võtab klass selle vastu konstruktori kaudu:

public class BookService {
private final BookRepository repository;

public BookService(BookRepository repository) {
this.repository = repository;
}

public String getAllBooks() {
return repository.findAll().toString();
}
}

Nüüd lasub BookRepository teostuse valimise ja loomise vastutus BookService loojal:

// In production — use a real database
BookRepository repository = new DatabaseBookRepository();
BookService service = new BookService(repository);

// In tests — use a mock
BookRepository repository = mock(BookRepository.class);
BookService service = new BookService(repository);

BookService klassi ennast ei pea muutma BookRepository teostuse muutmiseks. Sõltuvuste süstimise põhimõte seisnebki selles, et sõltuvused süstitakse klassi kusagilt väljaspoolt.

Süstimise termin

Termin tuleneb nähtusest, et sõltuvus surutakse klassi sisse väljastpoolt, selle asemel et klass ise selle sisse tõmbaks seda konstrueerides.

Ilma DI-ta kontrollib klass ise, mida ta kasutab:

BookService  --creates-->  DatabaseBookRepository

DI-ga kontrollib kutsuja, mida klass kasutab:

caller  --passes-->  BookRepository  --into-->  BookService

Klass deklareerib, mida ta vajab (konstruktori kaudu), ja kutsuja annab selle ette.

Konstruktori kaudu süstimine

Kõige levinum ja soovitatav DI vorm on konstruktori kaudu sõltuvuste süstimine, kus kõik vajalikud sõltuvused antakse konstruktorile parameetritena:

public class LibraryController {
private final BookService service;

public LibraryController(BookService service) {
this.service = service;
}

public String handleRequest(String request) {
// delegates to service
}
}

Selle eelised on:

  • Sõltuvused on selged. Konstruktori signatuur näitab täpselt, mida klass vajab.
  • Objekt on alati kehtivas olekus. Kõik sõltuvused antakse loomise hetkel ehk poolikult initsialiseeritud objekte ei ole võimalik luua.
  • Väljad võivad olla final, tagades muutumatuse.
  • Testimine on lihtne - anna konstruktorile mock või testiduubel.

Testimine DI taustal

DI üks praktilisemaid eeliseid on testitavus.

Ilma DI-ta nõuab BookService testimisel päris andmebaasi, mis on aeglane, habras ja vajab seadistamist. DI-ga saab repositooriumi asendada mock'iga:

@Test
void testGetAllBooks_returnsAllBooksAsString() {
BookRepository repository = mock(BookRepository.class);
BookService service = new BookService(repository);

Book book = new Book("Refactoring", "Martin Fowler", 1999, new BigDecimal("40"));
when(repository.findAll()).thenReturn(List.of(book));

String result = service.getAllBooks();

assertEquals(List.of(book).toString(), result);
}

Test käivitub koheselt, ei vaja välist infrastruktuuri ning kontrollib ainult BookService loogikat. See on võimalik, kuna sõltuvus süstiti sisse, mitte ei olnud koodi juba eelnevalt sisse kirjutatud.

DI ja DIP

DI on tihedalt seotud Dependency Inversion Principle (DIP)-iga.

DIP väidab, et kõrgtaseme moodulid peaksid sõltuma abstraktsioonidest, mitte konkreetsetest teostustest. DI on mehhanism, mis teeb selle praktikas võimalikuks. See määrab, kuidas need abstraktsioonid käitamise ajal ette antakse.

Ülaltoodud näidetes sõltub BookService BookRepository liidesest (abstraktsioonist) ning konkreetne teostus süstitakse väljastpoolt klassile sisse. See järgib nii DIP-i (sõltub abstraktsioonidest) kui ka DI-d (saab sõltuvused väljastpoolt).

BookService  -->  BookRepository (interface)  <--  DatabaseBookRepository
<-- mock(BookRepository.class)

DI suuremas kontekstis

Antud materjalid keskenduvad sõltuvuste süstimisele konstruktori kaudu. Näiteks programmi main meetod (või testmeetodid) loovad objektid valmis ning edastavad need vajalikele klassidele edasi:

BookRepository repository = new DatabaseBookRepository();
BookService service = new BookService(repository);
LibraryController controller = new LibraryController(service);

Seda nimetatakse käsitsi sidumiseks (manual wiring) ja see toimib hästi väiksemate rakenduste puhul.

Suuremates süsteemides, kus on kümneid või sadu klasse, muutub selline käsitöö tüütuks ja vigade teke on tõenäolisem. Siin tulevad mängu DI raamistikud (ehk DI konteinerid), mis automatiseerivad selle sidumise protsessi. Java ökosüsteemis on kõige tuntum DI raamistik Spring.

Põhimõte jääb samaks olenemata sellest, kas sidumine toimub käsitsi või raamistiku abil: klass deklareerib, mida ta vajab, ja keegi teine annab selle ette. See, kas selleks “kellekski teiseks” on sinu testikood, main-meetod või raamistik, ei oma klassi enda jaoks tähtsust.