Olio-ohjelmointi

Käsitteitä

Olio (engl. object) on ohjelmiston rakenteellinen perusyksikkö. Se ei ole puhtaasti toiminnallinen tai puhtaasti tietoa säilyttävä, vaan se sisältää kummankin aspektin.
Olio-ohjelmointi jakautuu kahteen pääsuuntaukseen. Smalltalkin sanastossa oliot kommunikoivat keskenään lähettämällä ja vastaanottamalla viestejä. Viestin vastaanottaminen suorittaa määritellyn toiminnon vastaanottavassa oliossa.
Simulan tai C++:n sanastossa oliot kutsuvat toisensa jäsenfunktioita.
Oliota käytetään ohjelmistosuunnittelussa esittämään jonkun abstraktin tai reaalimaailman käsitteen ilmentymää ohjelmistossa. Oliokielillä laaditut ohjelmat koostuvat tavallisesti lukuisista olioista, joiden yhteistoiminnan tuloksena on ratkaisu ohjelmointiongelmaan. (kts. myös oliomalli)
Olio pystyy pyydettäessä tekemään sille ominaiset toiminnot, joita kutsutaan metodeiksi tai jäsenfunktioiksi kielestä riippuen. Metodi koostuu nimestä, mahdollisista parametreista ja toiminnan määrittelevästä rungosta. Olio tallettaa tietoja attribuutteihin.
Luokka (class) on määritelmä, joka määrittelee tietyn oliojoukon yhteiset piirteet. Olio on luokan ilmentymä, instanssi. Esimerkiksi henkilörekisteriohjelmassa määritellään luokka Henkilö, joka määrittelee millaista tietoa henkilöistä halutaan esittää ja millä tavalla näitä tietoja voidaan käsitellä. Ohjelman ajon aikana luokasta Henkilö luodaan olioita esittämään yksittäisiä henkilöitä. Olioiden toiminnallisuus määritellään useimmiten luokkien jäsenfunktioissa, jolloin kaikki saman luokan oliot sisältävät täsmälleen saman toiminnallisuuden tiedon käsittelyyn. On olemassa myös vähemmän tunnettuja prototyyppipohjaisia kieliä, jotka eivät tunne luokkakäsitettä.
Sidonta (binding) tarkoittaa sitä miten symbolinen nimi yhdistetään dataan tai koodiin. Tähän on pääasiassa kaksi keinoa. Joko kääntäjä tekee sen käännöksen yhteydessä, jolloin puhutaan staattisesta (aikaisesta) sidonnasta, tai se tapahtuu ajonaikana, jolloin kyseessä on dynaaminen (myöhäinen) sidonta. Jossain tilanteissa objektin tyyppi ei ole tiedossa vielä käännösaikana, jolloin on käytettävä dynaamista eli myöhäistä sidontaa.
Perintä (inheritance) mahdollistaa uusien luokkamääritysten tekemisen vanhojen määritysten pohjalta. Periyttämisen ansiosta on mahdollista käyttää uudestaan jo tehtyä ohjelmakoodia uusien luokkien pohjana. Periyttäminen mahdollistaa myös abstraktien perusluokkien käytön, joista periytetään useita luokkia, joita kaikkia voidaan käyttää samannimisillä funktioilla ja samoilla parametreilla kuin abstraktia perusluokkaa. Periyttäminen ja abstraktien perusluokkien käyttö ovat oliopohjaisten kielien vahvimpia mutta myös vaikeimpia ominaisuuksia. Periyttäminen mahdollistaa abstraktit rajapinnat, joiden kautta voidaan käsitellä tuntemattomien tahojen rakentamia olioita ennalta määritellyllä tavalla.
Moniperintä (multiple inheritance), mahdollistaa uuteen luokkaan periyttämisen useista kantaluokista. Moniperintä on hyödyllistä tilanteessa, jossa "lepakko" sisältää "lentävä eläin"- ja "nisäkäs"-luokkien ominaisuudet. Moniperintä on tuettu kielissä kuten C++, Eiffel, OCaml ja Scala. C++ tukee virtuaalista moniperintää, jonka avulla periytyy vain yksi toteutus yhteisen kantaluokan jäsenistä. Kaikissa oliokielissä moniperintä ei ole mahdollista.
Eräissä kielissä moniperintä on vain rajatusti mahdollista. Rajattua moniperintää (interface eli rajapintaperintää) tukevat kielet kuten Ada, C# ja Java. Javascript käyttää perimisen toteuttamiseen prototyyppejä.
Kapselointi (encapsulation) -termiä käytetään kahdessa merkityksessä. Ensimmäisen merkityksen mukaan kapseloinnin ideana on datan ja käyttäytymisen kokoaminen yhteen yksikköön, olioon. Toinen merkitys lisää tähän vielä tiedonpiilotuksen: olion sisäisiin muuttujiin ei (pääsääntöisesti) päästä suoraan käsiksi olion ulkopuolelta, jolloin ohjelmointivirheiden määrä vähenee.
Olion käyttäjän ei tarvitse tietää enää käyttövaiheessa sitä, miten olio toimii sisäisesti: riittää kun tietää, miten oliota itseään käytetään ja miten olio käyttäytyy. Olion käyttäjät voivat käyttää oliota sen julkisen rajapinnan kautta (public interface). Olion luokkakuvauksessa voi olla myös yksityisiä muuttujia tai funktiota (private interface), jotka ovat vain olion itsensä käytettävissä.
Rakentaja (constructor) on metodi, jota kutsutaan oliota luotaessa. Rakentaja alustaa olion jäsenmuuttujien alkuarvot.
Hajottaja tai purkaja (destructor) on metodi, jota kutsutaan olion tuhoamiseksi. Hajottaja vapauttaa olion varaamat resurssit.
Polymorfismi eli monimuotoisuus tarkoittaa sitä että yhtä rajapintaa käytetään eri tyyppien käsittelyyn, tai yksi symboli edustaa monia tyyppejä. Esimerkiksi funktio tai datatyyppi voidaan kirjoittaa siten että se voi käsitellä dataa riippumatta sen tyypistä.

