Skip to content

Instantly share code, notes, and snippets.

@geeksam
Created May 4, 2012 19:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geeksam/2597267 to your computer and use it in GitHub Desktop.
Save geeksam/2597267 to your computer and use it in GitHub Desktop.

Rubyists! In the process of doing an "extract method object" refactoring, I wound up with a service object that calls several other (inner) service objects. A simplified version is below.

Note that (a) I'm using a simple Result object to report success/failure back to the calling code, and (b) I'm passing the same Result instance around to subservices (to keep track of overall process state).

My question is this: in a multistep process, how should I handle failure in an intermediate step?

The options I can think of are:

  1. As in the example below (do_phase_3 unless @result.failure?), explicitly check for failure in a substep, and abort manually.
  2. Raise an exception, which gets caught in the outer (class-level) .perform call, which then returns the result object as usual.
  3. Use throw/catch to accomplish the same effect as #2, above.

Am I missing other options? Which one would be best?

class Service
def self.perform
instance = new
instance.perform
return instance.result
end
attr_reader :result
def initialize(result = nil)
@result = result || Result.new
end
class Result
attr_reader :status
attr_accessor :status_message
def initialize
@status = nil
end
def success!; @status = :success; end
def failure!; @status = :failure; end
def success?; status == :success; end
def failure?; status == :failure; end
end
end
class MethodObject < Service
def perform!
do_phase_1
do_phase_2 if some_other_condition? # Note: might report failure
do_phase_3 unless @result.failure?
end
def do_phase_1; Phase1.perform(@result); end
def do_phase_2; Phase2.perform(@result); end
def do_phase_3; Phase3.perform(@result); end
class Phase1 < Service
def perform!
# do some stuff
end
end
class Phase2 < Service
def perform!
# do some stuff
if something_horrible_happened
@result.failure!
@result.status_message = 'OMGWTFBBQ'
end
end
end
class Phase3 < Service
def perform!
# do some stuff
end
end
end
@avdi
Copy link

avdi commented May 4, 2012

Highly dependent on context. I actually wrote a whole framework once to raise the level of abstraction on stuff like this where failures in different steps might need to be handled differently. Sadly, it is not really documented. https://github.com/avdi/methodical

@geeksam
Copy link
Author

geeksam commented May 4, 2012

Thanks, will have a look when I get done with this meeting (=

@geeksam
Copy link
Author

geeksam commented May 4, 2012

@avdi: wow, that's... more than slightly insane. I suspect I'll need a usage example before deciding whether I mean "insane" as a compliment. ;>

@geeksam
Copy link
Author

geeksam commented May 4, 2012

Following up on a tweet from @JEG2, I did a quick implementation of throw/catch. It does feel better than raising an exception, but doing something like throw(:instafail) and catching it in Service.perform doesn't work.

I'll admit to a few minutes of head-scratching, but eventually I realized that there were two catch blocks on the stack, and the throw was terminating the child service (Phase[123], above), and the parent service was happily proceeding there.

I can think of a variety of ways to work around that (and @davetron5000 had two more), but they all suffer from excessive cleverness. In combination with the fact that both exceptions and throw/catch are a kind of "action at a distance", I think I have my answer: leave it alone.

Thanks to all (including several people at LivingSocial) for the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment