Events i Ruby

Ruby har ikke events slik som vi er vandt med fra .Net. Det er derimot ikke særlig vanskelig å implementere noe som ligner. Jeg begynte å eksperimentere litt, og her følger en slags log over hva jeg har forsøkt. Jeg har ikke landet på noen “best practice”, og det er heller ikke noe rocket science her – men jeg tror følgende kodesnutter kan være insteressante, særlig om man ikke er så veldig erfaren med Ruby enda.

Iterasjon 1

Jeg ønsker å implementere en Counter-klasse. Den skal ha en metode som heter increment som jeg kan kalle x antall ganger, hvor x er en limit jeg setter. Når limiten er nådd vil jeg at Counter-objektet informerer meg om dette ved å fyre av et event. Her er koden for klassen, samt litt kode som viser hvordan den brukes:

1 class Counter
2   def initialize limit
3     @limit = limit
4     @count = 0
5   end
6   def on_limit_reached= delegate
7     @on_limit_reached_delegate = delegate
8   end
9   def increment
10     @count += 1
11     @on_limit_reached_delegate.call if @count == @limit
12   end
13 end
14
15 c = Counter.new(5) # create a new counter
16 c.on_limit_reached = lambda{ puts Limit reached }
17 4.times { c.increment } # count up to limit -1
18 puts Limit not reached yet
19 c.increment # one more time, limit is now reached
Output:
Limit not reached yet
Limit reached

Løsninger er altså at vi lager en closure i linje 16 (også kalt lambda, kodeblokk, anonym metode, etc) som man sender til on_limit_reached= metoden. Counter-klassen tar vare på en referanse til denne closuren. Når limiten er nådd eksekveres den så i linje 11.

Merk at jeg glemte å legge til en test for om delegaten er satt, så koden i linje 11 vil feile om den ikke har noen lyttere (lover å skjerpe meg).

Iterasjon 2

Løsningen i iterasjon 1 støtter bare én lytter – hvis flere legges til vil den bare erstatte den første. Jeg ønsker derfor å utvide Counter for dette, og denne gangen husket jeg å legge til en test for om det er noen som lytter før jeg “fyrer av eventet”:

1 class Counter
2   def initialize limit
3     @limit = limit
4     @count = 0
5   end
6   def on_limit_reached= delegate
7     @limit_reached_delegates ||= []
8     @limit_reached_delegates << delegate
9   end
10   def increment
11     @count += 1
12     if @count == @limit
13       @limit_reached_delegates.each {|d| d.call } if @limit_reached_delegates
14     end
15   end
16 end
17
18 c = Counter.new(5) # create a new counter
19 c.on_limit_reached = lambda{ puts I was informed about limit reached }
20 c.on_limit_reached = lambda{ puts I was also informed about limit reached }
21 4.times { c.increment } # count up to limit -1
22 puts Limit not reached yet
23 c.increment # one more time, limit is now reached
Output:
Limit not reached yet
I was informed about limit reached
I was also informed about limit reached

Counter har nå et array av delegater: @limit_reached_delegates. Når limiten er nådd kaller jeg alle sammen. Jeg fikk desverre ikke til å bruke += for å legge til eventer, noe som ville ha virket riktigere for C#-utviklere.

Iterasjon 3

Det er på tide å legge opp støtte for flere eventer; jeg ønsker nå å bli fortalt hver gang telleren inkrementeres, og legger derfor opp en on_increment= metode. Jeg vil også sende med verdien på counteren i eventet.

