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