Esimerkki

Pseudokielinen esimerkki luokkakuvauksesta luokalle "Oppilas":
 // Aloitetaan kantaluokan määrittely
 class Henkilö {
 protected:
     String nimi; // yhteinen jäsenmuuttuja kaikille henkilöille
 
 public:
    function asetaNimi( String nimi) {
        this.nimi = nimi;
    }
 }
 
 // Periytetään luokka Opettaja kantaluokasta.
 class Opettaja inherits Henkilö {
 public:
     function haeTitteli() {
         return "prof. " + this.nimi;
     }
 }
 
 // Periytetään luokka Opiskelija kantaluokasta
 class Opiskelija inherits Henkilö {
 private:
     int arvosana; // sisäinen muuttuja, jota ainoastaan olio itse voi käsitellä
 
 // tästä eteenpäin kirjoitetut julkiset tiedot ovat kaikkien käytettävissä
 public:
     function asetaArvosana(int uusiArvosana) {
         this.arvosana = uusiArvosana; // laitetaan parametri "uusiArvosana" sisäiseen muuttujaan "arvosana"
     }
 
     function haeArvosana() {
         return this.arvosana;   // palautetaan sisäisen muuttujan "arvosana" arvo
     }
 
     function haeTitteli() {
         return "tekn. yo. " + this.nimi;
     }
 }
Esimerkki olion käytöstä:
 // Tehdään kaksi oliota "harri" ja "pekka"
 // HUOM! Jokaisella uudella oliolla on omat yksityiset muuttujansa
 harri = new Opiskelija();
 pekka = new Opiskelija();

 prof = new Opettaja();
 prof.asetaNimi("Tuumivainen");

 harri.asetaArvosana(5);
 harri.asetaNimi( "Harri Hakkeri" );
 pekka.asetaArvosana(4);
 
 // Tulostaa: Harrin arvosana on 5 ja Pekan on 4
 print "Harrin arvosana on " + harri.haeArvosana() + " ja Pekan on " + pekka.haeArvosana();
 // Tulostaa: opettaja oli: prof. Tuumivainen
 print "opettaja oli: " + prof.haeTitteli();
 // Tulostaa: opiskelija oli: tekn.yo. Harri Hakkeri
 print "opiskelija oli: " + harri.haeTitteli();

Ensimmäiset kielet

