Events i Ruby
tirsdag 9. mars 2010
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.