|
|||
Forrige < |
Innhold ^
|
Neste >
|
Song
,
[Som vi nevnte på side 9, starter klassenavn med en stor bokstav,
mens metodenavn start med en liten bokstav.]
som kun inneholder en enkel metode, initialize
.
class Song def initialize(name, artist, duration) @name = name @artist = artist @duration = duration end end |
initialize
er en spesiell metode i Ruby-programmer. Når du kaller
Song.new
for å lage en ny
Song
-objekt, vil Ruby lage et uinitialisert objekt, og så
kaller dette objektets initialize
-metode, og sender inn alle parametere som
ble gitt til new
. Dette gir deg mulighet til å skrive kode som setter opp
ditt objekts tilstand.
For klassen Song
tar initialize
-metoden tre parametere.
Disse parameterene er akkurat som lokale variabler inne i metoden, så de følger den lokale
navnekonvensjonen for variabler med å starte med en liten bokstav.
Hvert objekt representerer sin egen sang, så vi trenger hvert av våre
Song
-objekter til å bære rundt på sitt eget navn, artist, og varighet.
Det betyr at vi trenger å lagre disse verdiene som instansvariabler innen objektet.
I Ruby er en instansvariabel bare et navn med et ``at''-tegn (@) foran. I vårt eksempel
blir parameteren name
tilordnet til @artist
, og duration
(lengden
av sangen i sekunder) blir tilordnet til @duration
.
La oss teste vår flunkende nye klasse.
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.inspect
|
» |
"#<Song:0x401b299c @artist=\"Fleck\", @name=\"Bicylops\", @duration=260>"
|
inspect
-beskjeden,
som kan sendes til alle objekt, ut objektets id og instansvariabler. Det ser ut som
vi har satt dem opp korrekt.
Vår erfaring tilsier at under utvikling vil vi printe ut innholdet av
Song
-objektet mange ganger, og inspect
s
standard formatering kunne være bedre.
Heldigvis har Ruby en standardbeskjed, to_s
,
som den sender til alle objekter som den vil gjengi som en streng. La oss prøve den på
vår sang.
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.to_s
|
» |
"#<Song:0x401b2a14>"
|
to_s
i vår klasse. Mens vi gjør dette,
skal vi også ta et øyeblikk for å snakke om hvordan vi viser
klassedefinisjoner i denne boken.
I Ruby er klasser aldri lukket: du kan alltid legge til metoder til
en eksisterende klasse. Dette gjelder for klasser som du skriver så
vel som de standard, innebygde klassene. Alt du trenger å gjøre er å åpne
opp en klassedefinisjon for en eksisterende klasse, og det nye innholdet
du spesifiserer vil bli lagt til hva som enn er der fra før.
Dette er perfekt til våre formål. Mens vi går igjennom dette kapittelet,
og legger egenskaper til våre klasser, vil vi kun vise klassedefinisjonene
for de nye metodene; de gamle vil fortsatt være der. Det sparer oss fra å ha
overflødige saker i hvert eksempel.
Men naturligvis ville du bare kastet alle metodene inn i en enkelt klassedefinisjon dersom du skrev denne koden fra bunnen av.
Nok detaljer! La oss komme tilbake til å legge til en to_s
til
vår Song
-klasse.
class Song
|
||
def to_s
|
||
"Song: #{@name}--#{@artist} (#{@duration})"
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.to_s
|
» |
"Song: Bicylops--Fleck (260)"
|
to_s
for alle objekter, men vi sa
ikke hvordan. Svaret har å gjøre med arv, subklassing, og hvordan Ruby
bestemmer hvilken metode den skal utføre når du sender en beskjed til et
objekt. Dette er tema for et nytt avsnitt, så....
Song
. Så kommer markedsføringsavdelingen gående
og forteller oss at vi trenger å legge til støtte for karaoke. En karaoke-sang
er akkurat som alle andre(det er ingen vokal på den, men det bekymrer ikke oss).
Men, den har et assosiert sett av tekster, i sammenheng med timing-informasjon.
Når vår jukeboks spiller en karaoke-sang, skal teksten fly over skjermen på fronten
av jukeboksen i takt med musikken.
En tilnærming på dette problemet er å definere en ny klasse, KaraokeSong
,
som er akkurat som Song
, men med et tekst-spor.
class KaraokeSong < Song def initialize(name, artist, duration, lyrics) super(name, artist, duration) @lyrics = lyrics end end |
< Song
'' på klassedefinisjonslinjen sier Ruby at en
KaraokeSong
er en subklasse av Song
.
(Ikke overraskende betyr dette at Song
er en superklasse
av KaraokeSong
. Noen snakker også om foreldre-barn-forhold, slik at
KaraokeSong
vil være forelder til Song
.) For øyeblikket trenger
du ikke bekymre deg for mye om initialize
-metoden; vi vil ta for oss det
super
-kallet litt senere.
La oss lage en KaraokeSong
og sjekke om vår kode fungerte. (I det ferdige
systemet vil tekstene bli holdt i et objekt som inkluderer tekst- og timing-informasjon. For å
teste vår klasse vil vi derimot kun bruke en streng. Dette er enda en fordel med utypede
språk---vi trenger ikke å definere alt før vi begynner å kjøre kode.
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"Song: My Way--Sinatra (225)"
|
to_s
-metoden
teksten?
Svaret har å gjøre med måten Ruby bestemmer hvilken metode som skal kalles
når du sender en beskjed til et objekt. Når Ruby kompilerer metode-kallet
aSong.to_s
, vet den ikke egentlig hvor den skal finne metoden
to_s
. Istedet venter den med bestemmelsen til programmet er kjørt.
På den tiden ser den på klassen til aSong
. Hvis den klassen implementerer
en metode med det samme navnet som beskjeden, vil den metoden bli kjørt. Ellers vil
Ruby se etter en metode i forelder-klassen og så i besteforelderen, og så videre oppover
slektskjeden. Hvis den slipper opp for forfedre uten å finne den passende metoden,
vil den gjøre noe spesielt som normalt resulterer i en feilmelding.
[Faktisk så kan du snappe opp denne feilen og dette tillater deg å jukse ved å late som om du har metoder som du ikke har under kjøring. Dette er beskrevet under
Object#method_missing
på side 355.]
Så tilbake til vårt eksempel. Vi sendte beskjeden to_s
til
aSong
, et objekt av klassen KaraokeSong
.
Ruby ser i KaraokeSong
etter en metode kalt to_s
,
men finner den ikke. Fortolkeren ser da i KaraokeSong
s
forelder, Song
, og der finner den to_s
-metoden
vi definerte på side 18. Derfor printer den ut sang-detaljene men ikke tekstene --- Song
-klassen vet ingenting om tekster.
La oss fikse dette ved å implementere KaraokeSong#to_s
.
Det er mange måter å gjøre dette på. La oss starte på en dårlig måte. Vi kopierer
to_s
-method from Song
og legger til tekstene.
class KaraokeSong
|
||
# ...
|
||
def to_s
|
||
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
|
||
end
|
||
end
|
||
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"KS: My Way--Sinatra (225) [And now, the...]"
|
@lyrics
-instansvariabelen. For å
gjøre dette aksesserer subklassen instansvariabelen til sine forfedre.
Så hvorfor er dette en dårlig måte å implementere to_s
på?
Svaret har å gjøre med god programmeringsstil (og noe som kalles
decoupling). Ved å snuse rundt i vår forelders interne
tilstand, binder vi oss selv tett til dens implementasjon. Si at vi bestemte
å forandre Song
til å lagre varigheten i millisekunder.
Plutselig ville KaraokeSong
starte å gjengi latterlige
verdier. Tanken på en karaoke-versjon av "My Way" som varer i 3750 minutter
er bare for skremmende.
Vi unngår dette problemet ved å la hver klasse håndtere sin egen interne
tilstand. Når KaraokeSong#to_s
blir kalt,
vil vi ha den til å kalle forelderens to_s
-metode for å få sang-
detaljene. Den vil da legge til tekst-informasjonen og returnere resultatet. Trikset
ligger i Ruby-nøkkelordet "super
". Når du kaller super
uten
argumenter, vil Ruby sende en beskjed til det nåværende objektets forelder, og be
det om å kalle en metode med samme navn som den nåværende metoden. Nå kan vi implementere
vår nye og bedre to_s
.
class KaraokeSong < Song
|
||
# Formater oss som en streng ved å legge
|
||
# våre tekster til verdien vår forelders #to_s returnerer.
|
||
def to_s
|
||
super + " [#{@lyrics}]"
|
||
end
|
||
end
|
||
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
||
aSong.to_s
|
» |
"Song: My Way--Sinatra (225) [And now, the...]"
|
KaraokeSong
var en subklasse av Song
,
men vi spesifiserte ikke en foreldre-klasse til selve Song
. Hvis
du ikke spesifiserer en forelder når du definerer en klasse, vil Ruby bruke
klassen Object
. Dette betyr at alle objekter har
Object
som en forelder, og at instansmetodene til
Object
er tilgjengelig til hvert objekt i Ruby.
Tidligere på side 18 sa vi at
to_s
er tilgjengelig for alle objekter. Nå vet vi hvorfor; to_s
er en av over 35 instansmetoder i klassen Object
. Den komplette
listen begynner på side 351.
Song
.
Song
-objekter som vi har laget så langt har en intern tilstand
(slik som sangtittelen og artist). Den tilstanden er privat for disse objektene---ingen
andre objekter kan aksessere et objekts instansvariabler. Generelt er dette en god ting.
Det betyr at objektet ene og alene er ansvarlig for å bevare sin egen konsistens.
Imidlertid er et objekt som er totalt hemmelig også ganske ubrukelig---du kan lage det, men
så kan du ikke gjøre noe med det. Du vil normalt definere metoder som lar deg aksessere og
manipulere tilstanden til et objekt, samt tillate omverdenen til å samhandle med objektet.
Disse eksterne synlige fasetter ved et objekt blir kalt dets attributter.
Den første tingen vi kan trenge i våre
Song
-objekter er muligheten til å
finne ut tittel og artist (så vi kan vise dem mens sangen blir spilt),
og varigheten (så vi kan vise en slags tidsframviser).
class Song
|
||
def name
|
||
@name
|
||
end
|
||
def artist
|
||
@artist
|
||
end
|
||
def duration
|
||
@duration
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.artist
|
» |
"Fleck"
|
aSong.name
|
» |
"Bicylops"
|
aSong.duration
|
» |
260
|
attr_reader
lager alle disse tilgangsmetoder
for deg.
class Song
|
||
attr_reader :name, :artist, :duration
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.artist
|
» |
"Fleck"
|
aSong.name
|
» |
"Bicylops"
|
aSong.duration
|
» |
260
|
:artist
er et uttrykk som returnerer et Symbol
-objekt
som korresponderer til artist
. Du kan tenke på :artist
som å bety navnet på variabelen artist
, mens bare
artist
er verdien til variabelen. I dette eksemplet
navnga vi tilgangsmetodene name
, artist
, og duration
.
De korresponderende instansvariablene @name
, @artist
og
@duration
, blir skapt automatiskt. Disse tilgangsmetodene er identiske som de
vi skrev for hånd tidligere.
Song
-objektet.
I språk som C++ og Java ville du gjøre dette med setter-funksjoner.
class JavaSong { // Java kode private Duration myDuration; public void setDuration(Duration newDuration) { myDuration = newDuration; } } s = new Song(....) s.setDuration(length) |
aSong.name
. Så det synes naturlig å være i stand til
å tildele disse variablene når du vil sette verdiene til en attributt. Og for å holde seg i takt med
Prinsippet om den Minste Overraskelse, er det er akkurat det du gjør i Ruby.
class Song
|
||
def duration=(newDuration)
|
||
@duration = newDuration
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.duration
|
» |
260
|
aSong.duration = 257 # tilordner oppdatert verdi til attributtet
|
||
aSong.duration
|
» |
257
|
aSong.duration = 257
" kaller metoden
duration=
i aSong
-objektet, og sender inn 257
som argument. Faktisk trenger du bare å definere et metodenavn som ender i et likhetstegn
for å gjøre navnet berettighet til å framstå på den venstre side av en tildeling.
Igjen tilbyr Ruby en snarvei for å lage disse enkle metodene for tilordning til attributter.
class Song attr_writer :duration end aSong = Song.new("Bicylops", "Fleck", 260) aSong.duration = 257 |
class Song
|
||
def durationInMinutes
|
||
@duration/60.0 # tving bruk av flyt-tall
|
||
end
|
||
def durationInMinutes=(value)
|
||
@duration = (value*60).to_i
|
||
end
|
||
end
|
||
aSong = Song.new("Bicylops", "Fleck", 260)
|
||
aSong.durationInMinutes
|
» |
4.333333333
|
aSong.durationInMinutes = 4.2
|
||
aSong.duration
|
» |
252
|
durationInMinutes
ut til å være en attributt som
alle andre. Internt, derimot, er det ingen tilsvarende
instansvariabel.
Dette er mer enn en kuriositet. I hans milepæl-bok Object-Oriented Software Construction ,
kaller Bertrand Meyer dette for ``the Uniform Access Principle''. Ved å gjemme bort forskjellen
mellom instansvariabler og kalkulerte verdier kan du beskytte resten av verden mot detaljene i
implementasjonen av din klasse. Du er fri til å forandre hvordan ting fungerer i framtiden uten
å påvirke millioner av linjer med kode som bruker din klasse. Dette er et stort pluss.
@@count
''. I motsetning til globale og instansvariabler må klassevariabler initialiseres før de kan brukes.
Ofte er denne initialiseringen bare en enkel tildelig i kroppen til
klassedefinisjonen.
For eksempel kunne vi ønske at vår jukeboks registrerte hvor mange ganger en spesiell sang
har blitt spilt. Denne tellingen ville antagelig være en instansvariabel til
Song
-objektet. Når en sang blir avspilt vil verdien i instansen
bli inkrementert. Men la oss si at vi også vil vite hvor mange sanger som har blitt spilt totalt.
Vi kunne gjøre dette ved å søke etter alle Song
-objekter og legge til
deres tellinger, eller vi kunne risikere bannlysing fra
``the Church of Good Design''
og bruke en global variabel. Isteden vil vi bruke en klassevariabel.
class Song @@plays = 0 def initialize(name, artist, duration) @name = name @artist = artist @duration = duration @plays = 0 end def play @plays += 1 @@plays += 1 "This song: #@plays plays. Total #@@plays plays." end end |
Song#play
å returnere en
streng som inneholder antallet ganger denne sangen har blitt spilt, med det totale antallet
avspillinger for alle sanger. Dette er enkelt å teste.
s1 = Song.new("Song1", "Artist1", 234) # teste noen sanger...
|
||
s2 = Song.new("Song2", "Artist2", 345)
|
||
s1.play
|
» |
"This song: 1 plays. Total 1 plays."
|
s2.play
|
» |
"This song: 1 plays. Total 2 plays."
|
s1.play
|
» |
"This song: 2 plays. Total 3 plays."
|
s1.play
|
» |
"This song: 3 plays. Total 4 plays."
|
new
-metoden lager et nytt Song
-objekt,
men er ikke selv assosiert med en spesiell sang.
aSong = Song.new(....) |
File
representerer åpne filer
i det underliggende filsystemet. Imidlertid tilbyr klassen File
også flere andre klassemetoder for manipulering av filer som ikke er åpne
og derfor ikke har et File
-objekt. Hvis du vil slette en fil,
kaller du klassemetoden
File.delete
og sender inn navnet.
File.delete("doomedFile") |
class Example def instMeth # instansmetode end def Example.classMeth # klassemetode end end |
SongList
som sjekker for å se om en spesiell sang går
over grensen. Vi vil sette denne grensen ved å bruke en klassekonstant, som
ganske enkelt er en konstant (Du husker konstanter, ikke sant? De som starter med en stor bokstav?)
som blir initialisert i klassekroppen.
class SongList
|
||
MaxTime = 5*60 # 5 minutter
|
||
|
||
def SongList.isTooLong(aSong)
|
||
return aSong.duration > MaxTime
|
||
end
|
||
end
|
||
song1 = Song.new("Bicylops", "Fleck", 260)
|
||
SongList.isTooLong(song1)
|
» |
false
|
song2 = Song.new("The Calling", "Santana", 468)
|
||
SongList.isTooLong(song2)
|
» |
true
|
Logger.create
, og vi vil
forsikre oss om at kun et loggobjekt blir laget.
class Logger private_class_method :new @@logger = nil def Logger.create @@logger = new unless @@logger @@logger end end |
Logger
s metode new
privat, vil vi hindre
noen fra å lage et loggobjekt ved å bruke den vanlige kontruktøren. Isteden vil vi skaffe en
klassemetode Logger.create
. Denne bruker klassevariabelen
@@logger
til å holde en referanse til en enkel instans av loggeren, og returnerer bare
den instansen hver gang den blir kalt. [Implementasjonen av singletons som vi presenterer
her er ikke thread-safe; hvis flere tråder kjører, kunne det være mulig å lage flere
loggobjekter. Heller enn å legge til thread-safety selv, så kunne vi benyttet
Singleton
-mixinen Ruby tilbyr, som er dokumentert på side 468.
] Vi kan sjekke dette ved å se på objektets identifikator som metoden
returnerer.
Logger.create.id
|
» |
537762894
|
Logger.create.id
|
» |
537762894
|
Shape
som representerer et regulært polygon. Instanser
av Shape
blir skapt ved å gi en konstruktør det ønskede
antall sider og den totale omkrets.
class Shape def initialize(numSides, perimeter) # ... end end |
Shape
.
class Shape def Shape.triangle(sideLength) Shape.new(3, sideLength*3) end def Shape.square(sideLength) Shape.new(4, sideLength*4) end end |
initialize
, som alltid er private).
public
, protected
, og private
. Hver funksjon
kan bli brukt på to forskjellige måter.
Hvis benyttet uten argumenter, setter de tre funksjonene standard
tilgangskontroll for de påfølgende definerte metodene. Dette er antagelig oppførsel du er kjent med hvis du er en C++ eller Java-programmerer, hvor du ville
brukt nøkkelord som public
for å oppnå den samme effekten.
class MyClass def method1 # default er 'public' #... end protected # påfølgende metoder vil bli 'protected' def method2 # vil bli 'protected' #... end private # påfølgende metoder vil bli 'private' def method3 # vil bli 'private' #... end public # påfølgende metoder vil bli 'public' def method4 # og denne vil bli 'public' #... end end |
class MyClass def method1 end # ... og så videre public :method1, :method4 protected :method2 private :method3 end |
initialize
metode blir automatisk deklarert privat.
Det er tid for noen eksempler. Kanskje vi modellerer en regnskapssystem
hvor hver debet har en korresponderende kreditt. Fordi vi vil forsikre
at ingen kan bryte denne regelen, vil vi lage metoder som gjør debet
og kreditt private, og vi vil definere vår eksterne brukergrensesnitt
i form av transaksjoner.
class Accounts private def debit(account, amount) account.balance -= amount end def credit(account, amount) account.balance += amount end public #... def transferToSavings(amount) debit(@checking, amount) credit(@savings, amount) end #... end |
Account
-objekter til å sammenligne deres
råbalanser, men kanskje vil vi forhindre resten av verden å få tak i disse balansene (kanskje
fordi vi vil presentere dem i en annerledes form).
class Account attr_reader :balance # tilgangsmetode 'balance' protected :balance # og gjør den protected def greaterBalanceThan(other) return @balance > other.balance end end |
balance
er beskyttet, er den tilgjengelig
bare innen Account
-objekter.
Figur 3.1: Variabler holder referanser til objekter |
person = "Tim"
|
||
person.id
|
» |
537767064
|
person.type
|
» |
String
|
person
|
» |
"Tim"
|
String
-objekt
med verdien "Tim". En referanse til dette objektet er plassert
i den lokale variabelen person
.
En kjapp sjekk viser at variabelen har virkelig tatt på seg oppførselen til
en streng, med en objekt-id, en type, og en verdi.
Så, er en variabel et objekt?
I Ruby er svaret ``nei.'' En variabel er ganske enkelt en referanse til
et objekt. Objekter flyter rundt i et stort basseng et sted (som oftest på heapen) og blir referert til av variabler.
La oss gjøre eksemplet noe mer komplisert.
person1 = "Tim"
|
||
person2 = person1
|
||
|
||
person1[0] = 'J'
|
||
|
||
person1
|
» |
"Jim"
|
person2
|
» |
"Jim"
|
person1
,
men både person1
og person2
forandret seg fra
"Tim" til "Jim".
Det koker ned til faktumet at variabler holder referanser til
objekter, ikke objektene selv. Tildelingen av person1
til
person2
lager ingen nye objekter, den bare kopierer person1
s
objektreferanse til person2
, slik at både person1
og
person2
refererer til samme objektet. Vi viser dette i figur 3.1 på
side 31.
Tildeling lager alias til objekter, og kan potientielt gi
deg flere variabler som refererer til samme objektet. Men kan dette skape
problemer i din kode? Det kan, men ikke så ofte som du tror (objekter i Java f.eks.
virker på akkurat samme måten). I eksempelet i figur 3.1, kunne du unngå
aliasing ved å bruke dup
-metoden til String
, som
skaper et nytt String
-objekt med likt innhold.
person1 = "Tim"
|
||
person2 = person1.dup
|
||
person1[0] = "J"
|
||
person1
|
» |
"Jim"
|
person2
|
» |
"Tim"
|
TypeError
-unntak.
person1 = "Tim" person2 = person1 person1.freeze # forby endringer av objektet person2[0] = "J" |
prog.rb:4:in `=': can't modify frozen string (TypeError) from prog.rb:4 |
$Log: tut_classes.xml,v $ Revision 1.17 2003/09/14 10:21:21 kent Gjennomgang. Revision 1.16 2003/08/31 11:58:57 kent Fjerner masse KapitaliseringsMani i overskrifter. Revision 1.15 2003/02/10 19:25:18 kent Glemte et avsnitt. Revision 1.14 2003/02/10 19:19:49 kent Gjennomgang og pynting av små vendinger. Revision 1.13 2002/08/17 15:16:57 kent Ferdig nok en gjennomgang med mindre omskrivinger og retting av småfeil.
Forrige < |
Innhold ^
|
Neste >
|