Olioperustainen ohjelmointi syntyi Norjassa 1960-luvun jälkipuoliskolla, jolloin norjalaisessa Norsk Regnesentral -laskentakeskuksessa kehitettiin ensimmäinen oliokieli, Simula. Ole-Johan Dahl ja Kristen Nygaard kehittivät kieltä simulointitarkoituksiin. Jo vuonna 1961 kehitetyssä Simula I:ssa käytettiin luokkia ja niistä luotuja olioita, mutta vuonna 1967 Simula I:n seuraaja Simula67 oli ensimmäinen varsinainen ohjelmointikieli. Simulan rungon muodosti Algol60, joka oli laajennettu olio-ohjelmoinnin luokkakäsitteellä. Simulassa oli mukana jo kaikki olio-ohjelmoinnin peruskäsitteet.
Simula ei saanut paljoakaan käyttäjiä yliopistojen ulkopuolelta. Olio-ohjelmointi sai uutta kantavuutta Smalltalkin ja siihen liittyvän graafisen ohjelmointiympäristön myötä. Smalltalk kehitettiin Xeroxin kehityslaboratoriossa, Palo Alto Research Centerissä (PARC). Sitä käytettiin Alan Kayn ideoiman graafisella käyttöliittymällä varustetun henkilökohtaisen tietokoneen Dynabookin hallintaan. Xerox alkoi jakaa Smalltalkia 1980, mutta sekään ei saavuttanut merkittävää asemaa ohjelmistotuotannossa.
Niin kutsutun ohjelmistokriisin takia 1970-luvulla syntyi useita kieliä, joissa ohjelmistojen modularisointi oli keskeisenä ajatuksena. Kielet eivät olleet tiukassa mielessä olioperustaisia, mutta niihin kuului abstraktin tietotyypin käsite, joka on oleellinen myös olio-ohjelmoinnissa. Nämä kielet loivat maaperää olioperustaisen ohjelmoinnin läpimurrolle tuomalla tietoabstraktion ohjelmistokehitykseen.

Läpimurto

Olioperustainen ohjelmointi teki läpimurtonsa 1980-luvun puolivälissä, kun useat tahot kehittivät olioperustaisia kieliä, ohjelmistojen suunnittelumenetelmiä ja niitä tukevia CASE-välineitä sekä yksittäisiä sovelluksia. Lisäksi kiinnostuksen kasvu näkyi uusissa olioperustaisuutta käsittelevissä konferenssisarjoissa, lehdissä ja kirjoissa. Ensimmäinen tuotantokäytössä yleistynyt ohjelmointikieli oli C++. Bjarne Stroustrup kehitti sen Bell Labs -tutkimuskeskuksessa. Vuonna 1980 julkaistu versio oli kehitetty C-kielestä, johon oli lisätty oliotuki. Stroustrup kutsui sitä nimellä ”C with classes”. Kieli otettiin 1983 käyttöön myös Bell Labsin ulkopuolella, jolloin Rick Mascitti keksi nimen C++. Kielen kehittämisen motiivina oli alkuun saada simulointisovelluksiin Simulaa tehokkaampi kieli. Uuden kielen pohjaksi valittiin C, koska se oli suhteellisen matalan tason ohjelmointikieli ja sille oli valmiina toteutuksia lähes kaikissa ympäristöissä. C++ kehittyi vielä voimakkaasti 1980-luvun aikana: siihen lisättiin muun muassa moniperintä, parametrisoidut tyypit ja poikkeusten käsittely. Borland lisäsi omaan Turbo Pascal -kehitysympäristöönsä olio-ohjelmoinnin mahdollisuuden vuonna 1986 (Macintosh-versio). MS-DOS versio julkaistiin vuonna 1989. 
Java-ohjelmointikielen kehitys lähti liikkeelle 1990-luvun alkupuolella, kun tarvittiin luotettavia ohjelmistoja erilaisille mahdollisesti verkkoon kytketyille elektronisille laitteille. Sun Microsystemsillä toiminut ryhmä halusi kehittää tarkoitukseen oman kielensä. Ryhmä tutki ensin C++:n käyttöä, mutta he totesivat sen monissa tapauksissa epätarkoituksenmukaiseksi. Java kehitettiin C++:n pohjalta, ja sen pääasiallisin kehittäjä oli James Gosling. Siitä poistettiin monia C++-kielessä virheitä aiheuttavia ominaisuuksia, kuten moniperintä. Sun Microsystemsin ryhmä alkoi 1994 tutkia Javan sopivuutta verkkosovelluksiin, ja keväällä 1994 yritys julkisti Javan lisäksi HotJava-selaimen. Java alkoi tämän jälkeen levitä verkossa, ja uusia Java-ohjelmistoja ilmestyi nopeaan tahtiin. Microsoftkin ilmoitti vuoden 1995 lopulla lisäävänsä ohjelmistoihinsa Java-tuen.
Python-kieli on tullut suosituksi vuosituhannen vaihteen jälkeen. Python on tulkattu ja dynaamisesti tyypitetty ohjelmointikieli, jossa tyyppitarkastukset tehdään ajon aikana. Pythonia pidetään varsin puhdasoppisena oliokielenä ja se on rakenteeltaan yksinkertainen. Pythonia käytetään eniten pienimuotoisten prototyyppien nopeaan kehitykseen.
2000-luvulla on syntynyt ainakin yksi uusi merkittävä oliokieli. Javaa hyvin paljon muistuttava C# on Microsoftin vuonna 2002 lanseeraama .NET-alustassa käytettävä kieli.

