Over the years, I’ve come at the idea of using POROs (Plain Old Ruby Objects) in different ways in Rails. In a recent work project, we are using the concept of a “service object” to implement procedures as POROs. I’ve come to like the way we are putting them together, using things we’ve learned along the way, with input from other folks advancing these ideas, including Sandi Metz and Avdi Grim.

This means of organizing code removes methods, callbacks, and lines of code from Controllers and Models, putting them in a place it is much simpler to write and express what is happening, and subsequently much easier to test, maintain, and extend.

Note: The methods employed here work well with Ruby 2.3.0 and Rails 4.2.5 which I’m currently using. If you’re using earlier (or even later) versions, you may need to adjust things accordingly.

Create a class for the service under app/services

Example: app/services/build_new_product.rb:

class BuildNewProduct
  # ...
end

Create the main method based on the verb of the service

In the above example the main method would be called build:

class BuildNewProduct
  def build
    # ...
  end
end

Use the initialize method to set up the operation

In the case above, a new product would be build based on the combining information from a warehouse product and information from a third-party product description service.

class BuildNewProduct
  attr_attribute :warehouse_product, :product_specifications, :product

  def initialize(warehouse_product, product_specifications, options={})
    self.warehouse_product = warehouse_product
    self.product_specifications = product_specifications
    self.product = Product.new
  end

  def build
    self.product.
      assign_attributes(
        warehouse_product.attributes.except("id", "created_at", "updated_at")
	  )
    self.product.
      assign_attributes(
        product_specifications.attributes.except("id", "created_at", "updated_at")
      )
    self.product
  end
end

Invoking the service object

The invocation is simply:

product = BuildNewProduct.new(warehouse_product, product_specifications).build

More service object actions

Suppose I want to be able to not only build, but also save the product with the BuildNewProduct service object? While it’s simple to call .save or .save! on the returned (non-persisted) object, maybe it improves the readability at the point I’m using it. (Yes, I’m contriving the example a bit.)

In this example, it would be trivial to add two more calls:

  def create
    build.save
  end

  def create!
    build.save!
  end

This would be preferable, I think, to adding another service object called CreateNewProduct that basically did the same thing using the BuildNewProduct service object.

Why do it this way?

Using POROs for service objects makes testing the logic of the operation in isolation much easier than if the logic was embedded in a Controller or Model within the application. It’s easier to isolate other functions, modules, and framework elements from the code under test.

In general using POROs this way removes complexity from Controllers and Models (especially ActiveRecord Models) where there would otherwise be a growing pile of model class and instance methods, callbacks, validations, and so on.

This way of creating service objects provides a standard way of implementation, sure, but why not just a direct class method, or a module with module methods?

When I first began looking at service objects, it seemed the standard form was to create a module using nothing but module methods. Later refinements I found led to using the module’s singleton class, but I’m afraid I don’t quite get what all that means.

The primary advantage I can see for instantiating the object is that it stays within the usual notion of a Ruby object (an instance of a Class). This also allows other notions such as Composition to construct the object, thus allowing run time injection.

For example, while the above service object is really simple, suppose I needed to gather information from a few different places and the assembly required additional components and operations.

In the above example, one might not care to make any such substitutions. Looking at the service object, there is a piece of it that could be injected: the Product class could be replaced by something else and while for this particular example seems unnecessary, let’s just see what it might be like.

Injecting class of build object

  def initialize(warehouse_product, product_specifications, options={})
    self.warehouse_product = warehouse_product
    self.product_specifications = product_specifications
	product_klass = options.fetch(:product_klass) { Product }
    self.product = product_klass.new
  end

and I could substitute another product class as:

BuildNewProduct.new(gprod, eprod, product_klass: MyProduct)

However, I would need to ensure that the MyProduct class could respond to all the calls done to self.product in the service object. There are distinct pitfalls to doing this, and using Mocks in general.

Again, I wouldn’t recommend injecting a Mock in this particular situation.

In her famous talk, “Nothing is Something”, Sandi Metz runs through a great example of using injection to organize code. Her example also uses POROs, and I think it is a great study in organizing code.

Using this inside Rails service objects extends the elegance of this approach quite a lot, I think.

Using options to inject components

In some of our project’s ETL (Extract, Transform, Load) Runners, we go off to the network and fetch some data. While there are things like VCR and WebMock available for testing, I wanted to have something that would allow in-situ substitution should it prove necessary. In some cases, for example, I wanted to be able to execute the runner to gather the pristine responses for other uses, including load testing and building a working development database for other aspects of development.

Here is a somewhat redacted skeleton of one of the runners that fetches product specification information from a third party.

class Etl::Runner::ProductSpecificationFetch < Etl::Runner::Base
  DEFAULT_APP_ID = Rails.application.secrets.etl_default_app_id

  attr_accessor :datafilename, :mfr_datafilename, :product_spec_client, :app_id

  def initialize(*args)
    options = args.extract_options!
    super(*args)
    self.datafilename = options.fetch(:products_filename, default_products_filename)
    self.mfr_datafilename = options.fetch(:manufacturers_filename, default_manufacturers_filename)
    self.app_id = options.fetch(:app_id, DEFAULT_APP_ID)
    self.product_spec_client = options.fetch(:product_spec_client, default_product_spec_client)
  end

  def run
    # ... lots of other code ...
  end

  def default_product_spec_client
    ProductSpecificationClient.new(app_id: app_id)
  end

  def default_products_filename
    File.join(data_dir, Etl::Runner::DATA_FILES[:product_specifications])
  end

  def default_manufacturers_filename
    File.join(data_dir, Etl::Runner::DATA_FILES[:manufacturers])
  end
end

In this example, I can provide an alternate service object to implement the client that talks to third party, and different means of obtaining the two output files for this service object. This is one I used that saved the responses from the service into a file.

require "product_specification_client"
class SavingProductSpecificationClient < ProductSpecificationClient

  attr_accessor :save_dir

  def initialize(app_id: "", save_dir: ".")
    super(app_id: app_id)
    self.save_dir = save_dir
    FileUtils.mkdir_p(self.save_dir)
  end

  protected

  def get(method, parameters={})
    super(method, parameters).tap do |response|
      File.write(File.join(@save_dir, "#{method}.#{slugify(parameters)}.xml"), response)
    end
  end

  def slugify(parameters={})
    # some code that converts a hash into a slug for a file name fragment
  end

end

By using this alternate version, I could build a rake task to call the runner shown above, injecting the above client, and save all the raw XML responses.

In this particular case, I chose to inherit from the client because of the way the client handles the actual method calls using missing_method, requiring less code here.

Admittedly there is a lot more to this application that the snippet of code above, but it should serve as an example of the sort of thing one can do to create and use a service object.