Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Refleksjon, ObjectSpace og distribuert Ruby



En av de mange fordelene med dynamiske språk slik som Ruby er muligheten til introspect --- å undersøke aspekter til programmet innenfor programmet selv. Java, f.eks., kaller denne egenskapen for refleksjon.

Ordet "refleksjon" får fram et bilde av å se på seg selv i speilet---kanskje for å undersøke den uhemmede spredningen av en skallet flekk på toppen av ens hode. Det er en ganske passende analogi: vi bruker refleksjon for å undersøke deler av våre program som normalt ikke er synlige fra der vi står.

I dette dypt introspektive humøret, mens vi beskuer våre navler og brenner røkelse (og er forsiktig med å ikke bytte om på de to oppgavene), hva kan vi lære om vårt program? Vi kan oppdage:

Ustyrt med denne informasjonen, kan vi se på spesielle objekter og bestemme hvilke av deres metoder å kalle på i nåtid---selv om klassen til objektet ikke eksisterte når vi først skrev koden. Vi kan også starte å gjøre finurlige ting, slik som å modifisere programmet mens det kjører.

Høres det skummelt ut? Det trenger ikke å være. Faktisk så kan disse refleksjonsmulighetene la deg gjøre noen nokså nyttige ting. Senere i dette kapittelet vil vi se på distribuert Ruby og marshaling, to refleksjonsbaserte teknologier som lar oss sende objekter rundt i verden og gjennom tiden.

Se på objekter

Har du noen gang ønsket å ha muligheten å gå igjennom alle levende objektene i ditt program? Det har vi! Ruby lar deg gjøre dette trikset med ObjectSpace::each_object . Vi kan bruke det til å gjøre alle slags flotte triks.

For eksempel å iterere over alle objeketer av type Numeric, kunne du skrive det følgende.

a = 102.7
b = 95.1
ObjectSpace.each_object(Numeric) {|x| p x }
produserer:
95.1
102.7
2.718281828
3.141592654

Hei, hvor kom de siste to nummerene fra? Vi definerte ikke dem i vårt program. Hvis du ser på side 429, kan du se at Math-modulen definerer konstanter for e og PI; siden vi undersøker alle levende objekter i systemet dukker disse opp også.

Men det er en hake. La oss se på det samme eksempelet med andre tall.

a = 102
b = 95
ObjectSpace.each_object(Numeric) {|x| p x }
produserer:
2.718281828
3.141592654

Ingen av Fixnum-objektene vi lagde kom opp. Det er fordi ObjectSpace ikke vet om objekter med umiddelbare verdier: Fixnum, true, false og nil.

Se inn i objekter

Når du har funnet et interessant objekt, kan du bli fristet til å finne ut akkurat hva det kan gjøre. I motsetning til statiske språk, hvor variabelens type bestemmer dens klasse og derav metodene den supporterer, så støtter Ruby frigitte objekter. Du kan ikke si eksakt hva et objekt kan gjøre før du tar en titt under dets panser.

For eksempel kan vi lage en liste over alle metodene som et objekt vil respondere på.

r = 1..10 # Lag et Range-objekt
list = r.methods
list.length » 60
list[0..3] » ["size", "exclude_end?", "to_s", "length"]

Eller vi kan sjekke for å se om et objekt støtter en spesiell metode.

r.respond_to?("frozen?") » true
r.respond_to?("hasKey") » false
"me".respond_to?("==") » true

Vi kan bestemme vårt objekts klasse og dets unike objekt-id, samt teste hvilket forhold det har til andre klasser.

num = 1
num.id » 3
num.class » Fixnum
num.kind_of? Fixnum » true
num.kind_of? Numeric » true
num.instance_of? Fixnum » true
num.instance_of? Numeric » false

Se på klasser

Å vite om objekter er en del av refleksjon, men for å få det hele bildet kan du også trenge å kunne se på klasser---metodene og konstantene de inneholder.

