Oliosuunnittelu

 

Joku risupartainen ja kyynisen kieroutunut vanha jäärä saattaisi sanoa, että kaikki mitä tässä oppaassa on tähän mennessä kerrottu ei edes mitään ohjelmointia olekaan. Vain yksinkertaisia ohjeita siitä miten eräs ohjelmointikieli toimii.

Se on kyllä totta, että ohjelmoinnissa on paljon muutakin kuin ohjelmointikielen osaaminen. C++-kielen kehittäjä Bjarne Stroustrup kirjoittaa kirjassaan, että hän pyrkii opettamaan olio-ohjelmointia eikä C++-kieltä. Joten nyt minäkin vaatimattomasti yritän summata niitä olio-ohjelmointiin liittyviä suuria totuuksia, joihin olen törmännyt varsinkin työelämässä suurempien projektien yhteydessä. Tätä kappaletta voisi kuvata muutamaksi omakohtaiseksi tarinaksi oliosuunnittelusta ja siihen liittyvistä ongelmista.

Kappaleet lähtevät jossakin mielessä konkreettisista asioista ja kohoavat yhä käsitteellisemmälle ja myös ristiriitaisemmalle tasolle. Ota siis itseäsi niskasta kiinni ja sinkoa henkinen minäsi maata kiertävälle radalla. Irrottaudumme C++-kielen kiekuroista ja otamme hieman lintuperspektiiviä olio-ohjelmointiin!

 

Luokan kaksi liittymää

Kuten tiedät, oliossa yhdistyy tiedot ja toiminnallisuus. Tiedot, ohjelman muuttujat, ovat kapseloituja. Tätä kutsutaan informaation piilotukseksi, information hiding. Huomaa että en puhu tiedoista, ohjelman konkreettisesta datasta, vaan informaatiosta. Informaatiolla tarkoitetaan tietoa siitä, miten luokka oikeasti toimii. Luokka on musta laatikko, informaatio luokan - eli sen olioiden - toiminnasta on piilotettu. Tiedon rakenne, siis muuttujat ja niiden tyypit, kertoo hyvin paljon luokan toiminnasta ja niinpä se pitää piilottaa käyttäjältä, jotta pystymme kätkemään luokan todellisen toiminnan.

Niinpä - käyttäjän näkökulmasta - kaikkein tärkeintä luokassa on sen liittymä. Liittymän muodostavat luokan julkiset metodit: ne ovat mitä ulkopuolinen käyttäjä näkee (huomaa että ystäviksi määritellyt oliot ja funktiot näkevät luokan yksityisenkin tiedon). Julkisen liittymän suunnittelu on tärkeämpää kuin sisäisen toteutuksen: virhe sisäisessä toteutuksessa voidaan korjata lennossa, kulissien takana, käyttäjät eivät huomaa mitään. Mutta mikäli luokan liittymä muuttuu, joutuvat luokan käyttäjät muuttamaan omaa koodiaan. Pahimmassa tapauksessa käyttäjät ovat rakentaneet oman ohjelmansa jonkin ominaisuuden päälle, joka katoaa tai muuttuu - ja käyttäjät ovat silloin todella pulassa.

Kun puhun käyttäjistä, ei se tarkoita että tämä koskisi vain yleisesti käytettävien luokkakirjastojen suunnittelijoita. Käyttäjä voi olla ja lähes aina on luokan suunnittelija, sinä! Huomenna tai viikon päästä tai ensi vuonna et muista enää miten tekemäsi luokka tarkkaan ottaen toimii. Mutta muistat kyllä miten sitä käytetään, jos liittymä on taidokkaasti suunniteltu. Kirjoitat luokan tänään ja otat sen käyttöön suuremmassa kokonaisuudessa huomenna. Mikäli luokkasi ei ole kunnolla suunniteltu ja kapseloitu, joudut pohtimaan sitä jatkuvasti, etkä pysty keskittymään muun kokonaisuuden suunnitteluun. Hyvä ohjelmoija on nero, joka luo mahtavia, oivaltavia ja uskomattomia luokkia, kapseloi ne ja unohtaa seuraavana päivänä.

