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:
- Concerns
- Services (a.k.a. Interactors)
- Decorators
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 #create
action:
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 EventMailer
and 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 EventCreate
, EventUpdate
and EventDelete
for the Event
model. Let’s move the #notify_artists
and #poll_soundcloud
logic into an EventCreate
service.
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!