Riippuvuuksien injektointi
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