Kun suunnittelee luokkaa, on siis ensiarvoisen tärkeää millaisen liittymän luokka julkistaa muille luokille. Eikä siinä kaikki. Luokalla ei nimittäin ole vain yhtä liittymää ulkomaailmaan, vaan itseasiassa kaksi. Luokan näkökulmasta katsottuna ulkomaailma jakautuu kahteen osaan. Keksitkö mihin kahteen?

Mitkä luokat ovat erityisasemassa muihin verratuna? No, luonnollisesti kantaluokat. Mutta itseasiassa kantaluokat eivät ole osa ulkomaailmaa, vaan ne ovat osa itse periytettyä luokkaa. Kantaluokat eivät ole vastaus, mutta liippaavat läheltä. Oikea vastaus on lapsiluokat. Lapsiluokat näkevät enemmän kantaluokastaan kuin muut. private-tieto on niille näkymätöntä ja public julkista, kuten kaikille muillekin. Mutta on kolmaskin vaihtoehto, protected eli suojattu näkyvyys. Muistathan? protected on avainsana, jolla määritelty tieto ei näy julkisesti, mutta näkyy kantaluokille. 

Milloin tulisi siis käyttää private-näkyvyyttä, milloin vähemmän rajattua protected-näkyvyyttä? En tiedä, en todellakaan tiedä. Olemme nyt nimittäin niin kurjan pitkällä C++:n ominaisuuksissa, että jykeviä totuuksia ei tahdo oikein enää löytyä. Asian tekee erittäin hankalaksi se, että kantaluokka harvemmin tietää lapsiluokistaan - ja vastaavasti luokan ohjelmoija ei voi ennustaa miten luokkaa tullaan periyttämään. Tässä esimerkki tilanteesta:

class MuotoiltuAika
{
private:
	int sekunteja;
public:
	string annaMuotoiltuAika();
};

string MuotoiltuAika::annaMuotoiltuAika()
{
	string p;
	int tunteja = sekunteja / 3600;
	int minuutteja = (sekunteja - (3600 * tunteja)) / 60;
	if (tunteja % 12 > 0) {
		p = "PM";
		tunteja -= 12;
	} 
	else p = "AM";

	stringstream ma;
	ma << tunteja << ":" << minuutteja << " " << p;
	return ma.str();
}

     
class SuomalainenMuotoiltuAika : public MuotoiltuAika 
{
public:
	string annaMuotoiltuAika();
};

string SuomalainenMuotoiltuAika::annaMuotoiltuAika()
{
	// ei voi kääntää, koska sekunteja ei näy tähän lapsiluokkaan
	string p;
	int tunteja = sekunteja / 3600;
	int minuutteja = (sekunteja - (3600 * tunteja)) / 60;

	stringstream ma; 
	ma << tunteja << ":" << minuutteja;
	return ma.str();
}

Luokkaa SuomalainenMuotoiltuAika ei voi toteuttaa, koska sekunteja on yksityinen. MuotoiltuAika-luokkaa ei siis voi järkevästi periyttää, siitä ei voi rakentaa erikoistuneita versioita. Se on lajinsa viimeinen. Mikäli sekunteja olisi suojattu (protected), se olisi näkyvissä lapsiluokille ja eri maihin sopivia muotuiluja voitaisiin lisätä. Ja mikäli annaMuotoiltuAika olisi virtuaalinen, voitaisiin muotoiluja käyttää polymorfisesti ja siis ohjelman lokalisointi eli maan merkintätapoihin mukauttaminen olisi erittäin helppoa. MuotoiltuAika-luokan liittymä täysin ulkopuolisiin luokkiin on onnistunut, mutta liittymä lapsiluokkiin on mennyt pieleen.

