HiveBrain v1.2.0
Get Started
← Back to all entries
snippetrubyMinor

Triggerful Ruby Gem: create dynamic callbacks to methods

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
createrubymethodstriggerfuldynamiccallbacksgem

Problem

The main purpose of Ruby is to be readable. I hope I did a good job with this gem I made. If there's any kind of suggestion of how to make this better, then please tell me.

```
class Trigger
def initialize event, *callbacks
@callbacks = callbacks
@event = event

if @callbacks[0].is_a? TrueClass
@progression = true
@callbacks.delete_at(0)
elsif @callbacks[0].is_a? FalseClass
@progression = false
@callbacks.delete_at(0)
else
@progression = false
end
end

def trigger(*args)
case @event
when Proc
event_data = @event.call
when Method
event_data = @event.call
else
event_data = self.method(@event).call(*args)
end
@callbacks.each do |callback|
if callback.instance_of? Trigger
if @progression
callback.trigger(*args, event_data)
else
callback.trigger(*args)
end
else
case callback
when Proc
if @progression
callback.call(*args, event_data)
else
callback.call(*args)
end
when Method
if @progression
callback.call(*args, event_data)
else
callback.call(*args)
end
else
if @progression
method(callback).call(*args, event_data)
else
method(callback).call(*args)
end
end
end
end
end

#triggers the callbacks without executing the original method
def silent_trigger(*args)
@callbacks.each do |callback|
if callback.instance_of? Trigger
callback.trigger(*args)
else
case callback
when Proc
callback.call
when Method
callback.call
else
method(callback).call(*args)
end
end
end
end

# add callback(s) to instance
def add(*callbacks)
@callbacks.concat callbacks
end

def insert(index, *callbacks)
@callbacks.inse

Solution

1) get rid of conditionals

I think your main problem here is the nested conditionals that litter your code. This increases complexity and tends to be less readable.

You can get rid of those conditionals using a method like this :

def make_callable(object)
  case object
  when Proc, Method then object
  when Trigger      then ->(*args){ object.trigger(*args) }
  else                   ->(*args){ public_send object, *args }
  end
end


so you can do things like :

@event = make_callable(event)
@callbacks = callbacks.map{ |c| make_callable c }


This way, all your callbacks will respond to call uniformly, so you won't need conditionals anymore.

2) use inheritance

As i see it, your @progression instance variable masks the need for two different behaviors, which means two different classes : a "silent" trigger, and a "verbose" one that extends the former.

class Trigger

  # factory method to instantiate the right type of callback.
  # I slightly changed the signature from the original #initialize
  # as I thought it would make more sense this way, 
  # but it is possible to keep the original one with minor tweaks
  #
  def self.factory(verbose, event, *callbacks)
    verbose ? Verbose.new(event, *callbacks) : new(event, *callbacks)
  end

  def initialize(event, *callbacks)
    @event = make_callable(event)
    @callbacks = callbacks.map{ |c| make_callable c }
  end

  # SNIP : this class would also expose add_callback, remove_callback, etc.

  def trigger(*args)
    @callbacks.each{ |c| c.call(*args) }
  end

  private 

  def make_callable(object)
    case object
    when Proc, Method then object
    when Trigger      then ->(*args){ object.trigger(*args) }
    else                   ->(*args){ public_send object, *args }
    end
  end
end

class Trigger::Verbose < Trigger
  def trigger(*args)
    event_data = @event.call(*args)
    super(*args, event_data)
  end
end


As you can see, this simplifies the logic a lot, and makes clear that we have two different behaviors, which is invaluable for consumers of your API.

Code Snippets

def make_callable(object)
  case object
  when Proc, Method then object
  when Trigger      then ->(*args){ object.trigger(*args) }
  else                   ->(*args){ public_send object, *args }
  end
end
@event = make_callable(event)
@callbacks = callbacks.map{ |c| make_callable c }
class Trigger

  # factory method to instantiate the right type of callback.
  # I slightly changed the signature from the original #initialize
  # as I thought it would make more sense this way, 
  # but it is possible to keep the original one with minor tweaks
  #
  def self.factory(verbose, event, *callbacks)
    verbose ? Verbose.new(event, *callbacks) : new(event, *callbacks)
  end

  def initialize(event, *callbacks)
    @event = make_callable(event)
    @callbacks = callbacks.map{ |c| make_callable c }
  end

  # SNIP : this class would also expose add_callback, remove_callback, etc.

  def trigger(*args)
    @callbacks.each{ |c| c.call(*args) }
  end

  private 

  def make_callable(object)
    case object
    when Proc, Method then object
    when Trigger      then ->(*args){ object.trigger(*args) }
    else                   ->(*args){ public_send object, *args }
    end
  end
end

class Trigger::Verbose < Trigger
  def trigger(*args)
    event_data = @event.call(*args)
    super(*args, event_data)
  end
end

Context

StackExchange Code Review Q#32485, answer score: 4

Revisions (0)

No revisions yet.