John Feminella
John Feminella

Reputation: 311634

Decorating an object's interface with additional behavior in Ruby?

I have a Ruby object that dispatches events to a third-party service:

class Dispatcher
  def track_event(e)
    ThirdPartyService.track e.id, e.name, my_api_key
  end
end

The use of ThirdPartyService may raise errors (e.g. if no network connection is available). It's usually appropriate for consumers of Dispatcher to decide how to deal with these, rather than Dispatcher itself.

How could we decorate the use of Dispatcher in such a way that objects appear to be using a Dispatcher, but all exceptions are caught and logged instead? That is, I want to be able to write:

obj.track_event(...)

but have exceptions be caught.

Upvotes: 0

Views: 70

Answers (3)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84393

Use a Bang Method

There's more than one way to do this. One way to accomplish your goal would be a small refactoring to redefine the exception-raising method as a "cautionary" method, make it private, and use the "safer" bang-free method in the public interface. For example:

class Dispatcher
  def track_event(e)
    result = track_event! e rescue nil
    result ? result : 'Exception handled here.'
  end

  private

  def track_event! e
    ThirdPartyService.track e.id, e.name, my_api_key
  end
end

Using the refactored code would yield the following when an exception is raised by ThirdPartyService:

Dispatcher.new.track_event 1
#=> "Exception handled here."

There are certainly other ways to address this type of problem. A lot depends on what your code is trying to express. As a result, your mileage may vary.

Upvotes: 0

Jordan Running
Jordan Running

Reputation: 106077

Thoughtbot has a great blog post on different ways to implement decorators in Ruby. This one ("Module + Extend + Super decorator") is especially succinct:

module ErrorLoggingMixin
  def track_event(event)
    super
  rescue ex
    Logger.warn(ex)
  end
end

obj = Dispatcher.new          # initialize a Dispatcher as usual
obj.extend(ErrorLoggingMixin) # extend it with the new behavior

obj.track_event(some_event)   # call its methods as usual

The blog post lists these pros and cons:

The benefits of this implementation are:

  • it delegates through all decorators
  • it has all of the original interface because it is the original object

The drawbacks of this implementation are:

  • can not use the same decorator more than once on the same object *difficult to tell which decorator added the functionality

I recommend reading the rest of the post to see the other implementations and make the choice that best fits your needs.

Upvotes: 2

John Feminella
John Feminella

Reputation: 311634

The closest idea I've got is to surround the usage of track_event in a method which provides a block that catches exceptions, like this:

module DispatcherHelper
  def self.dispatch(&block)
    dispatcher = Dispatcher.new
    begin
      yield dispatcher
    rescue NetworkError
      # ...
    rescue ThirdPartyError
      # ...
    end
  end
end

so that we can then do:

DispatcherHelper.dispatch { |d| d.track_event(...) }

The other alternative I can see is to mimic the signature of track_event, so that you get:

module DispatcherHelper
  def self.track_event(e)
    begin
      Dispatcher.new.track_event(e)
    rescue NetworkError
      # ...
    rescue ThirdPartyError
      # ...
    end
  end
end

but I'm less fond of that since it ties the signatures together.

Upvotes: 1

Related Questions