Ainoa resepti näihin tilanteisiin on: mieti! Kun teet luokkaa, jota et itse juuri nyt aio periyttää, niin mieti silti olisiko sitä edes mahdollista järkevästi periyttää. Se on helpommin sanottu kuin tehty, mutta erilaisia tulevaisuuden käyttömalleja miettimällä lopputulos on ainakin parempi kuin keskittymällä pelkästään julkiseen liiittymään - joka toki taas on parempi kuin luokan liittymien suunnittelun unohtaminen kokonaan. Mietintöjen lopputulos ei automaattisesti ole, että luokka avataan lapsiluokille suojatun näkyvyyden avulla ja mahdollinen polymorfinen käyttö otetaan huomioon. Tulos voi hyvinkin olla, että luokkaa ei pidä periyttää - sen sisuskalut ovat sellaisia, että niitä ei voi avata lapsiluokille. Syynä voi olla esimerkiksi se, että luokka tulee ehkä radikaalisti muuttumaan tai että luokan metodit muodostavat monimutkaisen kokonaisuuden jonka muokkaaminen lapsiluokissa johtaisi helposti virheisiin. Tosin kannattaa kysyä, olisiko tällaisessa tapauksessa parempi pilkkoa monimutkainen luokka osiin. Kuitenkin: syvällisten pohdintojen perusteella voi olla perusteltu ratkaisu sulkea luokka periyttämiseltä kokonaan.

Onko tähän olemassa muita niksejä tai suuria teorioita? Niksejä kyllä. Niihin voit tutustua perehtymällä suunnittelumalleihin (design patterns). Suuria teorioita ei ole. Periyttäminen on mielestäni kiinnostava asia myös teoreettisessa mielessä. Yllä esiteltyä ongelmaa pidetään oliomallin suurimpana heikkoutena, tosin yleensä ajateltuna toisin päin: miten lapsiluokka voi suojautua kantaluokan muutoksilta. Mikäli minulta kysyttäisiin mikä olisi paras ratkaisu näissä perintään liittyvissä ongelmassa, voisi vastaukseni hyvinkin olla: hankkiutua periytymisestä kokonaan eroon. Miksikö? Se on pitkä tarina, en sitä tässä yhteydessä käsittele. Mutta voit ajatusleikkinä miettiä millaista olio-ohjelmointi olisi ilman yhteisen toiminnallisuuden jakamista luokkahierarkioiden avulla. 

 

Ohjelmoinnin ihmissuhdeteoria

Hyvät ystävät ovat ihmisiä, joihin voi luottaa. Hyvälle ystävälle voi vaikka antaa oman pankkikorttinsa tunnuslukuineen. Mattobasaarin kauppiaalle sen sijaan en suosittelisi kovin luottamuksellisten luottotietojen luovuttamista. Miten tämä sitten liittyy ohjelmointiin?

Ohjelmien maailmassa ei ole ystäviä. Kaikki ovat helppoheikkejä ja kepuliveivareita, jotka yrittävät huijata sinua. Puhun nyt luonnollisesti olioista ja muista ohjelman osista. Ohjelmia suunnittellessa pitäisi lähteä siitä, että olio tai muu ohjelman osa ei saa kertoa itsestään muille mitään, mitä voitaisiin käyttää sitä vastaan. Käännän äskeisen esimerkin C++-kielelle, jossa kaikki ovat ystäviä keskenään ja luottavat toisiinsa:

class CreditCard // luottokortti
{
public:
	void transferMoney(int amount, string secretNumber, string destAccount);
	void bill(int amount, string destAccount);
};

class Person // henkilö
{
public:
	CreditCard* getCreditCard();
	string getSecretNumber();
private:
	CreditCard card;
	string secretNumber;
};

class AnotherPerson // toinen henkilö, paras kaveri
{
	void bill(Person& bestFriend);
private:
	string myAccount;
};

void AnotherPerson::bill(Person& bestFriend) // laskuta toisen luottokorttia
{
	// laskuta luottokorttia, tämän metodin tarkoitus
	bestFriend.getCreditCard()->bill(100, myAccount);
	
	// AnotherPerson voisi myös kavalasti nostaa rahaa kaverinsa tililtä
	// bestFriend.getCreditCard()->transferMoney(10000, bestFriend.getSecretNumber(), myAccount);
	
	// tai hävittää (tai vaihtaa) kaverinsa luottokortin
	// delete bestFriend.getCreditCard();
}

