Riippuvuuksien injektointi (engl. dependency injection) on suunnittelumalli, jossa olioiden tarvitsemat riippuvuudet, kuten muut oliot tai palvelut, asetetaan niille ulkopuolelta esimerkiksi konstruktorin tai metodikutsun kautta. Tämä malli parantaa luokkien testattavuutta ja vähentää niiden välisiä tarpeettomia riippuvuuksia.

Alla oleva koodi löytyy Poetry-muotoisena projektina kurssin tehtävärepositoriosta hakemistosta koodi/viikko1/riippuvuuksien-injektointi

Tarkastellaan erittäin yksinkertaista laskinta:

class Laskin:
    def suorita(self):
        while True:
            luku1 = int(input("Luku 1:"))

            if luku1 == -9999:
                return

            luku2 = int(input("Luku 2:"))

            if luku2 == -9999:
                return

            vastaus = self._laske_summa(luku1, luku2)

            print(f"Summa: {vastaus}")

    def _laske_summa(self, luku1, luku2):
        return luku1 + luku2

def main():
    laskin = Laskin()

    laskin.suorita()

if __name__ == "__main__":
    main()

Ohjelman ikävä puoli on se, että Laskin-luokalla on konkreettinen riippuvuus tulostuksen ja syötteen luvun hoitaviin funktioihin print input.

Konkreettiset riippuvuudet vaikeuttavat testaamista ja tekevät ohjelman laajentamisen vaikeaksi.

Riippuvuuden eliminointi

Eristetään tulostuksen ja syötteen lukeminen omaan, luokan KonsoliIO olioon:

class KonsoliIO:
    def lue(self, teksti):
        return input(teksti)

    def kirjoita(self, teksti):
        print(teksti)

Muokataan luokkaa Laskin siten, että se saa konstruktorin parametrina olion, jonka kautta se hoitaa käyttäjän kanssa tapahtuvan kommunikoinnin:

class Laskin:
    def __init__(self, io):
        self._io = io

    def suorita(self):
        while True:
            luku1 = int(self._io.lue("Luku 1:"))

            if luku1 == -9999:
                return

            luku2 = int(self._io.lue("Luku 2:"))

            if luku2 == -9999:
                return

            vastaus = self._laske_summa(luku1, luku2)

            self._io.kirjoita(f"Summa: {vastaus}")

    def _laske_summa(self, luku1, luku2):
        return luku1 + luku2

Sovellus käynnistetään nyt siten, että sille injektoidaan kommunikaation hoitava olio konstruktorin parametrina:

def main():
    io = KonsoliIO()     # luodaan riippuvuus luokan ulkopuolella
    laskin = Laskin(io). # annetaan riippuvuus luokalle

    laskin.suorita()

main()

Riippuvuuksien injektoinnilla on hieno nimi, mutta se on lopulta todella simppeli asia: ei luoda riippuvuuksia luokan sisällä, vaan annetaan ne ulkopuolelta.

Testaus

Eräs riippuvuuksien injektoinnin eduista se, että testaus muuttuu todella paljon helpommaksi.

Laskimelle on nyt helppo tehdä yksikkötestit. Testejä varten toteutetaan valeluokka eli “stubi”, joka toimii ulkoisesti samalla tavalla kuin luokan KonsoliIO oliot:

class StubIO:
    def __init__(self, inputs):
        self.inputs = inputs
        self.outputs = []

    def lue(self, teksti):
        return self.inputs.pop(0)

    def kirjoita(self, teksti):
        self.outputs.append(teksti)

Stubille voidaan siis antaa “käyttäjän syötteet” konstruktorin parametrina. Ohjelman tulosteet saadaan suorituksen jälkeen kysyttyä stubilta.

Testi seuraavassa:

class TestLaskin(unittest.TestCase):
    def test_yksi_summa_oikein(self):
        # testissä kovakoodataan ohjelman syötteiksi 1, 3 ja -9999
        io = StubIO(["1", "3", "-9999"]).  

        laskin = Laskin(io)
        laskin.suorita()

        # varmistetaan, että ohjelma tulosti oikean summan
        self.assertEqual(io.outputs[0], "Summa: 4")

Yhteenveto

Riippuvuuksien injektointi on siis oikeastaan äärimmäisen simppeli tekniikka, moni on varmaan sitä käyttänytkin jo ohjelmoinnin peruskursseilla.

Tarkastellaan toisena esimerkkinä tietokonepeliä. Pelien toiminta riippuu usein satunnaisluvuista. Jos peli on koodattu seuraavasti, on automatisoitu testaus erittäin vaikeaa:

class Peli: 
    def liikuva_pelaajaa(self):
      suunta = random.randint(0, 8)

Luokalla Peli on siis suora riippuvuus satunnaislukugeneraattoriin random, jonka metodia randint kutsumalla se luo pelaajan liikesuunnan määräävän luvun.

Tilanne helpottuu testaamisen kannalta käyttämällä riippuvuuksien injektointia. Jos satunnaislukugeneraattori injektoidaan pelille seuraavasti

class Peli: 
    def __init__(self, arpa):
        self._arpa = arpa

  def liikuva_pelaajaa(self):
    suunta = self._arpa.randint(0, 8)

voidaan testatessa injektoida pelille versio satunnaisgeneraattorista, jonka arpomia lukuja voidaan kontrolloida testeistä käsin. Esimerkiksi seuraavassa sellainen versio satunnaislukugeneraattorista, joka palauttaa aina luvun 1 kutsuttaessa metodia randint:

class Arpa:
    def randint(self, a, b):
        return 1