Suurten kokonaisuuksien hallinta

Suurten ohjelmistojen kehittämisen ongelmakenttä tuli tunnetuksi ns. ohjelmistokriisistä, jossa tietokonelaitteistojen kyvykkyyden kasvu mahdollisti aina vain monimutkaisempien ohjelmistojen tekemisen, joka johti siihen, etteivät ohjelmistokehitysprojektit pysyneet aikataulussaan, ohjelmistot olivat tehottomia ja ylimonimutkaisia ja siten jopa mahdottomia ylläpitää. Kriisiä on pyritty ratkomaan moni tavoin niin ohjelmistokielellisesti kuin ohjelmistoprosesseja ja -työkaluja kehittämällä. On silti hyvä tunnustaa, että ohjelmistoprojektit ovat edelleen alttiita samoille ongelmille kuin käsitteen syntyaikoina 70-luvulla. Edsger Dijkstra onkin Turing award -puheessaan vuonna 1972 osuvasti sanonut: “as long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming had become an equally gigantic problem.” Mittavat ohjelmistokokonaisuudet ovat kuitenkin kasvava työllistäjä. Keskitytään ensimmäisenä niiden toteuttamisen keskeiseen elementtiin: abstraktioiden hyödyntämiseen.

Abstraktio

Ihmisen kyky käsitellä tietoa on pohjimmiltaan hyvin rajallinen. Työmuistimme pystyy käsittelemään yhtäaikaisesti varsin pientä tietomäärää (kuuluisasti kapasiteettimme kuvataan olevan 7+/-2-yksikköä, mutta tosiasiassa myös käsiteltävän tiedon tyyppi vaikuttaa asiaan. Meille riittää tässä kuitenkin tieto siitä, ettemme pysty mitenkään hallitsemaan työmuistissamme kovin laajaa tietojoukkoa kerrallaan.) Meille on siten kehittynyt varsin hienostunut työkalusto, jonka avulla ymmärrämme ja hallitsemme monimutkaisia asioita: abstrahointi. Kun katsot ympärillesi, abstraktioita on kaikkialla. Kartat eivät näytä kaikkia yksityiskohtia vaan perustuvat siihen, että detaljit piilotetaan, jotta kartan käyttäjä voi keskittyä olennaiseen. Ajaaksesi autoa sinun ei tarvitse tietää, miten auton moottori, voimansiirto ja jarrut toimivat, riittää että osaat käyttää auton ohjauslaitteita. Kielessä luomme uusia käsitteitä ja termejä, jotta voimme kuvata asioita, tapahtumia ja ryhmiä. Nämä kaikki hyödyntävät siis abstraktioita.

Abstraktio

Käsitteen yleistäminen tai pelkistäminen käsittelyn mahdollistamiseksi. Yksityiskohtien tarkoituksellinen piilottaminen ja huomiotta jättäminen olennaisten käsitteiden, yksityiskohtien tai rakenteiden esiintuomiseksi.

Abstraktioita käyttäessämme keräämme siis yhteen toisiinsa liittyviä asioita ja käytämme syntyneestä kokonaisuudesta yhteistä nimitystä. Ohjelmointi perustuu monin tavoin abstraktioiden käyttöön ja abstrahoinnin hyödyntämiseen kokonaisuuden hallitsemiseksi. Jaamme ongelmaa palasiin, jotka ovat riittävän pieniä yhden ihmisen hallittaviksi ja yksinkertaistamme käsittelyä käyttämällä abstraktioita. Tietoista yksityiskohtien ohittamista abstraktion luomiseksi kutsutaan tiedon kätkennäksi.

Abstraktiotasot