Tämä rakenne toimii, mutta ei ole hyvä. Se rikkoo informaation piilotuksen periaatetta vastaan. Periaatteen voisi muotoilla myös informaation paikallisuuden periaatteeksi: ohjelman osan rakenteesta ja toiminnasta kertova informaatio tulee pitää mahdollisimman paikallisena. Ystäville voit kertoa kaikki salaisuutesi - mutta ohjelmoinnin maailmassa sinulla pitäisi olla mahdollisimman vähän ystäviä. Rakenteellisesti parempi versio äskeisestä esimerkistä olisi tällainen:

class CreditCard // luottokortti (sama kuin äsken)
{
public:
	void transferMoney(int amount, string secretNumber, string destAccount);
	void bill(int amount, string destAccount);
};

class billingException {}; // virhe laskutuksessa

class Person // henkilö
{
public:
	void bill(int amount, string account);
protected:
	CreditCard& getCreditCard();
	string getSecretNumber();
private:
	CreditCard card;
	string secretNumber;
	static const int MAX_BILL;
};

const int Person::MAX_BILL = 25000; // suurin laskutettava summa

class AnotherPerson // toinen henkilö, epäluotettava
{
	void bill(Person& bestFriend);
private:
	string myAccount;
};

void Person::bill(int amount, string account)
{
	if (amount < MAX_BILL) card.bill(amount, account); 
	else throw billingException();
}

void AnotherPerson::bill(Person& bestFriend)
{
	bestFriend.bill(10000, myAccount);
}

Tässä versiossa AnotherPerson ei saa CreditCard-oliota haltuunsa eikä voi tehdä sille mitään pahaa. CreditCardin saavat vain Person-luokan alaluokat. Myöskin tunnusnumero on suojattu vain alaluokille. Kun laskutusta ei tehdä suoraan luottokortilla, vaan Person-luokan bill(..)-metodin kautta, voi Person valvoa tapahtumaa ja tarvittaessa heittää poikkeuksen ja estää laskutuksen.

Nyt verryttelemme visuaalista hahmotuskykyä: kuvittele ohjelman rakenne verkostona - vaikka ihmissuhteiden verkostona. Huono ohjelma on sellainen, jossa voidaan lähteä jostain kohti verkkoa ja kulkea verkon linkkejä pitkin mihin halutaan. Mikäli jostain ohjelman osasta voidaan saavuttaa tarpeetonta tietoa jonkin toisen osan toiminnasta, ovat nämä kaksi osaa kytkettyjä toisiinsa. Ja kun yksi osa muuttuu, niin joudutaan toistakin muuttamaan. Kun kyseessä ovat verkostot, jossa jokaisen linkin takana on useita uusia, johtaa tämä suunnitteluvirhe työmäärän eksponentiaaliseen räjähtämiseen ja käytännössä siihen, että osa työstä unohtuu ja jää tekemättä eli ohjelmaan tulee virheitä - mikä taas aiheuttaa hankaluuksia.

Hyvä ohjelma onkin kuin kartano, joka on täynnä lukittuja huoneita. Huoneesta toiseen näkee pelkästään avaimenreiän kautta. Ja voit kuvitella, että avaimenreiästä ei enää näe toisen huoneen avaimenreikää ja sitä kautta kolmatta huonetta. Siis jos sinun makuuhuoneesi ja uima-altaan välissä on pukuhuone, et koskaan saa tietää mikäli uima-allas jäädytetään ja siihen rakennetaan iglu. Uima-altaan rakenteesta kertova tieto on paikallista, eikä kuulu sinulle, koska et asu siinä. Jos siis hyvää ohjelmaa ajatellaan verkkona, ovat sen linkit kapenevia - niitä ei pysty seuraamaan kuin korkeintaan parin eri ohjelman osan läpi. Koitan havainnollistaa tätä toteamusta pienellä esimerkillä, joka ei onneksi liity millään tavalla igluihin. Siinä on eri funktioita, jotka muodostavat kutsuketjun ja joissa aina seuraava kutsuja saa kapeamman näkymän tietoalkioon - toisinsanoen jokainen saa sellaisen liittymän tietoon, joka on juuri ja juuri riittävä.

#include<iostream>

using std::cout;
using std::endl;

void j(const int luku)
{
//	luku++; // tämä olisi laiton, koska luku on vakio tälle metodille (const-parametri)
	cout << "j() vain tulostaa luvun: " << luku << endl;
}

