Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: C++: Olio-ohjelmointi

Markus Meskanen [27.04.2013 16:21:11]

#

Aloittaessaan ohjelman tai minkä tahansa isomman projektin teko, jokaisen tulisi huolehtia ensimmäisenä suunnittelusta. "Hyvin suunniteltu on puoliksi tehty", Tämä pätee myös (ja etenkin) ohjelmoinnissa.

Oikeaoppisen olio-ohjelmoinnin avulla vaikeasta ohjelman rakenteesta saa helposti ymmärretävän luokkahierarkian, mutta väärin käytettynä yksinkertainenkin ohjelma muuttuu katastrofiksi. Tämän takia luokkahierarkia tulee suunnitella ja toteuttaa huolella, jotta ohjelman hallitseminen olisi helppoa tulevaisuudessakin.

Suunnittelu

Ohjelmasta ja kielestä riippumatta, ensimmäisen askeleen tulee aina olla suunnittelu. Usein luokkahierararkian hahmottelu paperille auttaa pääsemään alkuun.
Suunnittelun ensimmäinen vaihe on päättää, mitä olemme ohjelmoimassa? Onko ohjelmamme 3D mallinnus ohjelma, johon tarvitsemme erilaisia 3D objekteja? Teemmekö GUI pohjaista käyttöjärjestelmää, vai yksinkertaista 2D matopeliä? Otan esimerkiksi GUI pohjaisen käyttöjärjestelmän, kuten windows, joka sisältää mm. ikkunoita, nappuloita, liukupalkkeja (slider) ja tekstikenttiä.

Kun olemme varmoja siitä mitä teemme, mietimme mitä kaikkia luokkia tarvitsemme kyseisen ohjelman toteuttamiseen. Liian tarkka ei kannata olla, mutta yleinen käsitys luokkahierarkiasta olisi hyvä hahmotella.
GUI pohjaiseen käyttöjärjestelmään tarvitsemme ainakin seuraavia luokkia:

Tulevaisuudessa tulemme tarvitsemaan lisää luokkia, mutta tämä riittää alkuun. Kuten mainitsin, ei kannata ajatella aivoja puhki.

Nyt kun tiedämme mitä luokkia tarvitsemme, voimme ruveta suunnittelemaan luokkia. Helpointa on aloittaa määrittelemällä luokille niiden attribuutit:

Ikkuna (Form)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls) // kuten nappulat, tekstikentät, yms.
- otsikko/nimi (text)

Nappula (Button)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls) // kyllä, nappulallakin voi olla ohjaimia
- teksti/nimi (text)

Liukupalkki (Slider)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls)
- teksti/nimi (text)
- tämän hetkinen arvo (current) // esim. internet selaimessa likupalkki jolla voi selata sivua ylös/alas

Tekstikenttä (TextBox)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls)
- teksti (text)

Ensimmäinen asia mitä huomataan, on että kaikilla luokilla on viisiyhteistä attribuuttia: sijainti, koko, emo-ikkuna, ohjaimet, ja jonkin sortin teksti. Tästä saamme yhteisen perusluokan; Ohjain (Control).

Ohjain (Control)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls)
- teksti (text)

Ikkuna (Form) : periytyy luokasta Control

Nappula (Button) : Control

Liukupalkki (Slider) : Control
- tämän hetkinen arvo (current)

Tekstikenttä (TextBox) : Control

Vain yhdellä yhteisellä kantaluokalla saimme pienennettyä yksittäisen luokan koodia huomattavasti, ja koodista tulee paljon selvempää. Luokkahierarkia näyttäisi tällä hetkellä tältä:

        Control
    /    |    |    \
Form Button Slider Textbox

Uusi luokka

Saatamme myöhemmin huomata tarvitsemamme luokan, jota emme alunperin suunnitelleet. Jos luokkahierarkia on hyvin suunniteltu, uuden luokan lisäämisen tulisi olla vaivatonta ja täsmätä luokkahierarkiaan (lähes) täydellisesti.

Esimerkiksi: Haluamme mahdollisuuden jakaa ikkunan erillisiin osiin, jotta käyttämisestä tulisi helpompaa. Tätä varten tarvitsemme Paneeli (Panel) luokan, jonka sisään voimme laittaa ohjaimia. Paneeli luokka näyttäisi tältä:

Paneeli (Panel)
- sijainti (position)
- koko (size)
- emo-ikkuna (parent)
- ohjaimet (controls)
- otsikko/nimi

Tai luokkahierarkiaan sijoittamisen jälkeen tältä:

