Controller is an entry point to our application. However, it’s not the only possible entry point. I would like to have my logic accessible from:
If I throw my logic into a controller it won’t be accessible from all these places. So let’s try “skinny controller, fat model” approach and move the logic to a model. But which one? If a given piece of logic involves
Product models – where should it live?
A class which inherits from
ActiveRecord::Base already has a lot of responsibilities. It handles query interface, associations and validations. If you add even more code to your model it will quickly become an unmaintainable mess with hundreds of public methods.
A service is just a regular Ruby object. Its class does not have to inherit from any specific class. Its name is a verb phrase, for example
CreateUserAccount rather than
UserCreationService. It lives in app/services directory. You have to create this directory by yourself, but Rails will autoload classes inside for you.
A service object does one thing
A service object (aka method object) performs one action. It holds the business logic to perform that action. Here is an example:
# app/services/accept_invite.rb class AcceptInvite def self.call(invite, user) invite.accept!(user) UserMailer.invite_accepted(invite).deliver end end
The three conventions I follow are:
Services go under the
app/services directory. I encourage you to use subdirectories for business logic-heavy domains. For instance:
callmethod. I found using another verb makes it a bit redundant:
ApproveTransaction.approve()does not read well. Also, the
callmethod is the de facto method for
procs, and method objects.
Service objects show what my application does
I can just glance over the services directory to see what my application does:
A quick look into a service object and I know what business logic is involved. I don’t have to go through the controllers,
ActiveRecord model callbacks and observers to understand what “approving a transaction” involves.
Clean-up models and controllers
Controllers turn the request (params, session, cookies) into arguments, pass them down to the service and redirect or render according to the service response.
class InviteController < ApplicationController def accept invite = Invite.find_by_token!(params[:token]) if AcceptInvite.call(invite, current_user) redirect_to invite.item, notice: "Welcome!" else redirect_to '/', alert: "Oopsy!" end end end
Models only deal with associations, scopes, validations and persistence.
class Invite < ActiveRecord::Base def accept!(user, time=Time.now) update_attributes!( accepted_by_user_id: user.id, accepted_at: time ) end end
This makes models and controllers much easier to test and maintain!
When to use Service Class
Reach for Service Objects when an action meets one or more of these criteria: