20/07/2019, 09:56

Servicing in Rails

Rails follows a Model-View-Controller pattern. This creates questions around where programming logic should go once a Ruby on Rails application reaches a certain size. Generally, the principles are: Forget fat Models (don’t allow them to become bloated) Keep Views dumb (so don’t ...

Rails follows a Model-View-Controller pattern. This creates questions around where programming logic should go once a Ruby on Rails application reaches a certain size. Generally, the principles are:

  • Forget fat Models (don’t allow them to become bloated)
  • Keep Views dumb (so don’t put complex logic there)
  • And Controllers skinny (so don’t put too much there)

So where should you put anything that’s more than a simple query or action?

Service Object

One way to support Object-Oriented Design is with the Service Object. This can be a class or module in Ruby that performs an action. It can help take out logic from other areas of the MVC files. Trying this out for the first time in a Rails project, I started with a bloated controller that included the following method:

class SuggestionController < ApplicationController
  def create
    @topic_name = params[:topic_name]
    @suggestion_text = params[:suggestion_text]
    @suggestion = Suggestion.new topic: Topic.first(name: @topic_name), text: @suggestion_text
    if @suggestion.save
      flash[:notice] = 'Suggestion added!'
      render @suggestion
    else
      flash[:alert] = create_fail_error_message @suggestion
      redirect_to :new
    end
  end
end

Extracting this to a Service Object is relatively painless once you understand the design principles.

Module Service Object

I decided to go for the module approach, and used a build method to create a new service. This made it look very similar to a factory design pattern.

module SuggestionService
  class << self
    def create params
      topic_name = params[:topic_name]
      suggestion_text = params[:suggestion_text]
      topic = Topic.find_by name: topic_name
      Suggestion.new topic: topic, text: suggestion_text
    end
  end
end

It also made the controller’s code a lot more manageable.

class SuggestionController < ApplicationController
  def create
    @suggestion = SuggestionService.create params
    if @suggestion.save
      flash.notice = 'Suggestion added!'
      render @suggestion
    else
      flash.alert = create_fail_error_message @suggestion
      redirect_to :new
    end
  end
end

Class Service Object

Other flavours of Service Objects use classes instead of modules that might store some instance variables and have other methods to support the service. My code could be rewritten as:

class NewSuggestionService
  def initialize params
    @topic_name = params[:topic_name]
    @suggestion_text = params[:suggestion_text]
  end
  
  def call
    topic = Topic.find_by name: @topic_name
    Suggestion.new topic: topic, text: @suggestion_text
  end
end

And the code would then look slightly different in the controller.

class SuggestionController < ApplicationController
  def create
    @suggestion = NewSuggestionService.new(params).call
    if @suggestion.save
      flash[:notice] = 'Suggestion added!'
      render :show
    else
      flash.alert = create_fail_error_message @suggestion
      redirect_to :new
    end
  end
end

If you start using more and more Service Objects you might find you end up with a rapidly-expanding services folder. You can manage this growth by organising them into folders and modules.

REF: https://www.engineyard.com/blog/keeping-your-rails-controllers-dry-with-services

0