Å se på klasse-hierarkiet er enkelt. Du kan få forelderen til hvilken som helst klasse ved å bruke Class#superclass . For klasser og moduler lister Module#ancestors både superklasser og moduler som er mixed-in.

klass = Fixnum
begin
  print klass
  klass = klass.superclass
  print " < " if klass
end while klass
puts
p Fixnum.ancestors
produserer:
Fixnum < Integer < Numeric < Object
[Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel]

Hvis du vil bygge et komplett klassehierarki kan du bare kjøre denne koden for hver klasse i systemet. Vi kan bruke ObjectSpace for å iterere over alle Class-objekter:

ObjectSpace.each_object(Class) do |aClass|
   # ...
end

Se inn i klasser

Vi kan finne ut mer om metoder og konstanter i et spesielt objekt. Istedenfor å bare sjekke om objektet responderer til en gitt beskjed, kan vi spørre etter metoder med et gitt aksess-nivå, vi kan spørre etter kun singleton-metoder, og vi kan ha en titt på objektets konstanter.

class Demo
  private
    def privMethod
    end
  protected
    def protMethod
    end
  public
    def pubMethod
    end
  def Demo.classMethod
  end
  CONST = 1.23
end
Demo.private_instance_methods » ["privMethod"]
Demo.protected_instance_methods » ["protMethod"]
Demo.public_instance_methods » ["pubMethod"]
Demo.singleton_methods » ["classMethod"]
Demo.constants - Demo.superclass.constants » ["CONST"]

Module.constants returnerer alle konstantene tilgjengelig via en modul, inkludert konstanter fra modulens superklasser. Vi er ikke interessert i disse i øyeblikket, så vi trekker dem fra vår liste.

Gitt en liste med metode-navn, kan vi nå bli fristet til å prøve å kalle på dem. Heldigvis er det enkelt i Ruby.

Kalle metoder dynamisk

C og Java-programmerere ender ofte opp med å lage en eller annen form for dispatch-tabell: funksjoner som blir kalt basert på en kommando. Tenk på et typisk C-idiom hvor du må oversette en streng til en funksjonspeker.

typedef struct {
  char *name;
  void (*fptr)();
} Tuple;

Tuple list[]= {   { "play",   fptr_play },   { "stop",   fptr_stop },   { "record", fptr_record },   { 0, 0 }, };

...

void dispatch(char *cmd) {   int i = 0;   for (; list[i].name; i++) {     if (strncmp(list[i].name,cmd,strlen(cmd)) == 0) {       list[i].fptr();       return;     }   }   /* not found */ }

I Ruby kan du gjøre alt dette på en linje. Stikk alle dine kommando-funksjoner inn i en klasse, lag en instans av den klassen (vi kalte den commands), og spør det objektet om å utføre en metode kalt det samme navnet som kommando-strengen.

commands.send(commandString)

I tillegg gjør den mye mer en C-versjonen---den er dynamisk. Ruby-versjonen vil finne nye metoder lagt til i kjøretid like enkelt.

Du trenger ikke å skrive spesielle kommando-klasser for send: det virker på hvilket som helst objekt.

"John Coltrane".send(:length) » 13
"Miles Davis".send("sub", /iles/, '.') » "M. Davis"

En annen mulighet for å kalle metoder dynamisk, er å bruke Method-objekter. Et Method-objekt har mye felles med et Proc-object: det representerer en kodebit og en kontekst som det skal kjøres i. I dette tilfellet er koden som skal kjøres metodekroppen og konteksten er objektet som lagde metoden. Når vi har fått tak i et Method-objekt kan vi kalle det på et senere tidspunkt ved å sende beskjeden call.

trane = "John Coltrane".method(:length)
miles = "Miles Davis".method("sub")
trane.call » 13
miles.call(/iles/, '.') » "M. Davis"

Du kan sende Method-objektet rundt omkring slik som med et hvert annet objekt, og når du kaller Method#call , blir metoden kjørt på akkurat samme måte som om du hadde kalt den direkte på det opprinnelige objektet. Det er som om man har en funksjonspeker i C, men gjennomført i en objekt-orientert stil.

