class Luokka { private: int foo; public: void SetFoo(int bar) { foo = bar; } int GetFoo() { return foo; } };
Aika usein näkee vastaavanlaista koodia. En keksi tälle mitään hyötyä, ellei haluta rajoittaa esim. pelkkää luku- tai kirjoitusoikeutta. Jos setterissä/getterissä ei tehdä mitään muuta kuin asetetaan/luetaan arvo, miksi ihmeessä se pitäisi tehdä funktiokutsujen kautta, miksei pelkkä public muuttuja kelpaa?
Lisäys: Näin aamun pikkutunteina ei tullut mieleenikään googletella asiaa. Löytyi kun löytyikin vallan hyvä threadi stack overflowsta
Kuten löytämälläsi sivulla selitetään, tästä on ehkä hyötyä jossain mutkikkaammassa tapauksessa. Kuitenkin jos on tarkoitus tehdä yksinkertainen luokka Piste, jonka ainoat muuttujat ovat x ja y ja joka ei varmasti tarvitse lisätarkistuksia, voi ihan hyvin käyttää julkisia muuttujia.
Esimerkiksi C# ja eräät Pascalin murteet ovat tässä askeleen edellä, kun voi määritellä ominaisuuksia, jotka näyttävät muuttujilta mutta käyttävätkin automaattisesti set- ja get-funktioita.
Joidenkin mielestä oliossa ei saa olla koskaan julkisia muuttujia, vaan muuttujia täytyy käsitellä aina settereiden ja gettereiden kautta. Kyseessä on samanlainen asia kuin goto-komennon ehdoton käyttökielto.
Toisaalta voihan käydä niin, että tällä hetkellä ei ole tarvetta rajoittaa muuttujan muuttamista mutta myöhemmin on.
Joidenkin toisten mielestä taas olioiden ominaisuuksien pitäisi olla propertyjä (muuttujia) ja toimintojen taas metodeja. Näin silloin, kun käytetty kieli tukee "accessoreita" eli piilotettuja get/set-metodeja. Eli kun yritetään sijoittaa jotain olion muuttujaan, niin sijoitustapahtuma kulkeekin jonkin funktion läpi.
C:ssä ja C++:ssa julkisten muuttujien käyttökieltoa voi ymmärtääkseni perustella myös binary compatibilityllä. Julkisia jäsenmuuttujia ei voi poistaa eikä nimetä uudelleen rikkomatta yhteensopivuutta. Gettereiden ja settereiden toteutuksen voi sen sijaan tehdä uusiksi tietyin ehdoin.
The Alchemist kirjoitti:
Joidenkin toisten mielestä taas olioiden ominaisuuksien pitäisi olla propertyjä (muuttujia)
Mielestäni hämärästi sanottu.
Ominaisuus=property, variable=muuttuja.
Sopan saamiseksi riittävän paksuksi ja sekavaksi, kerrottakoon että tosi OO-gurut eivät käytä setter/getter-kamaa lainkaan (tai ainakin hyvin minimaalisesti).
Tuossapa eräs selvitys, joka löytyi tietysti haulla 'getters and setters are evil'.:
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
Surkuhupaisaa on tietysti että lähes poikkeuksetta OO:n opetuksessa get/set-metodit työnnetään opiskelijaparan kurkusta alas kuin Turussa viinat. Ei olekaan ihme että OO:ia opiskelevat tuntuvat aina sössöttävän jotain sekavaa.
Oikeastaan tuossa on puhe lähinnä olion datojen tarjoamisesta ulkomaailman ihmeteltäväksi enemmänkin kuin siitä onko get/set sinänsä paha.
Get- ja set-funktioita käytetään, jotta olion sisäinen tila pysyy aina ehjänä ja kelvollisena. Tämä luokkainvariantin säilyttäminen on yksi olio-ohjelmoinnin kulmakivi, ja se auttaa tekemään toimivaa, uudelleenkäytettävää ja ylläpidettävää koodia. Joissakin kielissä getterit ja setterit voidaan toteuttaa propertyinä, jolloin syntaksi on sama kuin jäsenmuuttujaa suoraan käsiteltäessä, mutta asia on silti sama.
Pari esimerkkiä:
class piste { public: int x, y; };
Tämä tarkoittaa, että meillä on piste, jolla on x- ja y-koordinaatit. Koordinaattien arvot voivat olla mitä tahansa, ja kuka tahansa voi tehdä koordinaateilla mitä tahansa. Jos koordinaattien pitäisikin olla vaikkapa tyyppiä float, mistään gettereistä tai settereistä ei ole apua, vaan muutoksia tarvitaan luokan sisälle, luokan rajapintaan ja kaikkialle, missä luokkaa käytetään.
class piste { int u, v; public: piste(int x, int y) : u(x), v(y) {} piste &x(int a) { u = a; return *this; } piste &y(int a) { v = a; return *this; } int x() const { return u; } int y() const { return v; } };
Tämä voisi olla pisteen tavallinen toteutus. Gettereiden ja settereiden hyöty näyttää vieläkin kyseenalaiselta. Etu on selvä hiukankaan monimutkaisemmissa tapauksissa:
class piste { int u, v; public: piste(int x, int y) : u(x), v(y) { if (u < -X || X < u) throw out_of_range("piste.x"); if (v < -Y || Y < v) throw out_of_range("piste.y"); } int x() const { return u; } int y() const { return v; } };
Nyt pisteitä voikin olla vain tietyllä alueella. Settereitä ei ole, eli pistettä ei voi siirtää. Getterit on, joten piste tarjoaa riittävän rajapinnan muiden operaatioiden toteuttamista varten. Kaiken ei suinkaan kannata olla toteutettuna itse luokassa.
Gettereitä ja settereitä tarvitaan usein myös, jos luokan jäsenet ovat jotain muuta kuin perustietotyyppejä. Eikä pelkästään C++:ssa.
Luokkainvariantti on siis perussyy. Muita syitä on myös. Osa niistä on tästä johdettavia, osa kuviteltuja ja osa johtuu esimerkiksi ohjelmointiympäristöstä.
Yritetään vähän selittää.
Getterit ja setterit ovat kaikkein alimman tason metodeja. OO:ssa tulisi aina pyrkiä korkeamman tason toimintoihin.
Yliyksinkertainen esimerkki: jos meillä on 2D-peli, jossa otus-olio seikkailee, niin kumpi on parempi:
otus.setX(22); otus.setY(77); // vai otus.moveTo(22, 77);
Oho, jälkimmäinen näyttää selkeämmältä ja settereitä ei tarvita! Ja jos Otus-luokka on (yksinkertaistettuna) jotain näin:
class Otus { private: int x,y; public: void moveTo(int, int); // jne... };
Niin oho, kapselointi toteutuu myös.
Jo mainittu luokkainvariantin vaatimus toteutetaan tietysti moveTo-metodissa. Se voisi olla vaikka ettei Otus koskaan pääse pelialueen rajojen yli tms. (Tai olisi vaikka Pelikentta-luokka, joka tarkistaa pelialueella pysymisen tai seiniin törmäämisen tai jotain...)
(Luokkainvariantti tarkoittaa ehtoja, joita olion pitää aina noudattaa. "Ei saa koskaan mennä pelialueen ulkopuolelle" vaikkapa.)
Myös sellainen pelle kuin C++:n kehittäjä Bjarne Stroustrup selittää että ei niitä settereitä ja gettereitä:
http://www.artima.com/intv/goldilocks3.html
Ps. Ylläolevat koodit pyrkivät olemaan selventäviä esimerkkejä. Oikeasti x ja y tilalle tulisi varmaan Paikka-luokka jne.
Voihan tuo sinun moveto()-funktio käyttää settereitä sisällään. Voi olla että C++ ei ole luotu olio-ohjelmointiin, joten perinteiset proseduraaliset tyylit toimii kätevämmin.
Pascal:n kanssa jota itse käytän, saa käyttää olioita koko ajan. Propertyitä käytettäessä ei koodarin aina tarvitse tietää onko sillä getteriä ja setteriä edes olemassa.
TPiste = class private FX, FY: single; public property X: single read FX write FX; property Y: single read FY write FY; // Tai... private FX, FY: single; function GetX(): single; procedure SetX(value: single); public property X: single read GetX write SetX; end;
Näin siis jälkimmäisellä tavalla piste.X:=12.3; kutsuisi setteriä sisällään, tai ensimmäisellä tavalla käyttää suoraan muuttujaa.
JaskaP kirjoitti:
OO:ssa tulisi aina pyrkiä korkeamman tason toimintoihin.
Niinhän kaikessa pitäisi pyrkiä. Gettereiden ja settereiden ongelma ei ole "taso", vaan Stroustrupinkin mukaan se, että niitä käytetään silloinkin kun ei olisi tarpeen tai kun kyse ei oikeastaan edes ole olioista.
JaskaP kirjoitti:
Ylläolevat koodit pyrkivät olemaan selventäviä esimerkkejä. Oikeasti x ja y tilalle tulisi varmaan Paikka-luokka jne.
... jolloin homma palaakin nätisti get- ja set-tapauksiin:
class Otus { private: piste a; public: piste paikka() const { return a; } Otus &paikka(piste p) { a = p; return *this; } // mitä eroa? void moveTo(piste p) { a = p; } // jne... };
On toki tärkeää, että funktioiden nimet ovat tarkoituksenmukaisia. On järkevää, että luokka itsessään tarjoaa vain aika minimaalisen rajapinnan, mikä ei tarkoita sitä, että kaikki operaatiot ovat gettejä ja settejä. Monimutkaisemmat toiminnot on hyvä toteuttaa luokan ulkopuolella. Osa niistä voi olla tarjolla jo silloin kuin luokkakin, mutta aina kaikkia tarvittavia toimintoja ei voi tietää etukäteen.
User137 kirjoitti:
Propertyitä käytettäessä ei koodarin aina tarvitse tietää onko sillä getteriä ja setteriä edes olemassa.
Sehän se propertyjen tarkoitus onkin. Mutta jos propertyjä aikoo käyttää luokkainvariantin varjelemiseen, niin tuossa Pascalissa on pakko käyttää ainakin settereitä pinnan alla.
Jouko Koski kirjoitti:
Niinhän kaikessa pitäisi pyrkiä. Gettereiden ja settereiden ongelma ei ole "taso", vaan Stroustrupinkin mukaan se, että niitä käytetään silloinkin kun ei olisi tarpeen tai kun kyse ei oikeastaan edes ole olioista.
Siis vältä getter/setter-metodeja aina kun mahdollista ja koodin suuri get/set-määrä vihjaa huonosta suunnittelusta eli olemme samaa mieltä, (kai?)
lainaus:
... jolloin homma palaakin nätisti get- ja set-tapauksiin:
Ei palaa, jos Piste/Paikka on struct-tyyppinen eli POD (Plain Old Data) kuten myös Stroustrup haastattelussa selittää, jolloin luokkainvarianttia ei tietenkään pidä tarkistaa Piste-luokassa/tietueessa, vaan jossain muualla. Sorry, kirjoitin epäselvästi.
Nähtävästi Otus-esimerkki oli huonosti yksinkertaistettu ja tietenkin oikeasti 2D-pelin hahmojen luokat pitäisi tehdä fiksummin eli vielä korkeammalla abstraktiotasolla.
JaskaP kirjoitti:
Siis vältä getter/setter-metodeja aina kun mahdollista ja koodin suuri get/set-määrä vihjaa huonosta suunnittelusta eli olemme samaa mieltä, (kai?)
Ei. Gettereitä ja settereitä pitää käyttää silloin, kun ne ovat tarpeen; muulloin ei. Niiden määrä ei sellaisenaan kerro paljon mistään.
Huonosta suunnittelusta vihjaa pikemminkin se, jos luokassa itsessään on toteutettu kovin erityyppisiä operaatioita. On kyseenalaista, jos esimerkiksi Otuksessa on funktioina AsetaPaikka, Piirrä ja TalletaTietokantaan. Ensimmäinen näistä voisi luontevasti kuulua Otuksen vastuulle. Jälkimmäiset edellyttävät tietoa muusta maailmasta (mikä piirtokonteksti, mikä tietokanta), joten sellaiset toiminnot olisi järkevää jättää erillisten apuluokkien tai -funktioiden vastuulle.
JaskaP kirjoitti:
lainaus:
... jolloin homma palaakin nätisti get- ja set-tapauksiin:
Ei palaa, jos Piste/Paikka on struct-tyyppinen... jolloin luokkainvarianttia ei tietenkään pidä tarkistaa Piste-luokassa/tietueessa.
Mikä seuraavassa on lopulta paikka- ja moveTo-funktioiden välinen ero?
class Otus { private: piste a; public: piste paikka() const { return a; } Otus &paikka(piste p) { throw_jos_ei_kelpaa(p); a = p; return *this; } void moveTo(piste p) { throw_jos_ei_kelpaa(p); a = p; } // jne... };
Korkea abstraktiotasokin on hyvä perustaa järkevälle vastuiden jakamiselle eri komponenteille. Jokaisen komponentin perusvastuu on pysyä kelvollisessa tilassa (luokkainvariantti). Getterit ja setterit ovat osa tätä kuviota.
Jouko Koski kirjoitti:
Mikä seuraavassa on lopulta paikka- ja moveTo-funktioiden välinen ero?
Se että toinen on selkeä ja toinen on c++-hifistelyä.
Tuonkaltainen hifistely on perusteltua kun tehdään kirjastoa, jolla on potentiaalisesti miljoona käyttäjää. Kun tehdään lopputuotetta (esimerkissä 2D-peliä) niin hifistely on sairasta, turhaa ja ajantuhlausta.
Sanooko KISS-periaate mitään?
Otuksen paikka-setteristä voi ihan hyvin jättää pois otusviittauksen palautuksen. Se ei kuitenkaan ollut kysymyksen ydin vaan se, että korkeamman tason moveTo-funktio onkin ihan sama asia kuin alimman tason setteri. Tämä on ymmärrettävää, sillä molemmat vaikuttavat vain olion omaan tilaan.
Ja näin perusoliot on järkevää tehdäkin. Oliot huolehtivat tilansa kelvollisuudesta ja antavat rajapinnan tuon tilan käsittelyyn. Kukin luokka mallintaa mieluusti vain yhdenlaista asiaa. Luokkien ympärille tehdään tarpeen mukaan apuluokkia ja -funktioita kytköksiksi, vuorovaikutuksia välittämään.
Abstraktiotason nostaminen on vähän epämääräinen käsite. Käytännössä se voi tarkoittaa, että tehdään uudelleenkäytettäviä rajapintoja abstraktien kantaluokkien ja periyttämisen avulla. Periyttämistä ei kuitenkaan enää suosita niin paljon kuin muinoin, sillä se on (C++:ssa toiseksi) vahvin kytkös asioiden välillä ja vahvoista kytköksistä on opittu koituvan ongelmia.
Koko oliomallinnusta voi pitää jonkinlaisena hifistelynä. Ilmankin sitä kyllä pärjää. Olioajattelu on hyvä lähestymistapa, kun tehdään jotakin mitä myös ylläpidetään, muokataan ja uudelleenkäytetään.
Eiköhän se olio tehdä juuri sellaiseksi kuin arkkitehtuuriin on suunniteltu. Oli sitten gettereiä, settereitä,interfaceja, extension metodeja luokille tai mitä milloinkin. Varmasti jo kysyjälle getit ja setit selkiytyi. On myös turha jauhaa oliota käsittelevien metodien sijainnista ja käyttötavasta, koska niillekkään ei universaalia sääntöä löydy. On best practiset ja patternit joita seurataan, kunnes niillekkin parempi korvaaja aikanaan saapuu.eikä aina sopivaa patternia tai best practisea edes ole.
Aihe on jo aika vanha, joten et voi enää vastata siihen.