void i(int luku)
{
	luku++; // luvun paikallinen kopio on 5
	cout << "i(): luku: " << luku << endl;

	j(luku);
}

void h(int* luku) 
{
	(*luku)++; // muutan alkuperäistä lukua, tarvitsen siihen osoittimen
	i(*luku); // *luku on 4
}

void g(int** luku)
{
	(*luku)++; // siirrän osoitinta, joten tarvitsen osoittimen osoittimeen
	h(*luku); // **luku on nyt 3
}

int main() 
{
	int taulu[] = {1, 2, 3};
	int* luku = taulu + 1; // osoittaa lukuun 2

	cout << "*luku " << *luku << endl; // pitäisi olla 2
	g(&luku);
	cout << "*luku " << *luku << endl; // pitäisi olla 4

	return EXIT_SUCCESS;
}

Huomaa kuinka ketjun ensimmäinen metodi saa kaksoisosoittimen, jonka avulla se voi muuttaa main():ssa olevaa osoitinta. Seuraava saa osoittimen, jolla se voi muuttaa alkuperäistä lukua, seuraava pelkän kopion arvosta jota se voi kasvattaa (mutta se ei vaikuta alkuperäiseen) ja viimeinen pelkästään arvon, jota se ei voi kasvattaa edes paikallisesti. Yksikään metodi ei voisi tehdä enempää kuin nyt tekee. Jokainen metodi siis katsoo aina vaan pienemmästä avainmenreiästä alkuperäistä tietoa eli main()-metodin luku-osoitinta.

 

Oliosuunnittelun pyhä kolminaisuus

Oliosuunnittelu on käsitteenä suuri mutta venyvä kuin ohjuskauppiaan omatunto. Nyt kuitenkin ajan mutkat suoriksi ja vedän kulmat pyöreiksi ja yritän summata koko kupletti juonen niin tiiviiksi paketiksi, että se mahtuisi vaikka tekstiviestiin. Opeta itsellesi oliosuunnittelu viidessätoista minuutissa, ole hyvä!

Asian voi parhaiten aukaista pohdiskelemalla käytännön esimerkkiä ja siihen liittyviä ongelmia. Kuvitelkaamme ampumasimulaattori, jossa on kaksi maalityyppiä, ihminen ja nukke, luokat Human ja Dummy. Molempiin voi ampua, mutta Dummyn voi nostaa ylös osuman jälkeen. Molemmat luokat käyttävät samanlaista osumametodia ohjelman ensimmäisessä versiossa. Ohjelmakoodina asia voitaisiin esittää näin:

class Human 
{
public:
   void hit(int precision);
};
     
class Dummy 
{
public:
   void hit(int precision);
   void liftUp();
};
     

Mutta hyi hyi! Kyseessähän on selvä ohjelmakoodin kopiointi, täysin samanlainen hit(int)-metodi on koodattu kahteen kertaan. Otetaanpa uudelleen, käyttäen periytymistä avuksi:

class Human 
{
public:
   void hit(int precision);
};
     
class Dummy : public Human 
{
public:
   void liftUp();
};

Silmää jää hieman kirvelemään nuken periyttäminen ihmisestä, koska nukkehan ei liene ihminen. No, se lienee vain allerginen reaktio, ei sen kummempaa. Yksi todellinen käytännön ongelma koodissa kuitenkin on: kapselointi ei ole täysin kunnossa. Simulaattorin ensimmäisessä versiossa ei ole osumakohtien mallinnusta, mutta sellainen tulee jo seuraavaan versioon. Pohdiskelemalla tätä tulevaisuuden visiota näemme, kuinka kapselointi ei todellakaan ole kohdallaan: pahvisella nukella ei ole osumakohtia, mutta ihmisellä on ja metodia joudutaan muuttamaan, jolloin muutos virheellisesti heijastuu myös nukkeen. Ajatus periyttää ihminen nukesta olikin käytännössä mätä - tarkka silmä ja delikantti vainu ei koskaan erehdy!