Du kan også bruke Method-objekter med iteratorer.

def double(a)
  2*a
end
mObj = method(:double)
[ 1, 3, 5, 7 ].collect(&mObj) » [2, 6, 10, 14]

Siden alle gode ting er tre, følger her nok en annen måte man kan kalle metoder dynamisk. Det er eval-metoden (samt dens varianter, slik som class_eval, module_eval og instance_eval) parser og utfører en vilkårlig streng som inneholder gyldig Ruby-kode.

trane = %q{"John Coltrane".length}
miles = %q{"Miles Davis".sub(/iles/, '.')}
eval trane » 13
eval miles » "M. Davis"

Når man bruker eval kan det være behjelpelig å angi eksplisitt hvilken kontekst uttrykket skal evalueres innenfor, i stedet for å bruke den gjeldende konteksten. Du kan få tak i en kontekst ved å kalle Kernel#binding på det ønskede punktet i koden.

class CoinSlot
  def initialize(amt=Cents.new(25))
    @amt = amt
    $here = binding
  end
end

a = CoinSlot.new eval "puts @amt", $here eval "puts @amt"
produserer:
$0.25USD
nil

Det første kallet til eval evaluerer uttrykket @amt i konteksten til instansen av klassen CoinSlot. Det andre eval-kallet evaluerer @amt i konteksten til Object hvor instansvariabelen @amt ikke er definert.

Ytelsesvurderinger

Som vi har sett, er det flere måte å kalle en vilkårlig metode til et objekt: Object#send , Method#call og de forskjellige variasjonene av eval.

Hvilken av teknikkene du foretrekker kan avhenge av dine behov, men vær oppmekrsom på at eval er betydelig tregere enn de andre (eller, for mer optimistiske lesere, send og call er betydelig raskere enn eval).

require "benchmark"   # fra RAA, Ruby sin applikasjonsarkiv
include Benchmark

test = "Stormy Weather" m = test.method(:length) n = 100000

bm(12) {|x|   x.report("call") { n.times { m.call } }   x.report("send") { n.times { test.send(:length) } }   x.report("eval") { n.times { eval "test.length" } } }
produserer:
                  user     system      total        real
call          0.200000   0.000000   0.200000 (  0.206547)
send          0.240000   0.000000   0.240000 (  0.232178)
eval          2.570000   0.000000   2.570000 (  2.560201)

Systemkoblinger

En kobling (hook) lar deg fange forskjellige hendelser i Ruby, slik som når et objekt lages.

Den enkleste koblingsteknikken i Ruby er å fange opp kall til metoder i systemklassene. Kanskje ønsker du å logge alle operativsystemkommandoene programmet ditt utfører. Da trenger du bare å endre navnet på metoden Kernel::system [Denne Eiffel-inspirerte eiendommeligheten med å omdøpe en fasilitet og redefinere en ny en kan være veldig nytt, men det kan også forårsake problemer. Dersom en subklasse gjør det samme og omdøper metodene med de samme navnene kan du ende opp med en uendelig løkke. Du kan unngå dette ved å bruke et unikt symbolnavn eller en konsekvent navngivningskonvensjon når du omdøper metoder. ] og bytt den ut med din egen som både logger kommandoen og kaller den opprinnelige Kernel-metoden.

module Kernel
  alias_method :old_system, :system
  def system(*args)
    result = old_system(*args)
    puts "system(#{args.join(', ')}) returned #{result}"
    result
  end
end

system("date") system("kangaroo", "-hop 10", "skippy")
produserer:
Sun Nov 25 23:45:40 CST 2001
system(date) returned true
system(kangaroo, -hop 10, skippy) returned false

En kraftigere kobling du kan gjøre, er å fange objekter etter hvert som som de lages. Dersom du kan være tilstede når hvert enkelt objekt fødes, har du mulighet til å gjøre allverdens interessante ting med dem: du kan pakke dem inn, legge til metoder, fjerne metoder, legge dem inn i beholdere for å implementere persistens, du kan gjøre hva enn du vil. Vi vil vise et enkelt eksempel her: Vi legger til et tidsstempel til hvert objekt når det lages.

