Ohjelmoinnin harjoitustyö (periodi I) : Suunnitteludokumentin ohjeet
Suunnitteludokumentti
Suunnittelussa kannattaa edetä seuraavasti:
- Päätä mitä luokkia ohjelmaan tulee.
- Suunnittele luokkien rajapinnat.
- Suunnittele käyttöliittymä.
- Suunnittele ohjelman toimintojen toteutus
Luokkarakenne
Ensimmäiseksi pitäisi päättää mitä luokkia ohjelmaan kuuluu. Älä kiinnitä vielä tässä vaiheessa huomiota ohjelman käyttöliittymään. Ideaalitapauksessa koko ohjelman voi nähdä yhtenä abstraktina tietotyyppinä, jota käyttöliittymä käyttää (Kuva 1).

Kuva 1. Käyttöliittymän ja tietorakenteen erottaminen
Tämä lähetymistapa selkiyttää luokkien rooleja ja mahdollistaa esimerkiksi toisen käyttölliittymän ohjelmoimisen helposti käyttämään samaa tietorakennetta (Kuva 2).

Kuva 2. Tietorakenne ja kaksi käyttöliittymää
Nyt siis suunnitellaan tuota abstraktia tietotyyppiä, joka toteuttaa ohjelman toimintalogiikan.
Luokkia kannattaa aluksi etsiä todellisesta maailmasta. Mieti mitä asioita, olioita ja esineitä ohjelmasi mallintamaan osaan todellisuudesta liittyy. Yksi mekaaninen keino luokkien löytämiseen on listata kaikki substantiivit ohjelman määrittelystä ja miettiä, mitkä niistä olisivat sopivia luokkia.
Kaikilla luokilla ei välttämättä ole reaalimaailman vastinetta, luokat voivat olla myös tietokonemaailman käsitteitä tai abstraktioita. Kaikkiin luokkiin tulisi kuitenkin liittyä jotakin tietoa: luokka on joukko yhteen liittyvää tietoa ja sen käsittelemiseen tarvittavat operaatiot.
Piirrä luokista kaavio, johon tulevat kaikki luokat ja luokkien väliset suhteet: periytyminen, koostuminen ja minkä toisten luokkien palveluita luokka käyttää. Voit esimerkiksi merkitä luokat suorakulmioilla ja niiden väliset suhteet nimetyillä nuolilla, jotka osoittavat suhteen suunnan. Luokkakaavion tekemiseen saa käyttää myös jotakin tunnettua merkintätapaa, kuten UML-tekniikkaa.
Kuvassa 3 on luokkakaavio pokeriin. Kortti kuvaa yhtä korttia, Pakka korttipakkaa, Käsi pelaajalla olevaa kättä ja Pokeri-luokka toteuttaa pokeripelin logiikan. Pakka ja Käsi koostuvat korteista ja Pokeri käyttää sekä Pakka- että Käsi-luokkaa.

Kuva 3. Esimerkki pokeripelin luokkakaaviosta
Kuvassa 4 on osa shakkipelin luokkakaaviota. Siinä Nappula on kaikkien nappuloiden yliluokka. Sotilas perii Nappulan, Ratsu perii Nappulan, ja niin edelleen.

Kuva 4. Esimerkki shakkipelin luokkakaaviosta
Luokkien rajapinnat
Kun ohjelman luokat ovat selvillä, niille pitää suunnitella rajapinta. Rajapinta tarkoittaa luokan tarjoimia palveluita, siis luokan julkisia metodeita ja muuttujia.
Muista rajapinnan suunnittelussa abstraktin tietotyypin idea: tietorakenteen käyttö on eristetty toteutuksesta (Kuva 5). Päähuomio on toimivan rajapinnan suunnittelussa, vain luokan tärkeimpien tietorakenteiden esitystapa tulee miettiä.

Kuva 5. Abstrakti tietotyyppi
Pyri tekemään luokan rajapinnasta mahdollisimman yleiskäyttöinen. Mieti mitä kaikkia toimintoja luokalle yleensä liittyy, älä rajoitu vain oman ohjelmasi tarpeisiin. Älä myöskään rasita rajapintaa tarpeettomilla operaatioilla, se tekee luokasta vaikeammin hallittavan. Tavoitteena on tiivis ja täydellinen kokoelma palveluita luokan käyttäjälle.
Voit suunnitella luokkien rajapintoja aluksi paperille. Luokkakaavioon voi merkitä myös luokkien muuttujia ja metodeita (Kuva 6). Lopuksi rajapinnan määrittelyt kirjoitetaan Javalla. Kirjoita luokat ja niiden julkisten metodien otsikot sekä mahdolliset julkiset muuttujat. Kommentoi kaikki: luokasta kuvaus, metodeista kuvaus tehtävästä, parametreista ja paluuarvosta. Suunnittelusta ei palauteta kirjallista dokumenttia, mutta kaaviot ja suunnitelmat on oltava paperilla ohjaajan nähtävillä.

