Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Klasser, objekter og variabler



Ut fra eksemplene vi har vist så langt, undrer du kanskje på vår tidligere fastslåelse av at Ruby er et objekt-orientert språk. Vel, dette kapitlet er stedet hvor vi vil rettferdiggjøre den påstanden. Vi skal se på hvordan man lager objekter og klasser i Ruby, og på noen av måtene Ruby er kraftigere enn de fleste objekt-orienterte språk. Langs veien vil vi implementere deler av vårt neste milliard-dollar produkt, Den Internet-tilpassede Jazz- og BlueGrass-jukeboksen.

Etter måneders arbeid har våre høyt betalte forsknings- og utviklingsfolk fastslått at vår jukeboks trenger sanger. Så det ser ut som en god ide å sette opp en Ruby-klasse som representerer sanger. Vi vet at en ekte sang har et navn, en artist, og en varighet, så vi vil være sikker på at sang-objektene i vårt program også har det.

Vi vil starte opp med å lage en grunnleggende klasse 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>"

Vel, det ser ut til å fungere. Vanligvis dumper 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 inspects 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>"

Det var ikke særlig nyttig---den rapporterte bare objekt-id-en. Så la oss overstyre 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)"

Herlig! Vi gjør framskritt. Men, vi har også lagt inn noe subtilt. Vi sier at Ruby støtter 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å....

Arv og beskjeder

Arv tillater dere å lage en klasse som er en raffinering eller spesialisering av en annen klasse. Vår jukeboks har for eksempel sang-konseptet som vi kapsler inn i klassen 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)"

Vel, den kjørte, men hvorfor viser ikke 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 KaraokeSongs 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...]"

Vi viser korrekt verdien av @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...]"

Vi fortalte Ruby tydelig at 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.

Arv og Mixins

Noen objekt-orientere språk (spesielt C++) har støtte for multippel arv, hvor en klasse kan ha mer enn en direkte forelder, og arver funksjonalitet fra dem alle. Selv om dette er et kraftig verktøy, kan denne teknikken være farlig, fordi arvehierarkiet kan bli flertydig.

Andre språk, som Java, støtter kun enkel arv. Her kan en klasse kun ha en direkte forelder. Selv dette er renere (og lettere å implementere), har enkel arv også sine ulemper---i den virkelige verden arver ofte ting attributter fra flere kilder (en ball er både en sprettende ting og en sfærisk ting, for eksempel).

Ruby tilbyr et interessant og kraftig kompromiss, som gir deg enkelheten av enkel arv og kraften av multippel arv. En Ruby-klasse kan bare ha en direkte forelder, og derav er Ruby et enkel-arv-språk. Imidlertid kan Ruby-klasser også inkludere funksjonaliteten til et hvilket som helst antall av mixins (en mixin er som en delvis klassedefinisjon). Dette tilbyr en egenskap som ligner på kontrollert multippel-arv, uten ulempene. Vi vil utforske mixins mer på side 98 og utover.

Så langt i dette kapittelet har vi sett på klasser og deres metoder. Nå er det på tide å gå over til objektene, slik som instansene av klassen Song.

Objekter og attributter

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

Her har vi definert tre tilgangsmetoder for å retunere verdiene til de tre instansattributtene. Fordi dette er slikt et vanlig tilfelle, tilbyr Ruby en bekvem snarvei: 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

Dette eksemplet har introdusert noe nytt. Konstruksjonen :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.

Skrivbare attributter

Noen ganger så trenger du å være i stand til å sette en attributt fra utsiden av et objekt. For eksempel la oss anta at varigheten, som opprinnelig assosieres med en sang, er et estimat (kanskje fått fra informasjon på en CD eller i MP3-data). Den første gangen vi spiller sangen finner vi ut hvor lang den faktisk er, og vi lagrer denne nye verdien tilbake i 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)

I Ruby kan attributter til et objekt bli aksessert som de var hvilken som helst variabel. Vi har sett dette ovenfor med fraser som 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

Tildelingen "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

Virtuelle attributter

Disse aksesseringsmetodene for attributter trenger ikke være slike simple innpakninger rundt et objekts instansvariabler. For eksempel, trenger du kanskje å aksessere varigheten i minutter og brøkdeler av et minutt, i motsetning til sekunder som vi har gjort tidligere.

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

Her har vi benyttet attributt-metoder for å lage en virtuell instansvariabel. For utenomverdenen, ser 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.

Klassevariabler og klassemetoder

Så langt har alle de klassene vi har skapt inneholdt instansvariabler og instansmetoder: variabler som er assosiert med en spesiell instans av klassen, og metoder som virker på disse variablene. Noen ganger har klasser sin egen tilstand. Dette er hvor klassevariabler kommer inn.

Klassevariabler

En klassevariabel blir delt mellom alle objekter til en klasse, og den er også tilgjengelig for klassemetoder som vi skal beskrive senere. Det er kun en kopi av en spesiell klassevariabel for en gitt klasse. Klassevariabler starter med to ``at''-tegn, slik som ``@@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

For avlusingsformål har vi arrangert for 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."

Klassevariabler er privat eiendom tilhørende en klasse og dens instanser. Hvis du vil gjøre dem tilgjengelige for verden utenfor, må du skrive en tilgangsmetode. Denne metoden kan enten være en instansmetode eller, som omtalt i neste seksjon, en klassemetode.

Klassemetoder

Noen ganger må en klasse tilby metoder som ikke trenger å knyttes til et spesielt objekt.

Vi har allerede kommer over en slik metode. new-metoden lager et nytt Song-objekt, men er ikke selv assosiert med en spesiell sang.