1 class Counter
2   def initialize limit
3     @count, @limit = 0, limit
4     @event_handlers = {}
5   end
6   def on_limit_reached= delegate
7     (@event_handlers[:limit_reached] ||= []) << delegate
8   end
9   def on_increment= delegate
10     (@event_handlers[:increment] ||= []) << delegate
11   end
12   def increment
13     @count += 1
14     @event_handlers[:increment].each {|d| d.call(@count) } if @event_handlers[:increment]
15     if @count == @limit
16       @event_handlers[:limit_reached].each {|d| d.call } if @event_handlers[:limit_reached]
17     end
18   end
19 end
20
21 c = Counter.new(5) # create a new counter
22 c.on_limit_reached = lambda{ puts Limit reached }
23 c.on_increment = lambda{|count| puts Counter was incremented to #{count} }
24 5.times { c.increment }
Output:
Counter was incremented to 1
Counter was incremented to 2
Counter was incremented to 3
Counter was incremented to 4
Counter was incremented to 5
Limit reached

Jeg har nå brukt en Hash(-tabell) til å holde rede på alle handlerne – denne opprettes i linje 4. Den litt hårete syntaksen på linje 7 og 10 legger inn en ny array for en gitt event-nøkkel om arrayet ikke finnes enda, før den legger delegaten til arrayet. Deretter kan jeg trigge increment-eventet hver gang increment kalles (linje 14). Legg merke til at jeg sender inn @count når jeg kaller delegaten, og kan derfor bruke den i closuren i linje 23.

Iterasjon 4

Det ble litt mye “bråk” i Counter-klassen for å holde rede på event-handlerene i iterasjon 3, og jeg forsøker derfor å trekke ut denne logikken (Single Responsibility Principle). Jeg lager en Ruby-modul som jeg kan mikse inn i Counter (vi kaller det en mixin, som er Ruby’s løsning på multippel arv, noe vi ikke har i .Net). Modulen har nå handler-hashen, og brukes også til å trigge eventene:

1 module Events
2   def add_handler event, delegate
3     @event_handlers ||= {}
4     (@event_handlers[event] ||= []) << delegate
5   end
6   def raise_event event, *args
7     @event_handlers[event].each {|d| d.call(*args) } if @event_handlers[event]
8   end
9 end
10
11 class Counter 
12   include Events
13   attr_reader :limit
14   def initialize limit
15     @count, @limit = 0, limit
16   end
17   def on_limit_reached= delegate
18     add_handler(:limit_reached, delegate)
19   end
20   def on_increment &delegate
21     add_handler(:increment, delegate)
22   end
23   def increment
24     @count += 1
25     raise_event(:increment, self, @count)
26     raise_event(:limit_reached) if @count == @limit  
27   end
28 end
29
30 c = Counter.new(5) # create a new counter
31 c.on_limit_reached = lambda{ puts Limit reached }
32 c.on_increment do |sender, count|
33   puts Counter was incremented to #{count}
34   puts #{sender.limit – count} left..
35 end
36 5.times { c.increment }
Output:
Counter was incremented to 1
4 left..
Counter was incremented to 2
3 left..
Counter was incremented to 3
2 left..
Counter was incremented to 4
1 left..
Counter was incremented to 5
0 left..
Limit reached

Jeg valgte også å endre litt på on_increment for å illustrere en annen måte å lage closures på, som nok er mere vanlig i Ruby. I stedet for å bruke lambda-metoden kan jeg nå lage en kodeblokk ved hjelp av ‘do’ og ‘end’ (do og end kan byttes ut med { og } om man foretrekker det). Jeg sender også med selve counter-objektet som et argument til handleren (‘self’ i linje 25 tilsvarer ‘this’ i C#), som jeg så kan bruke til å beregne hvor mange increments som gjenstår – fordi jeg har definert en reader for limit-variabelen (linje 13).

Iterasjon 5

En annen approch jeg ville teste ut var å opprette en generisk Event-klasse. Jeg droppet da Events-modulen, selv om jeg kunne ha brukt dem i kombinasjon. I dette eksempelet har jeg sneket inn litt dynamisk evaluering også – se om du skjønner hva jeg gjør på linje 17 og 32.

1 class Event
2   def initialize
3     @handlers = []
4   end
5   def raise *args
6     @handlers.each {|h| h.call(*args)}
7   end
8   def << handler
9     @handlers << handler
10   end
11 end
12
13 class Counter 
14   attr_reader :limit
15   def initialize limit
16     @count, @limit = 0, limit
17     events :limit_reached, :increment
18   end
19   def on_limit_reached= delegate
20     @limit_reached << delegate
21   end
22   def on_increment &delegate
23     @increment << delegate
24   end
25   def increment
26     @count += 1
27     @increment.raise(self, @count)
28     @limit_reached.raise if @count == @limit   
29   end
30   private
31   def events *attr
32     attr.each {|e| eval(@#{e} = Event.new)}
33   end
34 end
35
36 c = Counter.new(5) # create a new counter
37 c.on_limit_reached = lambda{ puts Limit reached }
38 c.on_increment do |sender, count|
39   puts Counter was incremented to #{count}
40   puts #{sender.limit – count} left..
41 end
42 5.times { c.increment }
(Output som i iterasjon 4)
Jeg håper dette fungerte som eksempler på hvordan man kan kode med events i Ruby, og at du lærte litt underveis. Spørsmål til koden mottas og besvares med største fornøyelse.

Kategorier: Ruby.
RSS feed for kommentarene. Tilbaketråkk.

6 kommentarer til “Events i Ruby”

  1. Odd Rune Says:

    Foretrekker ofte å bruke Observer jeg – http://ruby-doc.org/stdlib/libdoc/observer/rdoc/index.html

  2. Torbjørn Says:

    Jeg unnlot med vilje å nevne ruby’s implementasjon av Observable pattern, siden målet mitt var å demonstrere noe som minner mer om events slik man har i C#. De to vil nok egne seg til litt ulike senarier, men Observer er noe man absolutt bør kjenne til. Takk for kommentaren!

  3. Jonas Follesø Says:

    Tips er og å se hvordan man lytter på events i IronRuby, som jo allerede må ha støtte for dette for å kunne programmere mot .NET objekter. Jeg vet ikke hvordan den interne implementasjonen ser ut, men eksempler på hvordan lytte/slutte å lytte på events finner du på

    http://www.ironruby.net/Documentation/.NET/Events

    Her brukes kodeblokker framfor lambdas (som nok er mer Ruby, som du og skriver i itterasjon 4). De støtter og Proc objekter som kan brukes for å fjerne lyttere igjen.

    Gøy og se hvor mange forskjellige strategier man kan bruke på å løse samme problem. Første tanke er ikke alltid beste tanke, og løsningene blir bedre over flere iterasjoner :)

  4. Torbjørn Says:

    Er faktisk litt forvirret over de ulike strategiene man kan bruke for å lage closures i Ruby. Vi har lambda, som jeg stort sett brukte her, kodeblokker, proc og Proc.new. Og så kan man lage en closure av en metode (et Method objekt) ved å si obj.method(:method_name) – som blir som å delegere til en metode i C#.

    Foreløpig bruker jeg det jeg føler for der og da – det som ser best ut. Jeg vet det er nyanser i hvordan de ulike metodene faktisk fungerer, men glemmer hva det er like raskt som jeg leser det.

  5. Torbjørn Says:

    Skulle forresten ikke forundre meg om IronRuby – slik som mine egne eksperimenter – aksepterer et hvilket som helst object med en call-metode som en mulig eventhandler. Duck-typing er gøy :)

  6. Lispy C# (og hva er en closure) Says:

    [...] Vær obs på at closures ofte brukes feilaktig som et annet ord for anonyme funksjoner (jeg kan selv dokumentere å ha gjort det for ikke så lenge siden). Anonyme funksjoner kan danne closures (i mange språk), men det er likevel to separate begrep. [...]

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Bjørn Einar Bjartnes: Jeg har også latt meg fascinere av Clojure, uten at jeg har kommet så veldig l...

Bjørn Einar Bjartnes: Sweet :) Jeg tror egentlig jeg liker det som det er, med musikk. Litt av utford...

Mulig relaterte linker

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Template Method del 2: På vei mot funksjonell programmering
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (20)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta