Metodit ja funktiot, osa 2
Tässä osassa vertailemme kuinka Kotlin taipuu OOP ja FP tyyleihin esimerkkitapauksen kautta. Edellinen osa löytyy täältä.
Tarkoitus on havainnollistaa kuinka ongelma voidaan toteuttaa OOP (Object Oriented Programming) ja FP (Functional Programming) tyylisesti.
Käytämme Kotlinia joka tukee sekä OOP sekä FP lähestymistapoja. Kielen OOP tuki on vallan mainio mutta FP ominaisuudet ovat jossain määrin rajoittuneet vaikkakin vallan riittävät perustason FP toteutuksiin.
Otetaan kuvitteellinen tapaus:
"Järjestelmä käsittelee tilauspyyntöjä (OrderRequest). Uudet tilauspyynnöt hyväksytään (OrderAccepted) ja jo hyväksytyt hylätään (OrderRejected). Järjestelmän tulee tarjota myös rajapinta, josta voi tarkistaa onko tilaus jo hyväksytty."
Esimerkki on sen verran pieni että sen pystyy tässä mukavasti käsittelemään. Toisaalta se on sen verran suppea että tyylisuuntausten läheskään kaikkia vivahteita ei päästä käsittelemään.
Domain voitaisiin mallintaa vaikka näin
Alla olevat molempien lähestymistapojen toteutukset ovat tyylilleen melko karikatyyriset ja suoraviivaiset. Toteutukset on haluttu pitää melko lähellä toisiaan jotta vertailu on helpompaa. Mutta testipuolella on nähtävissä enemmän mielenkiintoisia eroja.
OOP versio
Toteutus voisi näyttää vaikka tältä:
OOP tyylissä yksikkötesteissä melko usein suositeltu tapa on käyttää triviaalia muistinvaraista tallennustapaa. Itse valitsin myös tämän lähestymisen.
Tästä johtuen testattava luokka OrderService on riippuvainen tilallisesta komponentista (InMemoryOrderCrudRepository) jolloin tila joudutaan alustamaan uudelleen jokaista testiä varten.
FP versio
FP version toteutus voisi olla vaikka:
FP tyylissä päädyin testaamaan funktion invariantteja vasten. Toisin sanottuna funktion riippuvuuksia saatetaan vaihtaa testistä toiseen mutta varsinainen argumentti (esim. someOrderRequest) saattaa pysyä samana. Tämä saattaa tuntua perin nurinkuriselta mutta ajattelu on varsin luonnollinen FP:ssä (universumi (=riippuvuudet tässä tapauksessa) muuttaa tilaa, ei sovellus).
Koodia on 3-4 riviä per testi (vaikkakin yksi ekspressio ja ilmaistavissa halutessa yhdellä rivillä).
Vastakkain
Toteutuksien osalta erot ovat pienet ja jossain määrin maku kysymys kumpaa tapaa preferoi.
Testit taas antavat hiukan enemmän pohdittavaa.
Ensimmäinen havainto on että testit on nimetty hiukan eri tavalla vaikka lopulta ne testaavat täysin samaa käyttäytymistä. Tämä selittyy sillä että testit heijastelevat testattavan toiminnallisuuden toteutustapaa.
Koodin luettavuus onkin hankalampi punnittava. Vanhana OOP kettuna OOP versio on kieltämättä aika luettava. Toisaalta myös Kotlinin FP tuki on jossain määrin vaatimaton mikä vaikuttaa omalta osaltaan FP version luettavuuteen.
OOP toteutus riippuu tilallisesta komponentista, mikä tekee testeistä aavistuksen raskaampia ajaa. Toisaalta vaikka sovellus olisi suurempikin tällä ei pitäisi olla suurta vaikutusta niin kauan kuin toteutukset ovat aidosti muistivaraisia. Käytettäessä tilallisia komponentteja on kuitenkin pieni vaara että ajautuu tilanteeseen jossa testejä ei pysty enää ajamaan rinnakkain. Sama tosin koskisi FP lähestymistapaa jos teisteissä hyödynnettäisiin tilallisia komponentteja.
Harmillisen usein projekteissa testejä ei kuitenkaan voi ajaa rinnakkain. Tämä taas näkyy siten että isommissa projekteissa pelkkien yksikkötestien ajaminen mitataan minuuteissa ellei jopa kymmenissä minuuteissa.
Käytettäessä FP lähestymistapaa testit ovat tilattomia, jolloin ne voidaan suorittaa rinnakkain.
Tämä saattaa jakaa mielipiteitä. Onko jompikumpi parempi? Sanoisin että riippuu. Molemmille on paikkansa. Tilalliset testit voivat olla parempia korkeammalla komponenttitasolla kun taas tilattomat testit taas lähempänä yksikkötestitasoa.
Kytkentää
Edellisessä osassa puhuimme kytkennästä ja koheesiosta sekä toiminnallisuuden koostamisesta ja yhdistelystä. Palataan aiheisiin hetkeksi.
Esimerkkimme on sen verran triviaali että siinä näitä ei vielä juurikaan päästy käsittelemään.
Kuvitellaan että muualla sovelluksessa tarvitsee tarkistaa, onko tilaus hyväksytty. OOP mallissa riippuvuutena täytyisi välittää koko OrderService tavalla tai toisella jotta päästään käsiksi varsinaiseen toiminnallisuuteen (isOrderAccepted). FP versiossa usein riittäisi pelkkä lambda, aivan kuten FP esimerkissä.
Juuri riippuvuudet luokista ja niiden mahdollinen alustaminen on OOP tyylissä hiukan hankalampaa. Ja tämä omalta osaltaan puoltaa teisteissä käytettäviä tilallisia komponentteja. Eli luodaan instanssi tilallisesta luokasta kerran, jonka jälkeen instanssin kautta pääsee käsiksi toiminnallisuuksiin, eli metodeihin.
FP:ssä ongelma on yleensä paljon triviaalimpi koska kyse on funktioista, jolloin niiden simuloiminen onnistuu usein yksinkertaisella lambdalla.
Lopuksi
Teimme kevyen katsauksen sekä OOP ja FP tyyleihin konkreettisen esimerkin avulla ja vertailimme lähestymistapoja.
Ainakin Kotlinssa OOP lähestyminen näyttäisi hiukan houkuttelevammalta pelkkää koodia tarkastellessa mutta samalla myös FP:ssä on nähtävissä paljon hyviä puolia.
FP versioon olisi voinut yrittää hyödyntää esimerkiksi Arrow kirjastoa koska kielen FP tuki on rajoittunut. Mutta tällöin ongelmaa olisi ratkottu framework edellä, mikä ei ole hyvä asia.
Ehkä blogautuksen perimmäisenä tarkoituksena oli luoda kevyt silta OOP ja FP lähestymisien välille esimerkin avulla. Molemmat tyylit ovat osaavissa käsissä tehokkaita mutta ymmärtämällä molempia voi parantaa omaa tekemistään ja kenties löytää uusia ulottuvuuksia omalle uralleen.
Syttyykö sisälläsi pieni kipinä aiheesta? Laita viestiä hello@bytecraft.fi ja jatketaan jutustelua.