Voi itkujen itku ja surkeuksien kurjuus, joudumme ilmeisesti tässä tilanteessa valitsemaan pienemmän kahdesta pahasta. Periytyminen ei vie puusta pitkään, pyörittelipä sitä miten vaan. Nuken periyttäminen ihmisestä ei ainakaan paranna tilannetta. Ihmisolion voisi myös laittaa osaksi nukkeoliota, jolloin nukke voisi delegoida osumien käsittelyn ihmiselle ja ohjelman jatkokehityksessä viritelmä voitaisiin purkaa. Mutta silloinhan jos kentällä olisi enää yksi nukke, laskisi simulaattori siellä olevan myös ihmisen ja se johtaisi määrittelemättömiin ongelmiin. Tämäkin ajatus oli surkea viritelmä jo syntyessään. Lienee parasta tyytyä ensimmäisenä esitettyyn, pieni koodin kopioiminen ei haittaa, kun asia tullaan kakkosversioon kuitenkin muuttamaan.

Ratkaisu ei kuitenkaan ole tyydyttävä. Kokemuksestani voin antaa vinkin: jos joku ratkaisu jää hieman kaivelemaan sinua, on se ollut väärä. Toki toisaalta pitäähän ohjelman valmistua ja jotkut ongelmat eivät vaan ratkea. Mutta se johtuu hyvin usein siitä, että olet etsinyt ratkaisua väärältä tasolta. Muista! Kolme tärkeintä oliosuunnittelun menetelmää ovat abstraktio, abstraktio ja abstraktio. Näin on joku alan guru joskus todennyt. Mikä tahansa noista kolmesta ratkaisee tämänkin ongelman.

Voimme luoda abstraktion, yläluokan Target, jossa hit()-metodi on. Siis kun asiaa oikein miettii, niin ihmiset ja nukethan ovat maaleja. Maali on abstrakti käsite, joka yhdistää molempia. Kun kapseloinnin ja uudelleenkäytön ristitulessa yrittää sommitella palasi kohdalleen, on abstraktio juuri se temppu jolla ne loksahtavat. Muuten saa jatkaa venkuilua vaikka maailmanloppuun asti, siltikään ei päädy kuin välttävään ratkaisuun. Ongelma on ratkaistu, opetus opittu, tässä on koodi:

class Target 
{
protected:
   Target(); // suojattu muodostin
public:
   virtual void hit(int precision);
};
  
class Human : public Target
{
};
     
class Dummy : public Target
{
public:
   void liftUp();
};
	 

Kakkosversio ei synny ongelma, Human vain esittelee oman osumametodinsa joka korvaa Target-luokan metodin - ja ehkä jopa käyttää sitä hyväkseen, mikäli uudessa osumankäsittelyssä tarvitaan vanhankin toiminnallisuutta. Käsitteellisellä tasolla luokkahierarkia on myös selkeä eikä mikään jää häiritsemään kokonaisuutta. Kun kapselointi ja uudelleenkäyttö riitelee, niin abstraktio palaset kohdalleen siirtelee. Siinä oliosuunnitteluun mielestäni tärkein idea, pyhä kolminaisuus.  

Muodostinfunktio on  määritelty suojatuksi, jotta Target-olioita ei voida luoda. Target on abstrakti käsite, mutta koska luokassa ei ole yhtään puhdasta virtuaalifunktioita, voitaisiin sellainen luoda - ellei muodostinta olisi suojattu. Lapsiluokat korvaavat suojatun muodostimen oletuksena julkisella muodostimella ja niinpä lapsiluokkaan voi luoda normaalisti olioita. Suojaus ei ole täydellinen, koska uusi olio voidaan luoda myös kopiomuodostimella tai sijoitusoperaattorilla. Ne olisi pitänyt suojata myöskin, mutta esimerkin selkeyden vuoksi jätettiin väliin.

Näin muuten ohimennen, esimerkki ei ollut täysin tuulesta temmattu. On aika ironista (tai lähinnä valitettavaa), että usea hyvä ohjelmistosuunnitteluun liittyvä keksintö on syntynyt sotateollisuuden tarpeisiin. Nykyajan pommit vaativat järjestelmän rakenteelta enemmän kuin tavalliset mikronkäyttäjät ja rahaahan sillä alalla on aina piisannut. Siviilisuunnittelijan kannattaakin ottaa oppia armeijan järjestelmistä - olisihan sääli jos kaikille niille hyville ideoille ei olisi mitään hyötykäyttöä.

 

