Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

validation and error-handling for service objects

I created a service object in Rails to work as an interface between our app and our API.

I got the idea from http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

Here is a small example:

class PackagesService
  def self.get_package(package_id)
    raise ArgumentError.new("package_id can't be nil") if package_id.blank?
    package = API::get "/packages/#{package_id}"
    package = JSON.parse package,
                          :symbolize_names => true unless package.blank?

  end
end

Is there any good pattern for handling validation and/or throwing errors for Service objects?

For validations:

  • I have to check all the inputs for nil or wrong type. Is there any way for easy validation? Maybe a rails extension?

For errors:

  • I could catch all API errors and then safely return a nil. But the programmer using the service object might not know the meaning of nil.
  • I could catch the API errors and raise another error which means extra effort to do this in all functions
  • Third option is leave it as it is and let the programmer handle all errors from API.

Let me know if you know any good pattern or if you have better ideas to interface an API.

like image 407
ieldanr Avatar asked Jun 21 '13 00:06

ieldanr


1 Answers

For simple cases (eg. with just one argument), then your check-and-raise with ArgumentError is fine. As soon as you start having complex cases (multiple arguments, objects, etc), I start leaning on Virtus and ActiveModel Validations.

Your linked article actually mentions these (see "Extract Form Objects"). I sometimes use something like these to construct service objects, eg.

require 'active_model'
require 'virtus'

class CreatePackage
  include Virtus
  include ActiveModel::Validations

  attribute :name, String
  attribute :author, String
  validates_presence_of :name, :author

  def create
    raise ArgumentError.new("Invalid package") unless self.valid?
    response = JSON.parse(
      API::post("/packages", self.attributes),
      :symbolize_names => true
    )
    Package.new(response)
  end
end

class Package
  include Virtus
  attribute :id, Integer
  attribute :name, String
  attribute :author, String
end

# eg.
service = CreatePackage.new(
  :name => "Tim's Tams",
  :author => "Tim",
)
service.valid? # true; if false, see service.errors
package = service.create

package.attributes
# => { :id => 123, :name => "Tim's Tams", :author => "Tim" }

In so far as exceptions, I'd leave them as-is for smaller actions (like this service class). I would wrap them if I'm writing something more substantial, though, such as an entire API client library.

I would never just return nil. Things like a network error, or a bad or unparseable response from the server both benefit from explicit errors.


Finally, there's a much heavier approach called use_case. Even if you don't use it, it has a bunch of ideas as to how to tackle service objects, validations and results that you might find interesting.

Edit: Also, check out Mutations. Like use_case, except simpler and less comprehensive.

like image 102
Rob Howard Avatar answered Oct 26 '22 14:10

Rob Howard