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.