Ohjelmoinnissa abstraktiota voi käyttää usealla eri ohjelmakoodin rakenteen tasolla. Tavoite on käsitellä monimutkaista ongelmaa luomalla siihen rakenne, jonka avulla pystymme luomaan ratkaisun kokonaisuuteen. Lähtökohta onkin vanha tuttu hajoita ja hallitse eli ongelmaa jaetaan pienempiin palasiin, kunnes sopiva yksityiskohtien taso on saavutettu. Jokainen hahmoteltu palanen voidaan edelleen jakaa osiinsa. Toisaalta voimme käsitellä asiaa myös erikoistumisen kautta: kirja on käsite, joka kuvaa yhteen sidottuja sivuja. Runokokoelma taas sisältää näillä sivuilla luettavissa olevia runoja. Nämä tavat lähestyä abtraktiota eli “is-a” (runokokoelma on kirja) ja “has-a” (kirjastolla on runokokoelmia) -suhteet ovat molemmat keskeisiä ohjelmoinnin kannalta. Erilaisen abstraktion käyttöä ohjelmoinissa voidaan lähestyä myös historiakatsauksen kautta: ohjelmointikielet ovat kehittyneet tarjoamaan mahdollisuuksia käyttää ohjelmoinnissa aiempaa vahvempia abstraktioita. Tällaisia abstraktioita ohjelmistoissa ovat tietorakenteet, moduulit, oliot ja komponentit.

Tietokoneiden alkuaikoina itse kone määräsi miten ongelma voitiin ohjelmallisesti ratkaista. Ensimmäinen askel kohti abstraktiota oli siten ylipäätään ihmiselle lähestyttävämmän tavan kirjoittaa ohjelmakoodia – assemblyn – kehittäminen. Tämän jälkeenkin vähänkään laajemman ohjelman kirjoittaminen teki koodista hankalasti hallittavaa, koska koodista puuttui lukemista ja ylläpitoa tukeva rakenne. Tällaisesta koodista on etenkin jälkikäteen hyvin hankala nähdä, mitä kaikkea tietoa ohjelma käsittelee ja mistä kaikkialta tietoa käsitellään. Assemblykoodin rakennetta kuvataankin usein spagetiksi ja edelleenkin rakenteeltaan sekavaa ja vaikeaselkoinen koodia kutsutaan pilkkanimellä spagettikoodi.

kuva, joka esittää assemblykoodin spagettirakennetta

Spagettikoodi, jossa ei rakennetta tukevia elementtejä.

Jakaminen osiin

Aliohjelmien, funktioiden ja tietorakenteiden kehittäminen mahdollisti asioiden käsittelemisen abstraktiona sekä tiedon että toiminnallisuuden osalta. Tietorakenteet kokoavat ohjelmakoodissa hierarkkisesti yhteen kuuluvan tiedon yhteen yhteisen nimen alle. Tietoa käsittelevää toiminnallisuutta puolestaan voidaan koota nimetyksi yhden toiminnallisuuden toteuttavaksi ohjelman osaseksi eli funktioksi tai aliohjelmaksi.

kuva, joka esittää koodirakennetta, jossa yhteenkuuluva tieto ja toiminnallisuus on koottu yhteen

Tieto ja toiminnallisuus koottuna yhteen.

Tiedon kapselointi

Pelkkä tiedon ja toiminnallisuuden niputtaminen yhteen ei kuitenkaan estä tiedon käsittelyä vapaasti mistä vain ohjelmasta, mikä voi johtaa vaikeasti löydettäviin virheisiin ja hankaluuksiin, jos toiminnallisuutta pitää muuttaa. Ohjelman rakenteen monimutkaistuessa viittaukset tietorakenteisiin pitää myös määritellä hierarkkisesti. Tämän saavuttamiseksi voidaan hyödyntää tiedon kätkentää: määritellään tietorakennetta käsittelevä toiminnallisuus ja kootaan se yhteen tietorakenteen kanssa moduuliksi. Moduuleissa on siten tiedon käsittelyyn rajapinta, joka koostuu joukosta rajapintafunktioita. Rajapinta kätkee tietorakenteen toteutuksen moduulin rajapintafunktioiden taakse. Moduuli näyttäytyy siten eri tavoin moduulin käyttäjälle ja sen toiminnallisuuden toteuttajalle eli näiden abstraktiotaso on eri. Käyttäjä on siis ohjelmoija, joka tarvitsee jotain moduulin tarjoamaa palvelua. Käyttäjälle kiinnostavaa onkin moduulin julkisesta rajapinnasta löytyvät rajapintafunktiot, se, tarjoavatko ne kaivatun toiminnallisuuden ja miten niitä kuuluu kutsua. Itse toteutuksella ei ole käyttäjälle merkitystä. Olennaista on, että rajapinnasta löytyy kaikki käyttäjän kaipaama toiminnallisuus. Moduulin toteuttaja taas täytyy toteuttaa moduulin toiminnallisuus. Heidän vastuullaan on myös pitää huoli siitä, että moduulin julkisessa rajapinnassa todella on kaikki käyttäjien moduulilta kaipaama toiminnallisuus toteutettuna. Tätä käyttäjän kannalta epäolennaisen tiedon kätkemistä määritellyn rajapinnan taakse kutsutaan kapseloinniksi (engl. encapsulatio). Vaikka rajapinta-ajattelu monimutkaistaakin ohjelman suunnittelua, siitä on muuten merkittävää etua. Toteutusta voidaan muuttaa ilman, että muutoksella on käyttäjään vaikutusta, kunhan rajapinta pysyy muuttumattomana. Lisäksi kätkettyä tietoa ei käsitellä mistä tahansa, mikä helpottaa virheiden etsintää ja ylläpitoa.

