Words and Code

One writer’s journey from words to code.

Taskmanaging Your App, Part 2: Service Objects


This blog post is part of a series on service objects. Read Part 1 here.

Everything seems to perform a service these days. We’ve got infrastructure as a service, platforms as a service, and even software as a service. But the servicing doesn’t end there: even our software applications often need specific services provided to them. But how, exactly?

Well, if you’ve ever opened up a Rails application and peeked inside of the main directories, you can get a good understanding of what exactly the application is doing. Pry into the models directory, and you’ll see the kinds of objects the app transforms and manipulates. Open the controllers directory, and you’ll see the different CRUD (create, read, update, delete) operations that are permitted by the application, and the various ways of handling requests and responses by the server.

Seems pretty straightforward, right? Except until your application starts to swell in size, and then you’re packing a ton of functionality into these two directories. We at least try and keep our models fat, and our controllers skinny. It would be great if just our trying to do that was successful all the time. Yet that’s not usually what happens. Instead, things start to get messy, code starts to leak out all over the place, and we all just want to give up and go home. But we don’t have to give up just yet! There’s one trick that we haven’t tried yet, and it’s guaranteed to make our lives easier: utilizing service objects. Or, in other words, servicing parts of our application and separating our code out into more appropriate places.

Separating All Dem Concerns

Last week, we started off by learning about rake tasks, which turned out be an awesome way of encapsulating a specific type of functionality into a single file. But when we started writing a rake data:stage task (which stages our database with some sample Book objects), we noticed that we were making a single rake task responsible for multiple things.

We’ve got a similar dilemma on our hands again this week. We have a WishLists Controller, which should be responsible for rendering all of the Wish Lists associated with a User. But here’s the kicker: it should work with the Goodreads API to pull in the books that the User wants to read, and add that to their Wish List. Here’s our current, clean and beautiful controller:

1
2
3
4
5
6
7
8
9
class WishListsController < ApplicationController
  def index
      if user_signed_in?
          render json: current_user.wish_lists
      else
          render json: { wish_lists: [] }
      end
  end
end

It really pains me to ruin this controller with all the logic we’re about to throw in there. Let’s start writing it and see how it might look…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WishListsController < ApplicationController
  def index
      if user_signed_in?
          client = Goodreads::Client.new(api_key: 'OUR_API_KEY',
          api_secret: 'OUR_SECRET_TOKEN')
          shelf = client.shelf(current_user.id, 'to-read')
          books = shelf.books.each do |book|
              # Creates an ActiveRecord instance
              # for all the books on the shelf 
              # and returns a JSON object.
          end

          render json: books
      else
          render json: { wish_lists: [] }
      end
  end
end

NOPE. Nope nope nope. This is already way too much for a single controller action. And all the requests between our application and the Goodreads API really shouldn’t be up publicly available – they need to be private methods. And what if something goes wrong during those requests? We aren’t handling those errors at all! And honestly, it really shouldn’t be the index action’s job to send a request, handle the response, create and persist book objects, and then, on top of all of that, render the books of the Wish List! This is not the right path.


What we really need to do is separate our concerns. Or, in other words, we need to divide and conquer our code. We know that the index action of our WishLists Controller shouldn’t be responsible for all this work. So, let’s delegate that task to someone else. In fact, let’s create an object who’s sole concern and single responsibility is going to be dealing importing books from the Goodreads API, persisting them to the database, and then returning an array of books to read in the form of a wish list.

This object doesn’t need to do anything more than that. It’s existence is purely to help us – and help the rest of our application – out. This object is just going to provide a service. In fact, we might even go so far as to call it a service object (see what I did there?).

Servicing Our Application

So, if this service object doesn’t have any other responsibilities except for getting, creating, and persisting books to a wish_list, we have to ask ourselves: do we really need this object to stay around? Well, not really, no. All we really want is for this object to show up when we need it to, do it’s job, and then conveniently disappear. This means that we don’t need to make it an ActiveRecord object; instead, let’s make it a Plain Old Ruby Object (remember those?).

We can start by either creating a /services directory on the top level and saving our goodreads_importer.rb service in there, or we can namespace it inside of our /models directory as /goodreads/importer.rb. Once we’ve made our file, we can pretty much use the same code we started writing before, and abstract it out into this service object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GoodreadsImporter
  def initialize(options = {})
      @options = options
  end

  private
  def client
      @client ||= Goodreads::Client.new(api_key:
      'OUR_API_KEY', api_secret: 'OUR_SECRET_TOKEN')
    end

    def shelf(id, shelf_name)
      @shelf ||= client.shelf(current_user.id, shelf_name)
    end

    def books
      @books ||= shelf.books.each do |book|
          # Creates an ActiveRecord instance
          # for all the books on the shelf 
          # and returns a JSON object.
      end
    end
end

So far, we’re just initializing our Importer with some options. Notice that we’re not inheriting from ActiveRecord::Base! This is just a plain Ruby class, like the ones we used to make when we were first learning about things like inheritance.

We’ve also abstracted out those API calls (Client.new) and the methods provided to us by the Goodreads API (client.shelf, shelf.books) into private methods that are only going to be accessible by this service object. Not only is this important because we don’t want any other person or part of this application to be able to access these endpoints, but this is also going to be super helpful to us down the road. Now, if something goes wrong, we can narrow down exactly where in the process our application broke. And, we can write tests for specific parts of this service object, just to double and triple check that everything is working exactly as we expect it to.

Next, we’ll want to actually have some point of entry for this object. In other words, we need a way to actually use this object – some way to access all those private methods that we wrote just above. A pretty cool convention for a service object’s point of entry is a call method. The idea here is that you “call” upon the service object to show up, perform its job, and then don’t worry about it after it’s done.

This is what ours might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class GoodreadsImporter
  def initialize(options = {})
      @options = options
  end

  def call
      books.each do |b|
          book = Book.where(id: b[:id]).first_or_initialize
          book.update(b)
      end
  end

  private
  def client
      @client ||= Goodreads::Client.new(api_key:
      'OUR_API_KEY', api_secret: 'OUR_SECRET_TOKEN')
    end

    def shelf(id, shelf_name)
      @shelf ||= client.shelf(current_user.id, shelf_name)
    end

    def books
      @books ||= shelf.books.each do |book|
          # Creates an ActiveRecord instance
          # for all the books on the shelf 
          # and returns a JSON object.
      end
    end
end

Pretty simple, right? Okay, so now we have this fantastic little service object. But it doesn’t actually exist in our application yet. At no point are we “calling” upon our service! Time to fix that.

Objects To Make Our Lives Easier

Now that we have a service that we can rely upon, all we need to do is plug it into our application. We could do this inside of our WishLists Controller, just as we originally planned:

1
2
3
4
5
6
7
8
9
10
class WishListsController < ApplicationController
  def index
      if user_signed_in?
          render json: current_user.wish_lists
          if GoodreadsImporter.new.call(current_user.id, 'to-read')
      else
          render json: { wish_lists: [] }
      end
  end
end

Or we could also stick it into the rake task we were writing last week:

1
2
3
4
5
6
7
namespace :data do
  desc "Stages the environment with data from Goodreads."
  task "goodreads:import" => "environment" do
    options = {}
    GoodreadsImporter.new(options).call
  end
end

And now in development, we can just run rake data:goodreads:import to call upon our service object. Writing tests for this would be super easy as well, since we’d literally only have to test this one Ruby class to make sure that the service object wasn’t broken.

The coolest thing about service objects is that they exist on their own. They don’t really need to know about what’s going on around them (or even that they exist in the context of a giant framework called Rails!) You can just rely on pure Ruby code to make them work, and they’ll do whatever they’re supposed to do, whenever you need them to do it. I really liked this blog post’s explanation of how they should work:

Rails has multiple entry points. Every controller action in Rails is the entry point! Additionally, it handles a lot of responsibilities (parsing user input, routing logic [like redirects], logging, rendering… ouch!). That’s where service objects comes to play. Service objects encapsulates single process of our business. They take all collaborators (database, logging, external adapters like Facebook, user parameters) and performs a given process. Services belongs to our domain - They shouldn’t know they’re within Rails or web app!

Even though it might seem like a lot of extra work in the moment, service objects can save you so much time and pain in the long run. And if you think about it, just like we need software as a service, our own code sometimes needs a service to do things for it. Cut your code some slack and help it out by creating a service object. You’ll probably thank yourself down the road.

tl;dr?

  • Service objects are POROs that you can use to encapsulate a specific piece of functionality, and can help you separate concerns in your application.
  • Still curious about the theory behind service objects in Rails applications? Check out this tutorial and this super helpful post, which unpacks service objects in the context of refactoring.
  • Where else can you use a service object? Well, a lot of places! This blog post has a ton of examples of how to use them throughout your application.