aSong = Song.new(....)

Du vil finne klassemetoder spredt ut gjennom Ruby-bibliotekene. For eksempel, objekter av klassen 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")

Klassemetoder skiller seg fra sine instansmetoder ved deres definisjon. Klassemetoder er definert ved å plassere klassenavnet og et punktum foran metodenavnet.

class Example

  def instMeth              # instansmetode   end

  def Example.classMeth     # klassemetode   end

end

Jukebokser tar penger for hver sang som blir spilt, ikke pr. minutt. Det gjør korte sanger mer lønnsomme enn lange. Vi vil kanskje hindre sanger som er for lange å være tilgjengelige på SongList. Vi kan definere en klassemetode i 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

Singleton og andre konstruktører

Noen ganger kan du ønske å overstyre den måten som Ruby vanligvis lager objekter på. Som et eksempel, la oss se på vår jukeboks. Fordi vi vil ha mange jukebokser, spredt over hele landet, vil vi gjøre vedlikeholdet så lett som mulig. Del av kravet er å logge alt som skjer med en jukeboks: sanger som blir spilt, pengene mottatt, merkelige væsker som blir helt i den, og så videre. Fordi vi vil reservere nettverksbåndbredden for musikk, vil vi lagre disse loggfilene lokalt. Dette betyr at vi vil trenge en klasse som håndterer logging. Imidlertid vil vi kun ha et loggobjekt pr. jukeboks, og vi vil at det objektet skal deles blant alle de andre objektene som trenger det.

Her kommer Singleton-mønsteret, som er dokumentert i Design Patterns , til sin rett. Vi vil arrangere ting slik at den eneste måte å lage et loggobjekt er å kalle 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

Ved å gjøre Loggers 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

Å bruke klassemetoder som pseudo-konstruktører kan også gjøre livet lettere for brukere av din klasse. Som et trivielt eksempel, la oss se på en klasse 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

Imidlertid et par år senere så skal denne klasse bli benyttet i en annen applikasjon hvor programmererene bli brukt til å lage former ved navn, og ved å spesifiere lengden på siden, ikke omkretsen. Bare legg til noen klassemetoder til Shape.

class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end

Det er mange interessante og kraftfulle bruksområder for klassemetoder, men å utforske dem vil ikke få ferdig vår jukeboks noe raskere, så la oss gå videre.

Tilgangskontroll

Når du designer brukergrensesnittet til en klasse, er det viktig å vurdere hvor mye tilgang du ønsker at klassen din skal eksponere til omverdenen. Tillat for mye tilgang til din klasse, og du risikerer å øke graden av kobling i din applikasjon---brukere av klassen vil bli fristet til å stole på detaljer i din klasses implementasjon, heller enn på dens logiske brukergrensesnitt. De gode nyhetene er at den eneste måten å forandre et objekts tilstand i Ruby er ved å kalle en av dets metoder. Kontroller tilgang til metodene og du har kontrollert tilgang til objektet. En god tommelfingerregel er å aldri å eksponere metoder som kan sette et objekt i en ugyldig tilstand. Ruby gir oss tre nivå av beskyttelse.

Forskjellen mellom "protected" og "private" er ganske subtil, og Ruby skiller seg her fra de fleste andre OO-språk. Hvis en metode er beskyttet kan den kalles av enhver instans av den definerende klassen eller dens subklasser. Hvis en metode er privat kan den bare kalles fra inne i konteksen til det kallende objektet---det er umulig å nå et annet objekts private metoder direkte, selv om objektet er av samme klasse som kalleren.

Ruby skiller seg fra andre OO-språk på en annen viktig måte. Tilgangskontroll er bestemt dynamisk, mens programmet kjører, ikke statisk. Du vil få en tilgangsfeil først når koden forsøker å kalle den begrensede metoden.

Spesifisere tilgangskontroll

Du spesifiserer tilgangsnivåer til metoder inne i klasse- eller moduldefinisjoner ved å bruke en eller flere av de tre funksjonene 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

Alternativt kan du sette tilgangsnivåer til navngitte metoder ved å liste dem som argumenter til tilgangskontroll-funksjonene.

class MyClass

  def method1   end

  # ... og så videre

  public    :method1, :method4   protected :method2   private   :method3 end

En klasses 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

Protected brukes når objekter trenger tilgang til den interne tilstanden til andre objekter av den samme klassen. For eksempel trenger vi å tillate de individuelle 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

Fordi attributten balance er beskyttet, er den tilgjengelig bare innen Account-objekter.

Variabler

Nå som vi har gått igjennom alt strevet med å lage alle disse objektene, la oss forsikre oss om at vi ikke mister dem. Variabler blir benyttet til å holde oversikt over objekter; hver variabel holder en referanse til et objekt.

Figur 3.1: Variabler holder referanser til objekter

La oss sjekke med litt kode.

person = "Tim"
person.id » 537767064
person.type » String
person » "Tim"

På den første linjen lager Ruby et nytt 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"

Hva skjedde her? Vi forandret det første tegnet i 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 person1s 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"

Du kan også hindre noen fra å forandre et spesielt objekt ved å fryse det (vi snakker mer om frysing av objekter på side 251). Forsøk på å endre et frossent objekt vil føre til at Ruby kaster et TypeError-unntak.

person1 = "Tim"
person2 = person1
person1.freeze       # forby endringer av objektet
person2[0] = "J"
produserer:
prog.rb:4:in `=': can't modify frozen string (TypeError)
	from prog.rb:4

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.17 $ )
$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 >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.