Paneeli (Panel) : Control

Paneeli ei välttämättä tarvitse text-attribuuttia, mutta se on hyvä olla paneelien luokitteluun, vaikkei se ohjelman käytäjälle esillä olisikaan. Ja on hyvin mahdollista, että saatamme tarvita paneeleja joissa näkyy teksti, joka kertoo esimerkiksi mitä paneeli sisältää.

Saatamme tarvita myös toisen uuden luokan, Etenemispalkki (ProgressBar), joka kertoo esimerkiksi kuinka paljon ohjelman asennuksesta on suoritettu. Etemispalkki näyttää tältä:

ProgressBar : Control
- tämän hetkinen arvo (current)

Huomaamme, että ProgressBar ja Slider sisältävät yhden yhteisen attribuutin Control attribuuttien lisäksi; current. Todellisuudessa ProgressBar voisi olla puhtaasti Slider eikä erillistä luokkaa tarvitsisi, mutta harjoituksen vuoksi kuvitellaan että se on pakollinen; tästä saamme uuden perusluokan, kutsutaan sitä vaikka SliderControl nimellä.

SliderControl : Control
- current

Slider : SliderControl

ProgressBar : SliderControl

Ja luokkahierarkia näyttäisi tältä:

          Control
     /    |    |    \
 Form Button TextBox SliderControl
                       /      \
                    Slider  ProgressBar

Koodin kirjoittaminen

Jos luokkahierarkia on hyvin suunniteltu, on lähdekoodin kirjoittaminen helppoa kuin nakki. Ohjelmoinnin vaikein osuus onkin suunnittelu, sillä kuka tahansa osaa kirjoittaa, mutta suunnittelemaan oppiminen vaatii paljon kokemusta. Kun hierarkia ja luokat on selvillä, voimme periaatteessa aloittaa ohjelmoinnin mistä tahansa luokasta, mutta on yleisesti suositeltava aloittaa ylimmästä kantaluokasta, tässä tapauksessa Control luokasta.

class Control {
    public:
        /* erilaisia metodeja, kuten esimerkiksi:
         * void draw()
         * getterit ja setterit
         * luokka voisi olla myös abstrakti kantaluokka ja esim. draw() abstrakti metodi.
         */
    private:
        Point m_position;
        Size m_size;
        Control* m_parent;
};

Huomatkaa, että oletan Point ja Size rakenteiden olemassaolon todeksi, mutta niiden ohjelmointi ei ole kovin vaikeaa. Jätän myös getterit ja setterit sekä muut luokan metodit pois selvyyden vuoksi, mutte todellisuudessa nekin tarvitaan.

Suunnittelusta näyttäisi siltä, että kaikki luokat ovat samanlaisia ja vain nimi poikkeaa toisista luokista. Suunnittelu on kuitenkin usein vain pintapuolinen, ja todellisuudessa esim. Ikkunalla tulisi olla ikoni, maksimointi ja minimointi nappulat, yms. Toisaalta suunnitelmaan tulisi lisätä uusia attribuutteja samaa tahtia kun niitä todelliseen ohjelmaan lisää.
Myös luokkien metodit ja eventit poikkeavat toisistaan, joten luokat eivät ole identtisiä.

Onko luokka, vai omistaako luokka?

Yleisin virhe on yrittää periyttää kaikki kaikesta.
Kuvitellaan Velho-luokka ja PimeäVelho-luokka. PimeäVelho periytetään Velho:sta ja lisätään sille pimeät voimat:

class Velho {
    public:
        virtual void karkki();
    private:
        Voima m_parantamisVoima;
};

class PimeaVelho : Velho {
    public:
        virtual void kepponen();
    private:
        Voima m_haavoittamisVoima;
};

Nyt jos myöhemmin haluamme Jedi ja PimeäJedi -luokat, olemme lirissä. Kaksi luokkaa tarvitsevat pimeitä voimia (m_haavoittamisVoima, kepponen), jolloin tulisi olla yhteinen kantaluokka. Käytetäänkö moniperintää?

class Velho {
    public:
        virtual void karkki();
    private:
        Voima m_parantamisVoima;
};

class Jedi {
    private:
        Ase m_valomiekka;
};

// yhteinen kantaluokka
class Pimea {
    public:
        virtual void kepponen();
    protected:
        Voima m_haavoittamisVoima;
};

class PimeaVelho : Velho, Pimea {
};

class PimeaJedi : Jedi, Pimea {
};