En måte å koble seg inn i objektlagingen, er å benytte triksen med å omdøpe metodenavn på Class#new , metoden som blir kalt for å få plass til et nytt objekt. Teknikken er ikke perfekt---noen innebygde objekter, slik som strenger skrevet direkte i kildekoden, blir konstruert uten et kall til new---men det vil fungere helt fint for objekter vi skriver.

class Class
  alias_method :old_new,  :new
  def new(*args)
    result = old_new(*args)
    result.timestamp = Time.now
    return result
  end
end

Vi behøver også å legge til et tidsstempelattributt til hvert objekt i systemet. Vi kan gjøre dette ved å gå løs på selve Object-klassen.

class Object
  def timestamp
    return @timestamp
  end
  def timestamp=(aTime)
    @timestamp = aTime
  end
end

Endelig kan vi kjøre en test. Vi lager et par objekter med noen sekunders mellomrom og sjekker tidsstemplene.

class Test
end
obj1 = Test.new
sleep 2
obj2 = Test.new
obj1.timestamp » Sun Nov 25 23:45:40 CST 2001
obj2.timestamp » Sun Nov 25 23:45:42 CST 2001

All denne metodenavn omdøpingen er vel og bra og den funker faktisk. Men der er andre, mer raffinerte måter å komme seg inn i et kjørende program. Ruby tilbyr flere tilbakekallsmetoder som lar deg fange spesielle hendelser på en kontrollert måte.

Tilbakekall ved kjøretid

Du kan få beskjed når en av de følgende hendelsene finner sted:

Hendelse Tilbakekallsmetode
En instansmetode legges til Module#method_added
En singleton-metode legges til Kernel::singleton_method_added
En subklasse blir opprettet Class#inherited
En modul blir mikset Module#extend_object

Disse teknikkene blir illustrert i biblioteksbeskrivelsene for de enkelte tilbakekallsmetodene. Ved kjøretid vil disse metodene bli kalt av systemet når den spesifiserte hendelsen tar sted. Til vanlig gjør disse metodene ingenting. Dersom du ønsker å få beskjed om hendelse, så trenger du bare å definere tilbakekallsmetoden.

Ved å holde oversikt over metodedefinering og klasse- og modulbruk kan du danne et nøyaktig bilde av den dynamiske tilstanden til programmet. Dette kan være viktig. For eksempel har du kanskje skrevet kode som pakker inn alle metodene til en klasse, muligens for å legge til transaksjonsstøtte eller implementere delegering. Dette er bare halve jobben: den dynamiske naturen til Ruby impliserer at brukere av denne klassen kan legge til nye metoder når som helst. Ved å benytte disse tilbakekallsmetodene kan du skrive kode som pakker disse nye metodene inn når de blir definert.

Følge programmets utførsel

Mens vi har det moro med å reflektere over alle objektene og klassene i våre programmer, la oss ikke glemme de ydmyke utsagene som får koden vår til å faktisk gjøre noe. Det viser seg at Ruby lar oss komme nært inn på disse utsagnene også.

Først av alt kan du holde øye med fortolkeren mens den utfører kode. set_trace_func kjører en Proc med allverdens saftig debuggingsinformasjon hver gang en ny kildekodelinje blir utført, metoder kalles, objekter lages, og så videre. Du finner en fullstendig beskrivelse på side 422, men her er en liten smakebit.

class Test
  def test
    a = 1
    b = 2
  end
end

set_trace_func proc { |event, file, line, id, binding, classname|   printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname } t = Test.new t.test
produserer:
    line prog.rb:11               false
  c-call prog.rb:11        new    Class
  c-call prog.rb:11 initialize   Object