kuva, joka esittää koodirakennetta, jossa moduuli kätkee toteutuksen julkisen rajapinnan taakse

Moduuli toteuttajan ja käyttäjän näkökulmasta

Abstraktit tietotyypit

Seuraava askel abstraktioiden hyödyntämisessä on koota tieto osaksi toiminnallisuutta olioajattelun avulla. Abstrakti tietotyyppi kokoaa tiedon ja toteutuksen tiiviisti yhteen ja mahdollistaa useiden saman tyyppisten elementtien – olioiden – käytön ohjelman osana. Käsitteellisesti abstrakti tietotyyppi on rajapinnan toiminnallisuuden ja tietotyypin sisäisen toteutuksen määrittävä kokonaisuus. Oliot ovat konkreettisia abstraktista tietotyypistä ohjelmassa luotuja tietoalkioita, jotka käyttäytyvät rajapinnan toiminnallisuuden mukaisesti. Siinä missä moduuli tarjoaa rajapinnan ja tiedon kätkentää, abstraktit tietotyypit yhdistävät tietorakenteen ja rajapinnan. Moduulit ja oliot myös täydentävät toisiaan. Modulaarisuudella voidaan ohjelma jakaa ylemmän tason komponentteihin, joiden toteutuksessa voidaan käyttää olioita. Syntyy jako staattiseen ja dynaamiseen osaan. Moduulien määrittämät rajapinnat ovat ohjelmiston pysyvä – staattinen – osa. Olioita taas syntyy ohjelman ajon aikana tarpeen mukaan. Esimerkiksi päiväyksiä voi ohjelmassa olla useita. Niillä kaikilla on sama julkinen toiminnallisuus, mutta toisistaan eroava sisäinen tila. Olio on myös ohjelmassa itsenäinen kokonaisuus. Ajatellaan, että jokaisella abstraktilla tietotyypillä on oma vastuualueensa.

Palvelunäkökulma

Yksi tärkeä abstrahoinnin kehittymisen mukanaantuoma muutos on siirtymä puhtaasta toteuttajan näkökulmasta käyttäjälle tarjottuun palveluun. Rajapinnat, oliot ja näiden toteutukset muodostavat ohjelmistokomponentin. Komponentti on itsenäinen ohjelmiston palanen, joka voidaan ottaa osaksi suurempaa kokonaisuutta. Java ohjelmointikielenä on hyvä esimerkki tästä ajattelusta. JavaBeans on esimerkki komponenttilähestymistavan hyödyntämisestä ohjelmointikielessä.

Lokaalisuusperiaate

Tällä kurssilla keskitytään oppimaan modulaarisen ohjelmarakenteen hyödyntämistä omissa ohjelmissa ja laajemmin isomman ohjelmiston toteuttamisen arjessa. Kurssi ei siis käsittele ohjelmistosuunnittelua. Sitä varten on COMP.SE.110 Ohjelmistojen suunnittelu. Käytännössä suurehkoakaan ohjelmaa ei pysty toteuttamaan suunnittelematta sitä, joten käymme joitain suunnittelun perusperiaatteita läpi.

