Continuing from my previous post, which introduced the idea of concerns to your models, in this post I will be discussing using services to keep your ActiveRecord’s responsibilities toned down.
In this short series I will be looking at three ways of reducing the responsibilities of your models:
Services, also known as Interactors, are a Rails pattern with the aim of keeping the logic in a controller clear and concise without passing off controller-like responsibility to the model. Another major reason for using this pattern is to increase the amount of code you can test in isolation without booting Rails as abhorred by DHH and taken to crazy lengths in Jim Weirich’s Decoupling from Rails talk.
So what do I mean by controller-like responsibility? Controllers are the glue between the rest of your application.
In the Rails world a controller is generally an interface for the CRUD operations of an ActiveRecord model. This involves handling the HTTP parameters of a request and using them to change data in an application, e.g. updating a user’s profile. When making such a change the controller must ensure the model was happy with the update made by checking for validity. It must then pass errors back to the user if any occurred, say if a required field was missing, so that the user can correct the problem. On successful updates a controller will redirect and set a success flash message.
Controllers are also used for sending mail using an ActionMailer class. Generally speaking this happens after another action of some kind, e.g. sending a welcome email after a user registers.
These are just some of the controller-like responsibilities I see in applications I work with. I’m sure there are many other things they are used for.
You may now be asking, what does this have to do with refactoring ActiveRecord models? Well in my experience you often see some of the responsibilities listed above leak their way into other parts of the application. Let’s look at two examples that illustrate this.
The first example looks at the need to create new Artists when creating a new Event for a gig listing site.
class Event < ActiveRecord::Base has_and_belongs_to_many :artists accepts_nested_attributes_for :artists end class Artist < ActiveRecord::Base has_and_belongs_to_many :events end
Okay, so ActiveRecord handles most of the logic for the relationship, you’d likely be calling
Event#create in your controller
class EventsController < ApplicationController def create @event = Event.create(params[:event]) if @event.valid? flash[:success] = 'Saved' redirect_to events_path else render :new end end end
Now things get hairy if we want to email each artist to say they’ve been added to an event. I often see this placed in the model:
class Events < ActiveRecord::Base has_and_belongs_to_many :events after_create :notify_artists def notify_artists artists.each do |artist| EventMailer.notify_artist(self, artist).deliver_later end end end
Your controller can stay the same. This isn’t so bad right? But what if you then need to add a delayed job to poll for the artist’s SoundCloud details?
class Events < ActiveRecord::Base # ... after_create :poll_soundcloud def poll_soundcloud artists.each do |artist| ArtistSoundcloudJob.perform_later(artist) end end end
Now the Events model has methods for sending welcome emails to artists and requesting jobs to be performed later. At first glance this might not seem too problematic. When you test the Events model however you will now need to stub or mock test the calls out to
ArtistSoundcloudJob. The responsibilities grow as we add more emails and jobs. You might send the event creator an email, or grab the tweets of the artists too.
You might alternatively like to keep this logic out of your models data layer and instead call within your controller.
class EventsController < ApplicationController def create @event = Event.create(params[:event]) if @event.valid? notify_artists poll_soundcloud flash[:success] = 'Saved' redirect_to events_path else render :new end end def notify_artists @event.artists.each do |artist| EventMailer.notify_artist(@event, artist).deliver end end def poll_soundcloud @event.artists.each do |artist| ArtistSoundcloudJob.perform_later(artist) end end end
The logic is now held by the controller. This can grow quite large too however. Think about when we add the email notification to the event creator, or grab the artist’s tweets. Not only that but we might add emails and jobs for the other CRUD operations, e.g. sending artists emails if the event is cancelled. These are all realistic possibilities.
Enter the Service class. Generally a service will handle one CRUD action. You might have classes such as
EventDelete for the
Event model. Let’s move the
#poll_soundcloud logic into an
class EventCreate def exec(attrs) event = Event.create(attrs) if event.valid? notify_artists poll_soundcloud end event end def notify_artists @event.artists.each do |artist| EventMailer.notify_artist(@event, artist).deliver end end def poll_soundcloud @event.artists.each do |artist| ArtistSoundcloudJob.perform_later(artist) end end end
The controller should be updated too.
class EventsController < ApplicationController def create @event = EventCreate.new.exec(params[:event]) if @event.valid? flash[:success] = 'Saved' redirect_to events_path else render :new end end end
The controller looks very much as it did at the start but it calls
EventCreate#exec instead of
Event.create. The difference lies in the fact that neither the controller nor the model now know about mail sending and job queuing, and we gain a number of advantages from this.
We can create services for each CRUD operation and thus keep class length down which means classes are easier to read, understand and maintain.
Since classes are smaller, the unit tests too can stay smaller. There is less mocking when testing controllers and models. In a controller you can stub or mock the
EventCreate out completely. In the model you needn’t stub or mock anything. In the service itself you can stub or mock the model, mailer and job queuer.
So there you have it. An explanation on using services to keep logic out of ActiveRecord models and your controllers too. Next time I will be discussing the use of decorators and how you can keep your models and views clean with them!