Väärin. Moniperintä ei ole koskaan oikea valinta (lukuunottamatta harvat poikkeukset, esim. interfaceja korvatessa).
Luokkia suunnitellessa tulee kysyä itseltään joka luokan kohdalla: "Onko luokka, vai omistaako luokka?".
Tässä tapauksessa PimeäJedi on Jedi, joten se periytyy Jedistä.
PimeäJedi EI ole pimeä voima, joten se ei periydy pimeästä voimasta, vaan omistaa pimeän voiman (esim. m_pimeaVoima).
Samoin Velhojen puolella, PimeäVelho ON Velho joten se periytyy, mutta PimeäVelho ei ole pimeä voima, vaan omistaa pimeän voiman, joten se ei periydy vaan sille tulee uusi attribuutti m_pimeaVoima.

----

Toinen esimerkki samasta ongelmasta: Yritämme tehdä yksinkertaista suorakulmio luokkaa. Suorakulmiolla on paikka (Point:x,y) ja koko (Size:w,h), jotka saattaisivat näyttää tältä:

class Point {
    private:
        int m_x;
        int m_y;
};

class Size {
    private:
        int m_width;
        int m_height;
};

Tässä vaiheessa usein tulee ajatelleeksi: "Suorakulmiollahan on myös x, y, width ja height attribuutit, se tulee siis moniperiyttää Size ja Point luokista!". Taas väärin.

Kysytään itseltämme: Onko suorakulmio paikka ja koko, vai omistaako suorakulmio paikan ja koon? Vastaus jääköön jokaisen pääteltäväksi, luokka näyttäisi tältä:

class Rectangle {
    private:
        Size m_size;
        Point m_location;
};

Muista aina verrata luokkia toisiinsa ja luoda yhteinen kantaluokka, jos yhteisiä attribuutteja löytyy. Muista myös periyttää vain, jos aliluokka on kantaluokka!

Metabolix [30.04.2013 11:50:59]

#

Pysy tyylilajissa. Asiateksti on tapana kirjoittaa neutraalisti passiivia tai kolmatta persoonaa käyttäen. Turhat täyteilmaukset ("kysytään itseltämme") eivät ole tyylikkäitä, ja muutenkin tekstissä on liiaksi esseistisiä, asiatekstiin kuulumattomia piirteitä kuten yleistyksiä, filosofointia ja retoriikkaa. Jos tarkoitus olikin kirjoittaa essee, voisit ottaa kaikki koodit ja liian tekniset selitykset pois ja julkaista tekstin blogissa.

Kiinnitä huomiota kieliasuun, virheitä on valtavasti. Isoja kirjaimia ei ole hyvä käyttää tyylikeinona, vaan tärkeät asiat voi tuoda esiin myös sanallisesti. Suomen kielessä tavallinen preesens ilmaisee myös futuuria eikä ole tapana käyttää väkinäisiä futuurirakenteita ("tullaan tekemään"). Epäsuoran kysymyslauseen eli kysyvän sivulauseen perään ei tule kysymysmerkkiä. Lauseenvastikkeita ei eroteta päälauseesta pilkulla. Sen sijaan kaikki alisteiset sivulauseet erotetaan päälauseistaan pilkulla. Sana "vaan" ei ole alistuskonjunktio vaan rinnastuskonjunktio, joten sen eteen ei automaattisesti kuulu pilkkua, ellei pilkulle ole muuta syytä. Tarkista myös yhdyssanat: esimerkiksi "2D-peli", "GUI-pohjainen", "parantamisvoima" ja "suorakulmioluokka" ovat yhdyssanoja; sen sijaan "lukuun ottamatta" ei ole yhdyssana. Tässä eivät suinkaan ole edes kaikki löytämäni virheet. Teknisenä seikkana mainittakoon ontuva rivinvaihdon käyttö ja kappalejako.

Asiasisällöltään vinkki on kohtuullinen. (Samoja asioita kyllä sivutaan myös C++-opassarjan seuraavassa osassa, kunhan saan sen julkaistua.) Mielestäni esität liikaa asioita faktoina ilman perusteluja ja sidot tekstisi liian tiukasti omiin esimerkkeihisi etkä selitä periaatteita riittävästi yleisellä tasolla. Vaikutelma saattaa toki osittain johtua myös tekstin tyyli- ja kieliongelmista. Kuitenkin esimerkiksi moniperinnän tuomitseminen jää aivan irralliseksi, kun et edes selitä, mitä "interfacet" eli rajapinnat ovat ja miten niitä voisi käyttää.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta