Slim down models and controllers using Rails Service Objects
Tags: rails, optimization, refactoring
Published: 2021-04-27 02:26:21

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0b75z7ctceyh5k4jhk1o.jpg)<figcaption>Designed by tartila / Freepik</figcaption>
"Fat models, skinny controllers" is one of the best practices for refactoring Rails applications. As an application grows larger and becomes more complex, however, there will be more business logic in models and it gets harder to apply the single-responsibility principle. This is when we should consider extracting the business logic into Service Object to achieve "Skinny models, skinny controllers".

#What is a Service Object?#
Remember encapsulation in OOP? A service object in Rails is basically encapsulating business logic as a Ruby object which is designed to execute one single action. A service object can be called from anywhere, and it is also easy to test. Consider using it when:

* the action is complex
* the action is called in multiple models
* the action interacts with an external service/API

#Extract a service object#
Let's see it in action. I have created a Rails application that generates hashtags using [Google Cloud Vision API](https://cloud.google.com/vision) when a user posts an image. I am using the sample code I introduced in [the previous post](https://dev.to/junko911/sample-code-to-use-cloud-vision-api-with-ruby-18f7).

I have `get_tags` method in the `Post` model:
```ruby
# app/models/post.rb

class Post < ApplicationRecord
  def get_tags(image)
    api_key = ENV['GOOGLE_API_KEY']
    api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{api_key}"
    base64_image = Base64.strict_encode64(File.new(image, 'rb').read)

    body = {
      requests: [{
        image: {
          content: base64_image
        },
        features: [
          {
            type: 'LABEL_DETECTION',
            maxResults: 5
          }
        ]
      }]
    }.to_json

    uri = URI.parse(api_url)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    request = Net::HTTP::Post.new(uri.request_uri)
    request['Content-Type'] = 'application/json'
    response = https.request(request, body)
    results = JSON.parse(response.body)
    results['responses'][0]['labelAnnotations'].each do |result|
      tag = Tag.find_or_create_by(name: result['description'])
      PostTag.create(post_id: id, tag_id: tag.id)
    end
  end
end
```
The model knows too much about how to tag the images. Let's refactor it extracting the action as a service object.

First, create `app/services` folder and `tag_generator.rb` below it. Move all the logic from `get_tags` to a class `.call` method:
```ruby
# app/services/tag_generator.rb

class TagGenerator
  def self.call(post, image)
    api_key = ENV['GOOGLE_API_KEY']
    api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{api_key}"
    base64_image = Base64.strict_encode64(File.new(image, 'rb').read)

    body = {
      requests: [{
        image: {
          content: base64_image
        },
        features: [
          {
            type: 'LABEL_DETECTION',
            maxResults: 5
          }
        ]
      }]
    }.to_json

    uri = URI.parse(api_url)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    request = Net::HTTP::Post.new(uri.request_uri)
    request['Content-Type'] = 'application/json'
    response = https.request(request, body)
    results = JSON.parse(response.body)
    results['responses'][0]['labelAnnotations'].each do |result|
      tag = Tag.find_or_create_by(name: result['description'])
      PostTag.create(post_id: post.id, tag_id: tag.id)
    end
  end
end
```
Now it is a class method of the `TagGenerator` class. Let's update `post_controller.rb` as well.

Before:
```ruby
# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def create
    @post = current_user.posts.new(post_params)

    if @post.save
      @post.get_tags(params[:post][:image])
      redirect_to @post
    else
      render :new
    end
  end
```
After:
```ruby
# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def create
    @post = current_user.posts.new(post_params)

    if @post.save
      TagGenerator.call(@post, params[:post][:image])
      redirect_to @post
    else
      render :new
    end
  end
```
Now we didn't just downsize the `Post` model but also made the tag generator action maintainable and reusable. 

This is the result of executing `TagGenerator.call`. The auto-generated hashtags from the image:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ye6dgx1rzpbupf27n11w.jpg)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uc70fljwhp0rv5kpk0ur.png)

***
Reference:
[7 Patterns to Refactor Fat ActiveRecord Models](https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/)