Ohjelmistosuunnittelussa tavoite on löytää ratkaisu ongelmaa. Tämä tarkoittaa modulaarisuuden kannalta sitä, että tunnistetaan sopivat komponentit ja niiden väliset yhteydet. Yksinkertaistetusti ohjelmiston suunnittelu koostuu komponenttien:

  • tunnistamisesta

  • vastuualueiden määrittelystä

  • välisten suhteiden tunnistamisesta

  • rajapintojen määrittelystä

Jako moduuleihin voidaan tehdä joko osittavasti (top down) tai kokoavasti (bottom up). Osittavassa lähestymisessä tunnistetaan ensin ohjelmiston suurimman toiminnalliset osat kuten käyttöliittymä, tietokanta, tiedon käsittely ja mahdolliset liittymät muihin ohjelmiin. Näiden tunnistamisen jälkeen jokainen osa voidaan puolestaan jakaa pienempii osakokonaisuuksiin kunnes lopulta muodostuu palasia, jotka voidaan toteuttaa moduuleina, abstrakteina tietotyyppeinä ja komponentteina. Kokoavassa lähestymistavassa tarjolla on jo tunnettuja ratkaisuja joihinkin osaongelmiin. Näitä kokoamalla yhteen voidaan lähteä rakentamaan tavoiteltua ohjelmistoa. Suunnittelutyö voi olla, ja usein onkin, yhdistelmä näitä lähestymisiä.

Suunnittelussa pyritään mahdollisimman itsenäisiin komponentteihin, sellaisiin, joiden toteuttaminen voidaan tehdä erillään muusta ohjelmistosta. Ohjelman eri osat väistämättä kommunikoivat keskenään ja viittaavat toisiinsa. Moduulien väliseksi viittaus tarkoittaa sitä, että yksi moduuli tarvitsee toisen tarjoamaa palvelua. Eri ohjelmistokomponenttien välisiä yhteyksiä pyritään suunnittelussa minimoimaan. Jokainen viittaus komponenttien välillä lisää ohjelmiston kompleksisuutta ja siten tekee siitä vaikeamman ylläpitää ja ymmärtää – asioita, joiden välttämiseksi abstraktioiden hyödyntäminen ohjelmistosuunnittelussa on olemassa. Kun komponenttien välisiä yhteyksiä minimoidaan, ohjelmiston rakenteesta tulee yksinkertaisempi ja tämä entisestään yksinkertaistuu, jos yhteydet pidetään aina yksisuuntaisina. Jos ohjelmistossa tunnistetaan joukko vahvasti toisiinsa kytkeytyneitä komponentteja, jotka viittaavat runsaasti toisiinsa, ne voidaan koota uuden yhteisen rajapinnan taakse ja näin säilyttää ohjelmassa ns. lokaalisuus.

Lokaalisuusperiaate

Vahvasti toisiinsa kytkeytyneiden moduulien paketoiminen uuden, pelkistetymmän rajapinnan taakse ohjelmakomponenttien välisten yhteyksien minimoimiseksi ja ohjelmiston kompleksisuuden pitämiseksi hallittavana.

Myös huomion kiinnittäminen riippuvuuksien suuntiin yksinkertaistaa ohjelman rakennetta: riippuvuudet kannattaa pyrkiä pitämään yksisuuntaisina. Mahdolliset riippuvuuksien muodostamat sykliset viittauskehät hankaloittavat komponenttien toteuttamista ja testaamista. Huolellisesta suunnittelusta huolimatta tilanteista, joissa komponenttien välinen riippuvuus on kahdensuuntainen ei kuitenkaan voida välttää. Ohjelmointikielissä on tällaiseen tavalla tai toisella varauduttu. Esimerkiksi C++:ssa tilanne, jossa luokka A tarvitsee luokan B palveluita ja B vastaavasti luokan A palveluita, ratkaistaan ennakkoesittelyllä (engl. forward declaration) class A;. Java puolestaan tunnistaa luokat ja metodit lähdekooditiedostoista ja siten tyyppejä ja metodeja voi käyttää ilman erillistä ennakkoesittelyä.

Abstraktioista (kesto 19:32)

Abstraktio tarkoittaa

Suuren ohjelman toteuttamisessa korostuu

Lokaalisuusperiaate pyrkii