Periaatteita

Olio-ohjelmoinnissa pyritään olioiden hyvään koheesioon. Olioiden tulee vastata jotakin yksiselitteistä ja selkeärajaista käsitettä. Olion tietojäsenet liittyvät kiinteästi toisiinsa. Esimerkiksi opintorekisteritietojärjestelmässä ei samaan olioon tallenneta opiskelijan ja hänen käymiensä kurssien tietoja, vaan opiskelijaolio sisältää pelkästään opiskelijan tiedot ja viittaukset opintosuoritusolioihin.
Olioiden välisten riippuvuuksien hallinta on tärkeää laajoissa oliojärjestelmissä. Oliolla on riippuvuus kaikkiin niihin olioihin, joille se lähettää viestejä. Olioiden tulisi kommunikoida vain muutamien läheisesti olioon liittyvien olioiden kanssa. On vältettävä tilannetta, jossa järjestelmän kaikki oliot joutuvat olemaan tietoisia suuresta määrästä muita olioita eri puolilta järjestelmää.
Olioita pyritään ohjelmoimaan niiden rajapintoja vastaan. Rajapinta kuvaa olion tarjoamat palvelut, eli mitä olio tekee. Olion toteutuksen yksityiskohdat, eli miten olio toimii, pyritään piilottamaan muilta olioilta. Käytännössä olion rajapinta koostuu sen julkisista metodeista ja jäsenmuuttujista. Oliolla voi olla myös suojattuja metodeja ja jäsenmuuttujia, jotka eivät ole käytettävissä olion ulkopuolelta. Usein jäsenmuuttujien käsittely olion ulkopuolelta estetään ja olion tilaa voidaan käsitellä vain sen jäsenfunktioiden avulla.
Rajapintaluokat (interface) määrittelevät pelkästään rajapinnan ilman toteutusta. Rajapintaluokkien käyttö mahdollistaa tietyn toiminnon toteutuksen varioimisen ohjelmassa. Konkreettiset luokat, jotka perivät rajapintaluokan, voivat toteuttaa rajapintaluokan määrittelemän rajapinnan haluamallaan tavalla.

Menetelmiä

Olio-ohjelmointi on vaikuttanut suuresti ohjelmistotuotannon menetelmiin ohjelmiston elinkaaren kaikissa eri vaiheissa.
Olioanalyysi on keskeinen osa olio-ohjelmiston määrittelyä ja suunnittelua. Olioanalyysissa selvitetään ongelma-alueen (problem domain) käsitteet (concepts) ja niiden väliset rakenteelliset ja toiminnalliset suhteet. Olioanalyysin tuloksena on käsitemalli (concept model), josta toteutuksen luokkamäärittelyt voidaan suoraviivaisesti johtaa.
Olioanalyysia varten on kehitetty useita mallinnuskieliä. Mallinnuskielet poikkeavat monella tavalla tavallisista oliokielistä: Mallinnuskielet eivät ole suoritettavia, vaan ne kuvaavat järjestelmän rakennetta korkeammalla abstraktiotasolla ilman yksityiskohtia. Mallinnuskieliä käytetään pääasiassa graafisella notaatiolla, mikä mahdollistaa suurten kokonaisuuksien havainnollistamisen diagrammien avulla. Mallinnuskielten rakenne on kuitenkin tarkoin määritelty, jotta mallit eivät olisi tulkinnanvaraisia. Object Management Groupin (OMG) Unified Modeling Language (UML) on saavuttanut hallitsevan aseman yleiskäyttöisten mallinnuskielien joukossa.
Suunnittelumallit (design patterns) syntyivät alun perin rakennusarkkitehtuurin piirissä, mutta niiden idea otettiin käyttöön olio-ohjelmoinnissa 1990-luvun alussa. Suunnittelumalli on toistettavissa oleva ratkaisu johonkin suunnitteluongelmaan tietyssä asiayhteydessä. Suunnittelumallin kuvaus sisältää ainakin nimen, kuvauksen olosuhteista joissa sitä voi soveltaa, ratkaisun kuvauksen ja ratkaisun seuraukset. Yleisesti tunnettujen nimettyjen ratkaisumallien käyttö tehostaa viestintää ohjelmistosuunnittelijoiden välillä ja parantaa varsinaisten toteutusten selkeyttä ja rakennetta.
Olioteknologioiden käyttö ohjelmiston suunnittelussa ja toteutuksessa ei ohjaa minkään tietyn prosessimallin käyttöön tai muuta ohjelmiston laadunvarmennuksen perusperiaatteita. Olioteknologian asiantuntijat ovat kuitenkin osallistuneet aktiivisesti keskusteluun ohjelmistoprosesseista. Ketterät menetelmät, käyttötapausmallinnus ja testivetoinen kehitys syntyivät pienessä Smalltalk-kehittäjien yhteisössä, josta ne ovat levinneet yleisempään käyttöön vuosituhannen vaihteen jälkeen.

