Reputation: 311634
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
Reputation: 84393
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
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
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