Monimutkaisuuden hallinta Drupal-sivustolla
Olemme jo pitkään tarjonneet Karhu Huolenpito -palvelua, jonka kantavana ajatuksena on toimia eräänlaisena digitaalisena talonmiespalveluna sivustoille. Huolenpito vastaa siitä, että sivusto toimii ja pysyy muuttuvan maailman mukana – niin teknisesti kuin sisällöllisestikin. Huolenpito-tiimimme kirjaimellisesti pitää huolta asiakkaidemme sivustoista.
Valtaosa Huolenpito-palvelumme piirissä olevista sivustoista ovat meidän rakentamiamme, mutta pidämme huolta myös monesta sellaisesta sivustosta, jota emme ole alunperin olleet toteuttamassa. Tällaisissa tapauksissa me otamme vastuullemme lähes täysin tuntemattoman verkkopalvelun, joka on tuotettu meille tuntemattomalla prosessilla. On sanomattakin selvää, että näiden palveluiden kohdalla riski meille on suuri.
Auditointi paljastaa ongelmakohdat
Teemme tuntemattomille verkkopalveluille aina auditoinnin ennen kuin hyväksymme ne Huolenpitomme piiriin. Auditoinnin tuloksena tuotamme raportin, joka pyrkii vastaamaan seuraaviin kysymyksiin:
- Onko sivuston toteutuksissa tietoturva-aukkoja?
- Mikä on sivuston toteutusten yleinen laatutaso?
- Mitkä osat sivustosta ovat jatkokehityksen kannalta erityisen ongelmallisia?
- Millaiset lähtökohdat sivustolla on yleisimpien jatkokehitystoimenpiteiden (kuten ilmeuudistus) tekemiseen?
Koska auditointi edellyttää perinpohjaista tutustumista sivuston toimintaan ja erityisesti sivustolle rakennettuihin räätälöityihin toimintoihin, olemme päässeet tarkastelemaan läheltä erilaisia tapoja rakentaa toiminnallisuuksia Drupal-sivustoille. Olemme myös oppineet näistä toteutuksista paljon siitä, mitkä seikat vaikuttavat ylläpidettävyyteen ja millaiset asiat korostuvat, kun toteutusten elinkaari kasvaa odottamattoman pitkäksi. Suurimmat ongelmat syntyvät poikkeuksetta monimutkaisuudesta.
Monimutkaisuus haittaa kaikkea
Monimutkaiset toteutukset ovat aina pahasta. Monimutkaisuus luo ongelmia jokaisella verkkopalvelun osa-alueella, sillä:
- Monimutkaiset toteutukset vaativat enemmän työtä niin määriteltäessä kuin rakennusvaiheessakin.
- Monimutkaisissa toteutuksissa on suurempi pinta-ala suojattavaksi tietoturvauhkia vastaan.
- Monimutkaisten toteutusten suorittaminen vaatii yleensä enemmän resursseja.
- Monimutkaisissa toteutuksissa on yleensä suuri määrä sisäisiä ja ulkoisia riippuvuuksia, mikä vaikeuttaa jatkokehitystä ja testaamista.
On sanomattakin selvää, että kukaan ei lähtökohtaisesti halua rakentaa monimutkaisia palveluita. Suuri osa monimutkaisuudesta on kuitenkin välttämätöntä, sillä – vaikka me koodarit sitä niin kovasti haluaisimmekin – todellisuus ei ole eleganttia ja yksinkertaista. Lisäksi alunperin yksinkertaisen suoraviivaiseksi suunniteltu järjestelmä muuttuu usein elinkaarensa aikana paljon odotettua monimutkaisemmaksi. Perustason päälle rakentuva ekstramonimutkaisuus hiipii usein palveluihin mukaan erilaisten vaatimusmuutosten ja uusien ominaisuuksien sivutuotteena. Monimutkaisuutta pitääkin siis pystyä hallitsemaan ja sen kanssa on opittava tulemaan toimeen.
Miten monimutkaisuuden kanssa voi pärjätä?
Koska monimutkaisuudesta ei käytännössä voi koskaan kokonaan päästä eroon, sen haittavaikutukset on saatava minimoitua. Usein tämä tarkoittaa sitä, että monimutkainen toteutus jaetaan pienemmiksi, yksinkertaisiksi osasiksi. Tämä on yksi tietojenkäsittelyn perusajatuksista ja sitä voidaan toteuttaa monella eri tasolla ja monin eri tavoin:
- Pienet, ketterät mikropalvelut korvaavat isot ns. monoliittijärjestelmät. Tämä on ollut muutaman viime vuoden ajan kova trendi verkkopalveluiden rakentamisessa. Mikropalvelut sopivat kuitenkin käytännössä parhaiten todella valtavien järjestelmien rakentamiseen, sillä mikropalvelut tuovat mukanaan myös oman monimutkaisuutensa etenkin devops- ja infrastruktuuriosastolle.
- Ohjelmistot koostetaan moduuleista, jotka ovat vuorovaikutuksessa keskenään selkeästi määriteltyjen rajapintojen kautta. Drupalin modulaarinen rakenne on tästä hyvä esimerkki.
- Ohjelmistomoduulit koostetaan komponenteista, jotka viestivät ja tarjoavat palveluita toisilleen selkeästi määriteltyjen rajapintojen kautta. Esimerkkinä tästä voidaan pitää mm. Drupal 8:n käyttämiä Symfony 2 -komponentteja tai React-sovelluskehyksen komponenttiparadigmaa.
- Ohjelmistomoduulien komponentit koostetaan yksinkertaisista ja selkeistä palasista, jotka mallintavat pieniä, helposti hallittavia yksityiskohtia komponentista. Esimerkiksi olio-ohjelmoinnissa tämä tarkoittaa luokkien, rajapintojen ja periytymisen tehokasta hyödyntämistä.
Drupal-sivustolla monimutkaisuuden aiheuttamat ongelmat syntyvät yleensä räätälöityihin, sivustoa varten toteutettuihin moduuleihin. Lähdettäessä toteuttamaan räätälöityä moduulia, kannattaa monimutkaisuutta pyrkiäkin hallitsemaan aktiivisesti – muuten salakavalasti toteutuksen vyötärölle kertynyt monimutkaisuus jää helposti huomaamatta. Seuraavassa muutamia vinkkejä siihen, miten olemme itse onnistuneet hallitsemaan monimutkaisuutta omissa toteutuksissamme.
Monimutkaisuudesta voi päästä eroon ainoastaan ajattelemalla koodia ilmaisukeinona – mitä ajatuksia ohjelmoija haluaa koodillaan välittää?
Monimutkaisuuden mittaaminen
”Jos sitä ei voi mitata, sitä ei voi hallita” on moneen kertaan todettu valheelliseksi väittämäksi, mutta tässä tapauksessa mittaaminen ainakin auttaa hallitsemaan monimutkaisuutta. Lähdekoodin monimutkaisuus tuntuu kuitenkin hyvin abstraktilta käsitteeltä – miten sitä edes voi lähteä mittaamaan? Koska Drupalin kanssa touhuillessa kyse on pääosin PHP-lähdekoodista, siihen on olemassa loistava työkalu: PHPMD, eli PHP Mess Detector.
PHPMD auttaa analysoimaan PHP-koodia hyvinkin monella erilaisella tavalla, mutta erityisesti monimutkaisuutta mitattaessa PHPMD:n codesize-sääntökokoelma on erittäin hyvä tapa löytää koodin ongelmakohdat. Kyseinen sääntökokoelma voidaan ajaa Drupal-moduulille seuraavasti: phpmd oman/moduulin/polku text codesize --suffixes=php,inc,module,install
Jos PHPMD löytää korjattavaa, on tuloksena jotakin seuraavien huomautusten kaltaista (ongelmien tarkat sijainnit jätetty pois tilan säästämiseksi):
The method getCheapestDeparture() has a Cyclomatic Complexity of 12. The configured cyclomatic complexity threshold is 10.
The method getCheapestDeparture() has an NPath complexity of 328. The configured NPath complexity threshold is 200.
Nyt on tiedossa, että metodissa getCheapestDeparture()
on jotakin korjattavaa, mutta mitä tarkoittavat termit ”cyclomatic complexity” ja ”NPath complexity”?
Syklomaattinen kompleksisuus (”cyclomatic complexity”)
Syklomaattisella kompleksisuudella mitataan sitä, kuinka monta haaraa tai iterointirakennetta funktiossa tai metodissa on. PHPMD laskee syklomaattisen kompleksisuusarvon funktiolle seuraavasti:
- Funktion alussa syklomaattisen kompleksisuuden arvoksi asetetaan 1
- Jokaisen logiikkahaaran (
if
,case
) kohdalla arvoa kasvatetaan yhdellä - Jokaisen iterointirakenteen (
while
,for
) kohdalla arvoa kasvatetaan yhdellä
Oletuksena PHPMD varoittaa, jos funktion syklomaattinen kompleksisuus on yli kymmenen. Ottaen huomioon, että tämä arvo kasvaa kohtuullisen maltillisesti, on PHPMD:n oletusasetus erittäin järkevä. Jos PHPMD varoittaa syklomaattisesta kompleksisuudesta, kannattaa funktiota lähteä refaktoroimaan yksinkertaisempaan suuntaan.
NPath-kompleksisuus (”NPath complexity”)
NPath-kompleksisuus kuvaa sitä, kuinka monta erilaista suorituspolkua funktion läpi on teoreettisesti olemassa. Tämä arvo kasvaa eksponentiaalisesti, koska jokainen suoritushaara tuplaa mahdollisten suorituspolkujen määrän. Ei olekaan mitenkään epätavallista, että monimutkaisissa funktioissa NPath-kompleksisuuarvo voi olla kymmeniä- tai jopa satojatuhansia. Oletuksena PHPMD varoittaa, jos funktion NPath-kompleksisuusarvo ylittää 200.
NPath-kompleksisuus on ainakin osittain riippuvainen samoista asioista kuin syklomaattinen kompleksisuus. Onkin hyvin yleistä, että molemmista arvoista tulee varoituksia samoissa funktioissa. Jos funktion syklomaattista kompleksisuutta saadaan laskettua, laskee yleensä samalla myös funktion NPath-kompleksisuus.
Taisteleminen monimutkaisuutta vastaan
Rakennettaessa uusia toteutuksia, monimutkaisuutta vastaan voidaan taistella ennakoivasti. Tämä on ehdottomasti järkevää, sillä rakennusvaiheessa monimutkaisuuden eliminointi on todella paljon helpompaa ja kustannustehokkaampaa kuin myöhemmin palvelun elinkaaren aikana. Rakennusvaiheessa monimutkaisuuden saa pidettyä kontrollissa muutamaa yksinkertaista periaatetta noudattamalla:
- Mittaa monimutkaisuutta jatkuvasti. Ota esimerkiksi tässä kirjoituksessa aikaisemmin mainittu PHPMD osaksi työskentelyäsi ja pyri automatisoimaan koodin tarkistusprosessi.
- Refaktoroi koodiasi koko ajan. Jos PHPMD varoittaa jostakin funktiosta, korjaa koodisi. Pyri koko ajan kohentamaan koodisi luettavuutta ja ymmärrettävyyttä. Tässä voi olla apua esimerkiksi funktionaalisen ohjelmoinnin periaatteiden noudattamisesta – vaikka PHP:n tapauksessa ohjelmointikieli asettaakin omat rajoituksensa.
- Kirjoita koodiasi kuin kirjoittaisit ulkoiseen käyttöön tulevaa ohjelmointirajapintaa. Rajapinnan tulisi olla helposti ymmärrettävissä ja selkeästi dokumentoitavissa. Selkeä rajapinta tarjoaa hyvät lähtökohdat jatkokehitystyölle sekä koodin uudelleenkäytettävyydelle.
Tilanne on hyvin erilainen silloin, kun pyritään eliminoimaan monimutkaisuutta olemassa olevasta järjestelmästä. Käytännössä järjestelmän toiminta ja siihen rakennetut ominaisuudet evät saa muuttua, mutta toteutusten monimutkaisuutta pitäisi pystyä jotenkin karsimaan osana jatkokehitystä. Koska tässä vaiheessa toteutusten muuttaminen on kallista ja aikaavievää puuhaa, kannattaa muutoksia miettiä hieman tarkemmin:
- Miten muutokset järjestelmään voidaan minimoida? Onko mahdollista, että palvelun nykyisen toteutuksen loppu häämöttää jo näkyvissä? Jos palvelu ollaan lähitulevaisuudessa rakentamassa uudelleen suurelta osin, kannattaako toiminnallisia ja taloudellisia riskejä lähteä ottamaan tässä vaiheessa?
- Voidaanko järjestelmän toimintaa testata luotettavasti nykyisellä koodilla? Jos ei voida, millaisilla muutoksilla koodi saataisiin testattavaksi?
- Mihin funktioihin pitää tehdä muutoksia, jotta halutut kehitystoimet saadaan tehtyä? Onko mahdollista muuttaa vain osia noista funktioista, vai pitääkö ne kirjoittaa kokonaan uusiksi?
Kun muutoksia halutaan koodiin lähteä tekemään, pitää palvelun nykytilanne ottaa huomioon jokaisessa vaiheessa. ”Tuntemattoman” koodin refaktorointi on hankalaa ja erittäin virhealtista, joten sen kanssa on toimittava silkkihansikkain:
- Rakenna testiympäristö, jossa voit testata palvelun toimintaa koko ajan. Tämä saattaa olla erittäin hankalaa tai jopa mahdotonta sellaisissa tapauksissa, joissa ylläpidettävä palvelu on integroitu muihin palveluihin.
- Mikäli koodi ei sisällä testejä, muokkaa muutettavia funktioita sen verran, että saat ne testattaviksi. Kirjoita testit, joilla pystyt testaamaan koodin toimintaa kattavasti. Testien ei tarvitse rakentua minkään hienon testijärjestelmän tai -kehyksen päälle, mutta niillä on pystyttävä varmistumaan siitä, että koodi toimii oikein. Nopeiden testiskriptien rakentaminen kannattaa tehdä esim. drush-plugineina, mikä helpottaa integraatio- ja regressiotestausta.
- Eristä koodista se osa, joka vaatii muokkausta. Tämä tarkoittaa yleensä uusien funktioiden tai luokkien rakentamista. Eristämisen ajatuksena on, että logiikkarakenteita voidaan tarkastella omana yksikkönään ilman muun koodin sotkemista. Samalla koodin käyttämät riippuvuussuhteet nousevat selkeästi esiin.
- Älä kirjoita funktioita, luokkia tai moduuleita kokonaan uusiksi! Tätä kiusausta on usein todella vaikea välttää, mutta vähääkään isompien toteutusten kohdalla uudelleenkirjoitusurakka on pääsääntöisesti valtava virhe. Uudelleenkirjoitettu koodi voi olla laadullisesti paljon parempaa kuin aikaisempi ja sen tarjoamat jatkokehitysmahdollisuudet voivat olla äärettömästi aikaisempaa toteutusta paremmat. Uudelleenkirjoitukseen käytetty työmäärä on kuitenkin yleensä massiivinen ja uuden koodin täysin yhtenevästä toiminnasta vanhan kanssa ei ole juuri mitään takeita.
- Hyödynnä versiohallintaa ja dokumentoi selkeästi tekemäsi muutokset sekä niiden syyt.
Kun nämä kohdat ovat kunnossa, voidaan lähteä miettimään monimutkaisuuden hallintaa toteutuksessa. Koska monimutkaisuus syntyy koodiin tahattomasti ja ilman varsinaista suunnitelmaa, se ilmenee eräänlaisena metatasona koodin päällä. Tästä syystä monimutkaisuudesta voi päästä eroon ainoastaan ajattelemalla koodia ilmaisukeinona – mitä ajatuksia ohjelmoija haluaa koodillaan välittää?
Refaktoroinnissa ilmaisua pyritään tiivistämään ja selkeyttämään. Refaktorointivaiheen toimenpiteet riippuvat aina kontekstista, eikä niiden tekemiseen voi helposti antaa mitään yhtä, selkeää toimintatapaa. Lopputuloksena koodin pitäisi olla selkeää, eleganttia ja yllätyksetöntä – eli juuri sellaista kuin tosimaailma ei ole.