Oliomallinnuksen heikkoudet

Kuten varmasti huomaat, on tämän kappaleen sävy erilainen kuin aikaisemmissa. En lauo suuria totuuksia ja höystä niitä jalkaproteesivitseillä, vaan päivittelen vaan että näin ne asiat ovat eivätkä siitä paljon paremmaksi muutu. Se johtuu yksinkertaisesti siitä, että olemme jättäneet perusasiat taaksemme ja purjehdimme aika tuntemattomilla vesillä. 

Tuntemattomilla vesillä purjehtiessa saattaa nähdä peräti hämmentäviä näkyjä. Jopa sellaisia, joissa epäillään että onko oliomallinnus sittenkään vastaus kaikkeen. Ja kyseessä ei ole pelkkä harhanäky ja illuusio, vaan tosiasia joka jokaiselle ohjelmistosuunnittelijalle on uurtunut selkärankaan: oliomallinnus ei tosiaankaan ole ratkaisu kaikkeen.

Millaiset ongelmat ovat erityisen hankalia oliomallinnuksen kannalta? Kokemuksesta voin luetella: ohjelman lokikirjanpito, käyttäjien oikeuksien valvonta, ohjelman osien asetusten hallinta, transaktiot jne... Mikä esimerkiksi lokista tekee niin hankalan? Se, että se koskettaa koko ohjelmaa. Tarpeeksi pitkälle mentäessä joka ikisen luokan tulee pystyä kirjaamaan toimenpiteitään ylös yhteiseen lokiin. Ja siitähän syntyy ongelmia - kuvittele vaikka miten vaikea on muuttaa lokijärjestelmää, kun se koskettaa koko ohjelmaa. Mitään erityisen hyvää ratkaisu ongelmaan ei ole, lokin on pakko liittyä koko ohjelmaan - rajaamalla lokikirjanpito vain pieneen alijärjestelmään ei saada tietoa koko järjestelmän käyttäytymisestä ja lokista tulee lähes hyödytön. Sama koskee käyttäjien oikeuksia ja asetusten hallintaa: ohjelmassa voidaan tehdä hyvin monenlaisia operaatioita hyvin monissa eri ohjelman kohdissa, ja jokaisen yhteydessä tulee tarkistaa onko käyttäjällä oikeuksia toimenpiteeseen - samoin asetuksia (tiedostojen nimet, verkko-osoitteet, tietokantojen asetukset...) tarvitaan joka puolella ohjelmaa. 

Havaitsemme, että nämä murheenkryynit leikkaavat poikittain ohjelmaa, ne siis eivät ole pilkottavissa oliohierarkioiksi samalla tavalla kuin muut ohjelman ominaisuudet ovat. Niitä voisi kuvata aspekteiksi, ohjelman toimintaa kuvaaviksi asioiksi joita ei voi paikallistaa mihinkään tiettyyn luokkaan tai luokkakokoelmaan. Jotta tätä ongelmaa pohtiessa voi saavuttaa valaistumisen, on hyvä tuntea hyperavaruusmalli. Se on IBM:n T.J. Watson.tutkimuskeskuksesta peräisin oleva malli, jossa ohjelman vaatimukset ajatellaan hyperavaruudeksi, moniulotteiseksi tilaksi. Ohjelman suunnittelu on tuon vaatimusavaruuden jäsentämistä, lokerointia. Jotta avarruutta voi kunnollisesti jäentää, tulee ohjelmointityökalun mahdollistaa ohjelman rakentaminen monessa eri ulottuvuudessa, siis monien eri näkökulmien avulla. Puhutaan moniulotteisesta vaatimusten  jäsentämisestä (multidimensional separation of concerns). 