Sovellusalueita

Olio-ohjelmoinnin ensimmäisiä käyttökohteita olivat diskreetit tapahtumasimulaatiot, joissa mallinnettiin esimerkiksi puhelinjärjestelmien tai logististen järjestelmien toimintaa.
Olio-ohjelmointi soveltuu erityisen hyvin graafisten käyttöliittymien toteuttamiseen. Käyttöliittymäkirjastot, kuten Swing, SWT tai MFC sisältävät luokkahierarkian, joissa käyttöliittymän komponenttien yhteisiä ominaisuuksia hallitaan perinnän avulla.
Olio-ohjelmointi on hallitseva paradigma myös tietojärjestelmien suunnittelussa. Reaalimaailman käsitteet mallinnetaan olioanalyysissä ja käsitteitä vastaavat luokat toteutetaan ohjelmointikielellä. Yleinen ongelma tietojärjestelmissä on olioiden avulla esitetyn mallin tallentaminen relaatiotietokantaan, sillä relaatiomalli voi esittää käsitteiden välillä monesta moneen suhteita, mutta ei toisaalta pysty esittämään perintäsuhteita.
Hajautetuissa oliosovelluksissa oliot on sijoitettu hajalleen eri palvelinten välille. Olioiden välinen viestintä hoidetaan sovitun protokollan, esimerkiksi SOAPin, CORBA tai RMI:n avulla. Olion luominen ja purkaminen tapahtuvat näennäisesti oliota kutsuvalla koneella, mutta itse olion suorittama proseduuri voidaan ajaa jollakin toisella tietoverkossa sijaitsevalla palvelimella.

Ongelmia

Kaikista eduistaan huolimatta olio-ohjelmointi ei ole täysin ongelmatonta. Siinä missä proseduraalisessa ohjelmassa yhdessä kohtaa esiintynyt virhe saattoi vaikuttaa vain paikallisesti, olio-ohjelmoinnissa virheet monistuvat välittömästi kaikkiin oliota käyttäviin tahoihin. Olio-ohjelmoinnissa hyvän suunnittelun ja automatisoidun yksikkötestauksen merkitys korostuu. Väärin käytettynä olioilla voi myös olla suorituskykyvaikutuksia. Koska jokainen olio rakennetaan aina kokonaisuudessaan tietokoneohjelman muistiin, suurten oliomäärien käsittely voi viedä huomattavasti muistia.
Olio-ohjelmoinnin perusfilosofian (ns. ohjelmointiparadigma) opettelu vie myös usein aikaa. Perinteisestä proseduraalisesta ohjelmoinnista olio-ohjelmointiin siirtyminen on joskus vaikeaa jopa kokeneille ohjelmistosuunnittelijoille. Nykyisin olio-ohjelmointia opetetaan useilla eri koulutustasoilla ja peruskäsitteet hallitaan yleisesti ottaen paremmin kuin ennen. Tästä huolimatta moni ohjelma suunnitellaan näennäisesti olioilla, mutta käytännössä ne noudattavat samaa vanhaa ohjelmointityyliä, johon aikaisemmin on opittu. Olio-ohjelmointi ei siis tee kenestäkään parempaa ohjelmoijaa eikä automaattisesti anna mitään etua suhteessa perinteiseen ohjelmointiin, ellei näitä etuja osata oikealla tavalla hyödyntää.
Luokkien periyttämisen heikkoutena on eri luokkien käytännön toteutuksen sitominen toisiinsa, mikä saattaa aiheuttaa ongelmia siinä vaiheessa kun ohjelmistosta tehdään uusia versioita. Näiden ongelmien ennaltaehkäisemiseksi on tehty suunnittelumalleja, jotka perustuvat hyväksi havaittuihin ratkaisuihin.