c-return prog.rb:11 initialize   Object
c-return prog.rb:11        new    Class
    line prog.rb:12               false
    call prog.rb:2        test     Test
    line prog.rb:3        test     Test
    line prog.rb:4        test     Test
  return prog.rb:4        test     Test

Det finnes også en metode trace_var (beskrevet på side 427) som lar deg legge til en kobling til en global variabel; hver gang en verdi tilordnes til den globale variabelen, blir ditt Proc-objekt kalt.

Hvordan endte vi opp her?

Et gyldig spørsmål som vi jevnlig stiller oss selv. Dersom vi ser bort i fra huller i hukommelsen vår, kan vi i det minste i Ruby finne ut nøyaktig ``hvordan vi vi kom hit'' ved å benytte metoden caller, som returnerer en Array av String-objekter som representerer den gjeldende kallstakken.

def catA
  puts caller.join("\n")
end
def catB
  catA
end
def catC
  catB
end
catC
produserer:
prog.rb:5:in `catB'
prog.rb:8:in `catC'
prog.rb:10

Når du så har funnet ut hvordan du kom dit, blir det opp til deg hvor du vil gå videre.

Marshaling og distribuert Ruby

Java har muligheten til å serialisere objekter, slik at du kan lagre dem et sted og hente dem frem igjen når de trengs. Du har kanskje benyttet denne muligheten, for eksempel til å lagre en trestruktur av objekter som representerer en del av programmets tilstand---et dokument, en DAK-tegning, et musikkstykke og så videre.

Ruby kaller denne formen for serialisering marshaling (oppstilling). [Se for deg jernbaneområder hvor individuelle vogner settes sammet til et fullstendig tog som så sendes avgårde et sted.] Lagring av et objekt og noen eller alle dets kompontenter gjøres ved å bruke metoden Marshal::dump . Det man ofte gjør, er å dumpe et fullstendig objekttre som starter med et gitt objekt. Senere kan du hente frem objektet igjen ved å bruke Marshal::load .

Her følger et kort eksempel. Vi har en klasse Chord som holder en samling av musikknoter. Vi ønsker å lagre en spesielt fin akkord slik at våre barnebarn kan laste den inn i Ruby versjon 23.5 og nyte den, de også. La oss starte med klassene til Note og Chord.

class Note
  attr :value
  def initialize(val)
    @value = val
  end
  def to_s
    @value.to_s
  end
end

class Chord   def initialize(arr)     @arr = arr   end   def play     @arr.join('-')   end end

Vi vil nå lage vårt mesterverk og benytte Marshal::dump for å lagre en serialisert versjon av den til disk.

c = Chord.new( [ Note.new("G"),  Note.new("Bb"),
                 Note.new("Db"), Note.new("E") ] )

File.open("posterity", "w+") do |f|   Marshal.dump(c, f) end

Til sist laster våre barnebarn den inn og svever til et annet sted, transportert av vårt underverks nydelighet.

File.open("posterity") do |f|
  chord = Marshal.load(f)
end
chord.play » "G-Bb-Db-E"

Egendefinert serialiseringsstrategi

Ikke alle objekter kan serialiseres og dumpes: bindinger, prosedyre-objekter, instanser av klassen IO og singleton-objekter kan ikke lagres utenfor det kjørende Ruby-miljøet (et TypeError-unntak heves dersom du forsøker). Men selv om ditt objekt ikke inneholder en av disse problematiske objekttypene kan du ha behov for å ta kontroll over serialiseringsprosessen selv.

Marshal tilbyr de koblingene du trenger. I objektene som krever egendefinert serialisering implementerer du bare to metoder: en instansmetode kalt _dump, som skriver objektet ut til en streng, og en klassemetode kalt _load, som leser en streng du har laget tidligere og omformer den til et nytt objekt.

Til eksempel følger en klasse som definerer sin egen serialisering. Av en eller annen grunn vil Special ikke lagre en av sine interne datamedlemmer, ``@volatile''.

