Luokka ja sen metodit määritellään Rubylla seuraavasti:
class Henkilo
def tervehdi
puts "Hello world"
end
def laske(x, y)
puts "#{x} + #{y} on #{x+y}"
end
end
Luokasta luodaan instansseja metodilla new
, ja instanssien metodeja kutsutaan normaaliin tapaan pistenotaatiolla:
h = Henkilo.new
h.tervehdi
h.laske(10, 5)
Muutetaan luokkaa lisäämällä sille konstruktori (eli metodi initialize
), ja iän sekä nimen kertovat oliomuuttujat:
class Henkilo
def initialize(nimi)
@ika = 0
@nimi = nimi
end
def vanhene
@ika += 1
end
def tervehdi
puts "olen #{@nimi} ikäni on #{@ika}"
end
end
Oliomuuttujat siis täytyy nimetä alkaen merkillä @
. Kaikki muut muuttujat luokan sisällä tulkitaan metodien paikallisiksi muuttujiksi (@@
:lla alkavat muuttujat ovat luokkamuuttujia).
Huom: Railsissa
ActiveRecord
:in perivät, tietokantatauluja vastaavat model-luokat poikkeavat tästä, niissä tietokantataulun sarakkeita vastaavien oliomuuttujien nimissä ei ole merkkiä@
. Teknisesti ottaen ne eivät olekaan oliomuuttujia.
Olioille voidaan määritellä merkkijonomuoto metodilla to_s
:
class Henkilo
# ...
def to_s
"#{@nimi} ikä #{@ika} vuotta"
end
end
h = Henkilo.new "Chang"
puts h
Huomaa, että Rubyssä konventiona on kirjoittaa metodikutsujen ja konstruktorien parametrit ilman sulkuja, erityisesti jos parametreja on vain yksi.
Luokkametodit määritellään seuraavasti:
class Henkilo
# ...
def self.luokkametodi
# ...
end
end
# nyt voidaan kutsua
Henkilo.luokkametodi
# mutta seuraava ei toimi
h = Henkilo.new "Chang"
h.luokkametodi
Oliomuuttujien getterit ja setterit saadaan tarvittaessa generoitua automaattisesti:
class Henkilo
attr_reader :nimi
attr_accessor :ika, :osoite
def initialize(nimi, osoite)
@ika = 0
@nimi = nimi
@osoite = osoite
end
def vanhene
@ika += 1
end
def to_s
"#{@nimi} ikä #{@ika} vuotta, osoite #{@osoite}"
end
end
attr_accessor
määrittelee oliomuuttujille setterin ja getterin,
attr_reader
pelkän getterin. Nyt oliomuuttujiin on mahdollista viitata pistenotaatiolla:
2.2.1 :020 > h = Henkilo.new "chang", "alppila"
=> #<Henkilo:0x007f8724189e28 @ika=0, @nimi="chang", @osoite="alppila">
2.2.1 :021 > h.nimi
=> "chang"
2.2.1 :022 > h.ika
=> 0
2.2.1 :023 > h.ika += 10
=> 10
2.2.1 :024 > puts h
chang ikä 10 vuotta, osoite alppila
=> nil
2.2.1 :025 > h.nimi = "arto"
NoMethodError: undefined method `nimi=' for #<Henkilo:0x007f8724189e28>
from (irb):25
from /Users/mluukkai/.rvm/rubies/ruby-2.2.1/bin/irb:11:in `<main>'
2.2.1 :026 >
Oliomuuttujaa @ika
vastaava getteri on siis metodi ika
, ja setteri metodi ika=(param)
. Jos getterit ja setterit on määritelty, voidaan niitä käyttää myös olion sisällä:
class Henkilo
# ...
def vanhene
ika = @ika + 1
end
def to_s
"#{nimi} ikä #{ika} vuotta, osoite #{osoite}"
end
end
Sijoitusmerkin oikealla puolella setteriä ei kuitenkaan voi käyttää, eli seuraava ei olisi toiminut:
class Henkilo
# ...
def vanhene
ika = ika + 1
end
end
Koodissa Ruby tulkitsisi että metodissa viitataan ika
-nimiseen paikalliseen muuttujaan. Jos getteriä haluttaisiin käyttää, onnistuisi se viittaamalla getteriin Rubyn this
:in, eli olioon itseensä viittaavan self
-viitteen avulla:
class Henkilo
# ...
def vanhene
ika = self.ika + 1
end
end
Rubyssä konventiona on, että luokan nimet kirjoitetaan CamelCasella. Metodit ja muuttujannimet taas snake_casella.
Railsissa jokainen luokka on tapana määritellä omaan tiedostoonsa. Tiedostojen nimet kirjoitetaan snakecasella, eli luokka Henkilo
määriteltäisiin tiedostossa henkilo.rb ja esim. luokka PersonController
tiedostossa person_controller.rb
Oletetaan että olemme määritelleet luokan Henkilo
tiedostossa henkilo.rb
# tiedosto henkilo.rb
class Henkilo
# ...
end
Jos haluamme käyttää luokkaa jostain muusta tiedostosta, on se sisällytettävä toiseen tiedostoon komennolla require
:
# tiedosto koodi.rb
require './henkilo.rb'
h = Henkilo.new 'chang', 'alppila'
Olioihin liittyvien tehtävien virheilmoituksista ei välttämättä selviä miten testattava olio on luotu ja mitä metodeja sille on kutsuttu ennen testattavaa asiaa. Kannattaakin tarkastaa asia testit sisältävästä tiedostosta testi_spec.rb
Tee luokka
Pelaaja
(tiedostoon pelaaja.rb). Pelaajalle asetetaan konstruktorissa nimi ja pituus. Pelaajalla on myös maalimäärä. Pelaajalla on seuraavat metodit
maaleja, palauttaa maalien määrän
lisaa_maali, lisää yhden maalin
getteri ja setteri pituudelle
getteri nimelle
to_s, palauttaa merkkijonoesityksen, joka on muotoa
Arto (179 cm) maaleja 4
Kopioi edellisessä tehtävässä tekemäsi tiedosto pelaaja.rb tämän tehtävän hakemistoon. Tee luokka
Joukkue
(tiedostoon joukkue.rb). Joukkue sisältää joukon pelaajia. Joukkueelle asetetaan nimi konstruktorissa. Joukkueella on seuraavat metodit:
- lisaa_pelaaja(pelaaja) lisää joukkueeseen parametrina annetun pelaajan
- maaleja_yhteensä, palauttaa joukkueen pelaajien yhteenlasketun maalimäärän
- paras_maalintekija, palauttaa joukkueen eniten maaleja tehneen pelaajan
Perintä tapahtuu seuraavasti:
class Opiskelija < Henkilo
def initialize(params)
@nimi = params[:nimi]
@opiskelijanumero = params[:opnro]
@ika = params[:ika] || 0
@opintopisteet = params[:op] || 0
end
def opiskele
@opintopisteet += 1
end
def to_s
"#{nimi} (#{@opiskelijanumero}) #{@opintopisteet} op"
end
end
Toisin kuin esim. Javassa, myös konstruktori perityy. Määrittelimme kuitenkin luokalle oman konstruktorin. Konstruktori saa vain yhden parametrin. Parametri on nyt hash
, jonka avulla luotava olio konfiguroidaan:
2.2.1 :084 > c = Opiskelija.new nimi:"Chang", opnro:"12345"
2.2.1 :085 > c.to_s
=> "Chang (12345) 0 op"
2.2.1 :086 > a = Opiskelija.new nimi:"Arto", opnro:"01111", op:450, ika:31
2.2.1 :087 > a.to_s
=> "Arto (01111) 450 op"
Olion luonnissa käytetty syntaksi on mielenkiintoinen:
Opiskelija.new nimi:"Chang", opnro:"12345"
Normaalistihan hash määritellään aaltosulkeissa { nimi:"Chang", opnro:"12345" }
, metodikutsun viimeisenä parametrina oleva hash voidaan kuitenkin määritellä ilman aaltosulkeita ja näin Ruby-konvention mukaan lähes aina tehdäänkin.
Edellä olleesta esimerkistä huomasimme, että iän ja opintopisteiden määritteleminen olion luonnin yhteydessä oli vapaaehtoisa. Tämän takia konstruktorissa käytettiin operaattoria ||
:
def initialize(params)
# ...
@ika = params[:ika] || 0
@opintopisteet = params[:op] || 0
end
Operaattori toimii siten, että lausekkeen params[:op] || 0
arvona on sen vasemman puolen arvo jos se on määritelty (ja muuta kuin false
). Jos vasen puoli ei ole määritelty, lausekkeen arvoksi tulee oikean puolen arvo eli 0. Operaattorin ||
käyttö vastaavissa tapauksissa on hyvin yleistä Rubyssä.
Tehtävän projektissa on valmiina luokka
Piste
. Toteuta Pisteen perivät luokatVaripiste
jaPiste3d
(tiedostoihin varipiste.rb ja piste3d.rb). Hyödynnä luokissa mahdollisimman paljon yläluokasta perittävää koodia. Huomaa, että yliluokan metodia tai konstruktoria kutsutaan viitteensuper
avulla. Luokkien haluttu toiminnallisuus selviää testeistä.
Perintä ei ole ainoa tapa liittää luokkaan muualla määriteltyä toiminnallisuutta. Jos sama toiminnallisuus pitää liittää useampaan, muuten toisistaan riippumattomiin luokkiin, kannattaa käyttää moduuleja.
Moduuli määritellään seuraavasti:
module Kasvattaja
def lisaa_ikaan(vuotta)
@ika += vuotta
end
end
Näin määritelty moduuli on itsessään käyttökelvoton, mutta moduulin määrittelemä toiminnallisuus voidaan sisällyttää luokkaan:
class Henkilo
include Kasvattaja
# ...
end
Sisällytetty metodi toimii täsmälleen kuten luokan omat oliometodit:
2.2.1 :119 > h = Henkilo.new "Arto", "Espoo"
=> #<Henkilo:0x007f93ba0934b8 @ika=0, @nimi="Arto", @osoite="Espoo">
2.2.1 :120 > h.lisaa_ikaan 20
=> 20
2.2.1 :121 > h.to_s
=> "Arto ikä 20 vuotta, osoite Espoo"
2.2.1 :122 > h.lisaa_ikaan 14
=> 34
2.2.1 :123 > h.to_s
=> "Arto ikä 34 vuotta, osoite Espoo"
2.2.1 :124 >
Moduulin määrittelemää toiminnallisuutta voidaan hyödyntää muissakin luokissa olettaen että, luokissa on määriteltynä oliometodi @ika
:
class Elain
include Kasvattaja
# ...
end
class Auto
include Kasvattaja
# ...
end
Tee moduuli
Siirrettava
, joka olettaa että luokassa, johon moduuli liitetään on, sen sijainnin kordinaatit kertovat oliomuuttujat@x
ja@y
. Moduuli määrittelee metodit:
- siirraPisteeseen(x, y) joka muuttaa sijainin parametreina olevaan pisteeseen
- siirraSuuntaan(dx, dy) joka muuttaa sijainnin siten, että se lisää parametrina olevat arvot vanhaan sijaintiin
Moduuleja voidaan käyttää myös muodostamaan Javan pakkausrakennetta vastaavia nimiavaruuksia.
module Yliopisto
class Henkilo
# ...
end
class Laitos
# ...
end
class Kurssi
# ...
end
end
Moduulin sisällä määriteltyihin luokkiin viitataan seuraavasti:
tktl = Yliopisto::Laitos.new nimi:"TKTL"
Saman moduulin sisällä olevassa koodissa luokkien nimiin voi viitata myös suoraan:
module Yliopisto
class Laitos
def perusta_kurssi(nimi)
kurssit[nimi] = Kurssi.new nimi
# ...
end
class Kurssi
# ...
end
end
Seuraavassa vielä muutama hieman haastavampi, omatoimista tiedonhakua edellyttävä tehtävä.
Tee moduuli
Debugattava
joka määrittelee metodintila
. Metodi tulostaa olion jokaisen oliomuuttujan nimen ja arvon.Esim. jos liität moduulin luokkaan
Henkilo
, ja kutsut metodia henkilöoliolle seuraavassa tilanteessaclass Henkilo include Debugattava # ... end h = Henkilo.new 'Chang', 'Alppila' h.tilatulostuu
nimi Chang osoite Alppila ika 0
Rubyn luokka Object, jonka kaikki luokat perivät tarjoavat sopivat metodit, joiden avulla pääset oliomuuttujiin ja niiden arvoihin käsiksi. Ks. http://ruby-doc.org/core-2.3.0/Object.html
Tee luokka
Lukija
, joka saa konstruktorin parametrina tiedoston nimen. Tiedosto sisältää tuntikirjauksia, ja on muotoa:2.9.15 7h 3.9.15 3h 4.9.15 8h 5.9.15 4h 6.9.15 5h 7.9.15 4h 8.9.15 1h 11.9.15 2h
eli tiedoston yksittäinen rivi sisältää päivämäärän ja tuntikirjauksen. Luokalla on metodi
tilasto
, joka palauttaa syötteen perusteella muodostettavan hashin, jolla on seuraavat avaimet ja arvot
- :yhteensa, tuntikirjausten summa
- :keskiarvo, keskimääräinen tuntikirjaus
- :alku, tuntikirjausten ensimmäinen päivä
- :loppu, tuntikirjausten viimeinen päivä
Huomaa, että kirjaukset eivät ole tiedostossa välttämättä järjestyksessä
Olemme jo moneen kertaan käyttäneet koodilohkoja, esim. metodin each
yhteydessä
t = [1, 2, 3, 4]
t.each do |alkio|
puts alkio
end
Esimerkissä metodin each parametrina on lohko, joka suoritetaan jokaiselle taulukon alkiolle.
Itseasiassa Rubyssä jokaiselle metodille voi antaa parametriksi lohkon. Jos olisimme määritelleet
def metodi
puts "hello world"
end
voitaisiin sitä kutsua seuraavasti
metodi do
puts "olen lohko"
end
tai käyttämättä vaihtoehtoista syntaksia lohkon määrittelylle
metodi {
puts "olen lohko"
}
end
Lohko ei nyt kuitenkaan vaikuta mitenkään metodin suoritukseen.
Metodissa voidaan suorittaa parametrina oleva lohko komennolla yield
. Jos määritellään
def metodi2
puts "metodin omaa koodia"
yield
puts "lisää metodin koodia"
end
ja kutsutaan
metodi2 do
puts "olen lohko"
end
tulostuu
metodin omaa koodia
olen lohko
lisää metodin koodia
eli komennon yield
kutsu suorittaa lohkon koodin.
Lohkolle voidaan myös välittää kutsussa yksi tai useampia parametreja:
def metodi3
puts "metodin omaa koodia"
yield 10
yield 3
yield 7
puts "lisää metodin koodia"
end
Kun kutsutaan
metodi3 do |x|
puts "olen lohko, parametri #{x}"
end
tulostuu
metodin omaa koodia
olen lohko, parametri 10
olen lohko, parametri 3
olen lohko, parametri 7
lisää metodin koodia
Tee (tiedostoon koodi.rb) metodi
tulosta(x)
, joka tulostaa parametrinsax
siten, että parametrille on suoritettu ensin metodin parametrina oleva koodilohko. Jos metodille ei ole annettu koodilohkoa parametriksi, se tulostaa ainoastaan parametrinx
. Vihje: metodi voi tarkistaa onko sen parametrina koodilohko kutsumallablock_given?
Esim. jos metodia kutsutaan seuraavasti:
tulosta 5 do |luku| "x"*luku endTulostuu
xxxxx
Tee luokka
Pino
, jolla on seuraavat metodit
push(x)
laittaa x:n pinoonpop
ottaa palauttaa pinon päälimmäisen alkion, jos pino on tyhjä heitetäänRuntimeError
poikkeus Tämän lisäksi pinon tulee sisällyttää moduuliEnumerable
Sisälläytyksen ansiosta pino siis saa kaiken toiminnallisuuden mitä Enumerable-moduuli määrittelee