Kuva 6. Muuttujien ja metodien esittäminen
Esimerkiksi edellä esitetyn Pakka-luokan rajapinnan määrittely voisi näyttää seuraavalta:
/** * Korttipakka korttipelejä varten. */ public class Pakka { /** * Pakassa olevat kortit. */ private Kortti[] kortit; /** * Luo uuden korttipakan. */ public Pakka(); /** * Nostaa pakan päällimäisen kortin. * * @return Pakan päällimmäinen kortti. */ public Kortti nosta(); /** * Sekoittaa pakassa olevat kortit satunnaiseen järjestykseen. */ public void sekoita(); // ja muita hyödyllisiä metodeja... }
Käyttöliittymäsuunnitelma
Käyttöliittymä suunnitellaan määrittelyvaiheessa tehdyn käyttöliittymäkaavion pohjalta. Jos teet graafisen käyttöliittymän, suunnittele mistä komponenteista rakennat näytöt, ja millä tavalla asettelet ne.
Tästä suunnitelusta on erityisesti hyötyä, jos et ole aiemmin tehnyt graafista käyttöliittymää!
Näyttö koostuu suunnittelumallista (layout) (Swing Tutorial) ja suunnittelumallissa sijaitsevista komponenteista (Swing Tutorial). Huomaa, että suunnittelumallin osa voi sisältää myös uuden suunnittelumallin, jolloin komponentit saadaan tarkemmin sijoitettua ikkunaan. Kuvassa 7 on esimerkkikuva suunnittelumallista ja kuvassa 8 erään komponentin suunnittelusta

Kuva 7. Suunnittelumalli

Kuva 8. Komponentin suunnittelua
Suunnittele lisäksi, mitä luokkia tarvitset käyttöliittymän toteutukseen. Voit lisätä käyttöliittymäluokat luokkakaavioosi, tai tehdä niistä oman kaavion.
Toiminnan hahmottelu
Luokkarakenne ei vielä kerro mitään siitä, missä järjestyksessä luokat kutsuvat toisiensa metodeita ja mitkä luokat ylipäätään käyttävät toistensa palveluita. Tämä olioiden "juttelu" tai viestintä on olio-ohjelman ehkä tärkein suunnitteluvaihe, ja se määrää suoraan, minkälainen ohjelmasta muodostuu.
Muistele aluksi mitä ohjelman toimintoja määrittelit määrittelydokumentissa. Tarkoituksena on tehdä näiden toimintojen kulusta sekvenssikaavio eli olioyhteistyökaavio. Kun mietit millaisia toimintoja ohjelmaasi tulee,
- Hahmottele tyypillisiä ohjelman käyttötilanteita, konkreettisia esimerkkejä yksittäistapauksista. Esim. ohjelman aloitus ja alustus, yhden tyypillisen "kierroksen"/etenemisaskelen/toiminnon kulku, jonkin toisen tyypillisen tilanteen kulku, poikkeustilanne, virhe, tilanteen muuttuminen, tiedon hakeminen/analysointi/keruu...
- Mikä luokka kutsuu mitäkin? Milloin? Miksi? Mitä kutsu palauttaa?
- Nimeä myös, mitä tietoa kutsuissa välitetään ja palautetaan.
- Tilanteita voi tarkentaa toisissa kuvauksissa, eli kuvaus voi rakentua osista.
- Jos huomaat, että jonkin tiedon kaivaminen tai välittäminen ei onnistu, tai muuten hahmotelma kaipaa täydentämistä, lisää uusia suhteita olioiden välille luokkakuvauksessa tai keksi uusia luokkia.
Erityistä huomiota täytyy kiinnittää kontrollin sijaintiin ohjelmassa: olio-ohjelma koostuu alusta loppuun metodikutsuista, eli palvelupyynnöistä, joita oliot lähettelevät toisilleen. Täten kontrolli on vuorollaan aina jollain tietyllä oliolla, ja sen pitäisi myös palata aikanaan palvelupyynnön esittäneelle kutsujalle. Kutsujen ja niistä palaamisen pitää siis esiintyä yleensä aina pareittain! (Poikkeuksiakin on, liittyen esim. Javan säikeisiin eli rinnakkaiseen kontrolliin, ja tapahtumien käsittelyyn, mutta näitä tilanteita ei käsitellä tässä ohjeessa.)
Helpoiten saat kutsut ja paluut pidettyä järjestyksessä, kun pidät kirjaa siitä, kuka on kutsuja ja ketä kutsutaan. Usein tilanteisiin liittyy sisäkkäisiä kutsuja, joissa oliot kutsuvat toisiaan "ristiin". Nämä ovat eri asia kuin kaksi peräkkäistä kutsua, ja näissä tulee myös useimmin tulkintavirheitä.
Tähänkin toimintakuvaukseen voi käyttää erilaisia menetelmiä. UML-menetelmässä käytettävät sekvenssikaaviot ovat havainnollinen tapa kuvata olioiden keskinäistä "juttelua" ja ajonaikaisia suhteita. Vaikka et käyttäisikään juuri näitä merkintätapoja, pitää ohjelman toiminnasta pystyä kertomaan nämä samat asiat jollain muulla tavoin! Seuraavat ohjeet pätevät siis kaikkeen toimintojen kuvaukseen.
Seuraavan yksinkertaisen koodinpätkän sekvenssikaavio on kuvassa 9:
public class Eka { private Toka tokaolio; public int teeJotain() { tokaolio.teeMuuta(kolmasolio); return 1; } } public class Toka { public void teeMuuta(Kolmas kol) { kol.teeVielaJotain(); } }

Kuva 9. Sekvenssikaavio
Yhden sekvenssin osapuolina ovat "tilanteen" selvittämiseen osallistuvat luokat. Jos tilanne on monimutkainen, se kannattaa jakaa osiin, eli rakentaa ensin yleiskuvaus, jossa viitataan eri osatoimintoihin ja tarkentaa sitä sitten asteittain. Nyrkkisääntönä voisi olla, että jos sekvenssissä esiintyy jokin epämääräinen maininta, joka tuntuu sisältävän paljon "toimintaa" (vaatii todennäköisesti aina juttelua muiden olioiden kanssa) esim. "tarkista tilanne" tai "laske seuraava vaihe", se pitää tarkentaa. Toimintaa ei siis saa piilottaa tällaiseen "mustaan laatikkoon", josta tulos maagisesti putkahtaa esiin, vaan se on esitettävä eri kuvauksessa tarkemmin. Jos näitä tilanteita ei kuvaa riittävän tarkasti, tulee koodauksessa vastaan ongelma: on luotettu siihen, että tieto ilmestyy tyhjästä, ja koodissa pitäisikin saada tieto jotenkin tuotettua.
Sekvenssikaavioita pitäisi olla useampia, kaikista realistisista tilanteista ja siten, että ne kattavat ohjelman toiminnot. Yksi sekvenssi sisältää yhden mahdollisen tilanteen, esim. yhden käyttäjän tekemän toimenpiteen ja siihen reagoinnin. Tarkoitus ei ole tehdä kaikenkattavia kuvauksia kaikista mahdollisista erilaisista poluista ohjelman suorituksen läpi, riittää kun eri tyyppisistä tilanteista on kustakin oma kuvauksensa.
Esimerkki 1: Jos mallinnettavana on vaikkapa shakkipeli, riittää että yhden esimerkkisiirron kulku kuvataan, ei tarvitse erikseen tehdä sekvenssejä erilaisista siirroista. Tässä olisi ehkä hyvä kuvata tilanne ensin yleisellä tasolla, ja sitten tarkentaa sekvenssi alisekvensseihin: osatoimintoja ovat esim. siirrettävän nappulan valinta, kohderuudun valinta, laillisuuden tarkistus, mahdollinen nappulan syönti, shakki- ja matti-tilanteiden tarkastus, tuloskirjanpidon päivittäminen. Lisäksi on tietysti vielä tarpeen kuvata muitakin ohjelman toimintoja, esimerkiksi pelin alustus, mahdollinen pelitilanteen talletus, tms.
Esimerkki 2: Mallinnetaan matematiikan/vieraan kielen tenttausohjelmaa. Tässä voitaisiin tehdä sekvenssi ainakin yhden kyselykierroksen kulusta (kysymysten arvonta/valinta, vastauksen kysyminen, oikeellisuuden tulkinta, oikean/väärän vastauksen rekisteröinti jne.). Jos kysymysten arvontaan liittyy esimerkiksi vaikeustason nosto tms. toiminnan muuttaminen ajon aikana, se lienee syytä kuvata omana alisekvenssinään. Jos ohjelmaan liittyy esim. väärin vastattujen kysymysten uudelleententtausmahdollisuus, myös sen järjestäminen pitää näyttää.
Oliko tehtävänanto sittenkin liian hutera? Voit hyvin palata välillä täydentämään tehtävänmäärittelyä! Ota ohjaajaan yhteyttä, jos työ tuntuu vaikealta. Oliosuunnittelu ei avaudu yhdessä silmänräpäyksessä!