class Special
  def initialize(valuable)
    @valuable = valuable
    @volatile = "Goodbye"
  end

  def _dump(depth)     @valuable.to_str   end

  def Special._load(str)     result = Special.new(str);   end

  def to_s     "#{@valuable} and #{@volatile}"   end end

a = Special.new("Hello, World") data = Marshal.dump(a) obj = Marshal.load(data) puts obj
produserer:
Hello, World and Goodbye

For ytterlige detaljer se i referanseseksjonen om Marshal som starter på side 428.

Distributert Ruby

Siden vi kan serialisere et eller flere objekter inn i en form som kan lagres utenfor prosessen vår, kan vi også overføre objekter fra en prosess til en annen. Kombinert denne muligheten med potensialet til nettverket og voilà: du har et distribuert objektsystem. For å spare deg fra strevet med å skrive koden, foreslår vi at du laster ned Masatoshi Seki sitt Distributed Ruby bibliotek (drb) fra RAA.

Ved hjelp av drb kan en Ruby-prosess oppføre seg som en server, en klient eller begge deler. En drb-server fortoner seg som en kilde av objekter, mens en klient bruker disse objektene. For klienten sin del ser det ut som objektene er lokale, men i virkeligheten blir koden fremdeles utført på serveren.

En server starter en tjeneste ved å assosiere et objekt med en gitt port. Tråder blir laget internt for å håndtere innkommende forespørsler på den porten, så husk å kalle join på drb-tråden før du avslutter programmet ditt.

require 'drb'

class TestServer   def doit     "Hello, Distributed World"   end end

aServerObject = TestServer.new DRb.start_service('druby://localhost:9000', aServerObject) DRb.thread.join # Vennligst ikke avslutt ennå!

En simpel drb-klient lager ganske enkelt et lokalt drb-objekt og assosierer det med objektet på den fjerne serveren; lokal-objektet er en proxy.

require 'drb'
DRb.start_service()
obj = DRbObject.new(nil, 'druby://localhost:9000')
# Now use obj
p obj.doit

Klienten kobler seg til serveren og kaller metoden doit, som returnerer en streng som klienten printer ut:

"Hello, Distributed World"

Det initielle nil-argumentet til DRbObject indikerer at vi vil knyttes til et nytt distribuert objekt. Vi kunne også bruke et eksisterende objekt.

Du kan humre over det. Det lyder som Javas RMI, eller CORBA, eller hva som helst. Ja, det er en funksjonell distribuert objekt-mekanisme---men det er skrevet i kun 200 linjer med Ruby-kode. Ingen C, ikke noe fancy, bare rett-fram Ruby-kode. Selvfølgelig, det er ikke noen navngivings- eller utvekslingstjeneste, eller noe annet du kan se i CORBA, men det er enkelt og forholdsvis raskt. På vårt 233 MHz test-system, kjørte denne eksempel-koden på ca. 50 fjernbeskjed-kall pr. sekund.

Og hvis du liker Suns JavaSpaces, basisen for deres JINI-arkitektur, vil du være interessert i å vite at drb er distribuert med en kort modul som gjør omtrent det samme. JavaSpaces er basert på en teknologi som heter Linda. For å bevise at den japanske forfatteren av modulen har humoristisk sans, kalles Rubys versjon "rinda."

Kompileringstid? Kjøretid? Hvilken som helst tid!

Den viktige tingen å huske ved Ruby er at det er ikke en stor forskjell mellom "kompileringstid" og "kjøretid". Det er helt det samme. Du kan legge til kode til en kjørende prosess. Du kan redefinere metoder underveis, forandre tilgang fra public til private, og så videre. Du kan til og med forandre grunnleggende typer, slik som Class og Object.

Med en gang du blir vant til denne fleksibiliteten er den hardt å gå tilbake til et statisk språk som C++, eller til og med et halv-statisk språk slik som Java.

Men, hvorfor skulle du ville det?

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.6 $ )
$Log: ospace.xml,v $
Revision 1.6  2003/07/01 11:40:46  kent
Gjort ferdig første kladd.


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.