Oliomaailmassa moniulotteinen jäsentäminen ei ole mahdollista. Tämän vuoksi ne asiat, jotka ovat hyperavaruudessa samalla tasolla oliomallin kanssa jäsentyvät helposti, mutta oliomallin kanssa eri ulottuvuudessa olevat asiat eivät. Tätä kutsututaan hallitsevan mallin tyranniaksi (tyranny of dominant decomposition). Kyse ei ole pelkästään oliomallia vaivaavasta ongelmasta, vaan minkä tahansa ohjelmointimallin valitseminen johtaa joidenkin ohjelman kannalta ehkä oleellistenkin ulottuvuuksien laiminlyöntiin. 

Mutta toisaalta, jokin tapa jäsentää on parempi kuin ei jäsentämistä ollenkaan. Niinpä oliomalli on varsin hyvä ratkaisu monissa tilanteissa - ja tulee myös muistaa, että uudet ulottuvuudet tekevät ohjelmasta hankalamman hahmottaa. Merkittävin hallitsevan mallin tyranniasta johtuva ongelma oliomaailmassa ovat aspektit. Erityisen yleisiä  aspektimaiset ominaisuudet ovat suurissa monen käyttäjän palvelinohjelmistoissa, kuten verkkokaupoissa ja intranet-sovelluksissa. Mitä enemmän voidaan olettaa tiettyjä vakioita (sama ympäristö, sama käyttäjä, vain yksi yhtäaikainen käyttäjä), sitä vähemmän ulottuvuuksia ohjelman vaatimuksissa on. Siis konkreettisesti, tavallisen Windows-sovelluksen suunnittelu on siksi helppoa, että liikkuvia osia on vähän. Ehkäpä juuri sen takia tasapäistävä Windows onkin menestynyt niin hyvin. Verkkomaailma, jossa hyvin harvojen asioiden voidaan olettaa pysyvän vakioina, on haastavampi mallinnettava. Eikä pelkästään siksi, että se on uusi asia, vaan juuri teoreettisen pohjan vuoksi. Verkkosovellusten vaatimuksissa on enemmän ulottuvuuksia, joten niiden hallitsemiseen tarvitaan moniulotteisempaa mallinnusta.

Verkkoympäristöjen monitahoisuus ei ole jäänyt huomaamatta ohjelmistoteollisuudelta. Vaikka harva tuntee hyperavaruusmallia, niin silti mallin hengessä olevia teknologioita on syntynyt jo useita. Merkittävin kehityslinja ovat komponenttiteknologiat (CORBA, EJB, COM+), joissa yksi avainsana ovat nk. komponentin deklaratiiviset ominaisuudet. Komponentti on oliota tiiviimmin paketoitu ohjelman osa. Komponenttiin liittyy määrittelytiedosto, jolla se säädetään toimimaan siinä ympäristössä missä sitä milloinkin käytetään. Määrittelytiedostoissa määritellään deklaratiivisia ominaisuuksia, kuten se, että miten komponentti osallistuu transaktioihin - eli jos joku menee pieleen, niin miten järjestelmä peruu keskeneräisen toimenpiteen. Siis osa ohjelman älystä on siirretty määrittelytiedostoihin. Määriteltävät asiat ovat hyvin aspektimaisia - kuten esimerkiksi transaktiot. Ne koskevat koko järjestelmää. Mielestäni merkittävin komponenttiteknologioiden tarjoama etu on vaatimuksen moniulotteinen jäsentäminen, eli aspektien parempi kapselointi.

Myös puhtaasti aspekti- ja hyperavaruusajattelun pohjalta on syntynyt konkreettisia teknologisia ratkaisuja. Aspektiohjelmointi on eräs tutkimussuunta, joka on tuottanut AspectJ-ohjelmointikielen (Javaan perustuva). AspectJ:n avulla tehdään jo jonkinverran oikeitakin ohjelmia ja myös C-versio on kehitteillä. Hyperavaruusmallia, joka on aspektiajattelua laajempi malli, voi kokeilla Hyper/J-ohjelman avulla. Hyper/J tukee hyperavaruuksia Java-ohjelmoinnisssa. Totuus kuitenkin on, että mihinkään todella vakavaan työhön ei näistä tutkimusprojektien tuotoksista vielä ole. Sen sijaan komponenttiteknologiat ovat jo ihan arkipäivää.

 

Takaisin