Welcome back to the fourth installment of the Ruby on Rails Patterns and Anti-Patterns series.
Previously, we covered patterns and anti-patterns in general as well as in relation to Rails Models and Views. In this post, we are going to analyze the final part of the MVC (Model-View-Controller) design pattern — the Controller. Let's dive in and go through the patterns and anti-patterns related to Rails Controllers.
At The Front Lines
Since Ruby on Rails is a web framework, HTTP requests are a vital part of it. All sorts of Clients reach out to Rails backends via requests and this is where controllers shine. Controllers are at the front lines of receiving and handling requests. That makes them a fundamental part of the Ruby on Rails framework. Of course, there is code that comes before controllers, but controller code is something most of us can control.
Once you define routes at the config/routes.rb
, you can hit the server on the
set route, and the corresponding controller will take care of the rest. Reading
the previous sentence might give an impression that everything is as simple as
that. But, often, a lot of the weight falls on the controller's shoulders.
There is the concern of authentication and authorization, then there are
problems of how to fetch the needed data, as well as where and how to perform
business logic.
All of these concerns and responsibilities that can occur inside the controller can lead to some anti-patterns. One of the most 'famous' ones is the anti-pattern of a "fat" controller.
Fat (Obese) Controllers
The problem with putting too much logic in the controller is that you are starting to violate the Single Responsibility Principle (SRP). This means that we are doing too much work inside the controller. Often, this leads to a lot of code and responsibilities piling up there. Here, 'fat' refers to the extensive code contained in the controller files, as well as the logic the controller supports. It is often considered an anti-pattern.
There are a lot of opinions on what a controller should do. A common ground of the responsibilities a controller should have include the following:
- Authentication and authorization — checking whether the entity (oftentimes, a user) behind the request is who it says it is and whether it is allowed to access the resource or perform the action. Often, authentication is saved in the session or the cookie, but the controller should still check whether authentication data is still valid.
- Data fetching — it should call the logic for finding the right data based on the parameters that came with the request. In the perfect world, it should be a call to one method that does all the work. The controller should not do the heavy work, it should delegate it further.
- Template rendering — finally, it should return the right response by rendering the result with the proper format (HTML, JSON, etc.). Or, it should redirect to some other path or URL.
Following these ideas can save you from having too much going on inside the controller actions and controller in general. Keeping it simple at the controller level will allow you to delegate work to other areas of your application. Delegating responsibilities and testing them one by one will ensure that you are developing your app to be robust.
Sure, you can follow the above principles, but you must be eager for some examples. Let's dive in and see what patterns we can use to relieve controllers of some weight.
Query Objects
One of the problems that happen inside controller actions is too much querying of data. If you followed our blog post on Rails Model anti-patterns and patterns, we went through a similar problem where models had too much querying logic. But, this time we'll use a pattern called Query Object. A Query Object is a technique that isolates your complex queries into a single object.
In most cases, Query Object is a Plain Old Ruby Object that is initialized with
an ActiveRecord
relation. A typical Query Object might look like this:
1# app/queries/all_songs_query.rb
2
3class AllSongsQuery
4 def initialize(songs = Song.all)
5 @songs = songs
6 end
7
8 def call(params, songs = Song.all)
9 songs.where(published: true)
10 .where(artist_id: params[:artist_id])
11 .order(:title)
12 end
13end
It is made to be used inside the controller like so:
1class SongsController < ApplicationController
2 def index
3 @songs = AllSongsQuery.new.call(all_songs_params)
4 end
5
6 private
7
8 def all_songs_params
9 params.slice(:artist_id)
10 end
11end
You can also try out another approach of the query object:
1# app/queries/all_songs_query.rb
2
3class AllSongsQuery
4 attr_reader :songs
5
6 def initialize(songs = Song.all)
7 @songs = songs
8 end
9
10 def call(params = {})
11 scope = published(songs)
12 scope = by_artist_id(scope, params[:artist_id])
13 scope = order_by_title(scope)
14 end
15
16 private
17
18 def published(scope)
19 scope.where(published: true)
20 end
21
22 def by_artist_id(scope, artist_id)
23 artist_id ? scope.where(artist_id: artist_id) : scope
24 end
25
26 def order_by_title(scope)
27 scope.order(:title)
28 end
29end
The latter approach makes the query object more robust by making params
optional. Also, notice that we can now call AllSongsQuery.new.call
.
If you're not a big fan of this, you can resort to class methods. If you write
your query class with class methods, it will no longer be an 'object', but this
is a matter of personal taste. For illustration purposes, let's see how we can makeAllSongsQuery
simpler to call in the wild.
1# app/queries/all_songs_query.rb
2
3class AllSongsQuery
4 class << self
5 def call(params = {}, songs = Song.all)
6 scope = published(songs)
7 scope = by_artist_id(scope, params[:artist_id])
8 scope = order_by_title(scope)
9 end
10
11 private
12
13 def published(scope)
14 scope.where(published: true)
15 end
16
17 def by_artist_id(scope, artist_id)
18 artist_id ? scope.where(artist_id: artist_id) : scope
19 end
20
21 def order_by_title(scope)
22 scope.order(:title)
23 end
24 end
25end
Now, we can call AllSongsQuery.call
and we're done. We can pass in params
with artist_id
. Also, we can pass the initial scope if we need to change it
for some reason. If you really want to avoid calling new
over a query class, try out this 'trick':
1# app/queries/application_query.rb
2
3class ApplicationQuery
4 def self.call(*params)
5 new(*params).call
6 end
7end
You can create the ApplicationQuery
and then inherit from it in other query
classes:
1# app/queries/all_songs_query.rb
2class AllSongsQuery < ApplicationQuery
3 ...
4end
You still kept the AllSongsQuery.call
, but you made it more elegant.
What's great about query objects is that you can test them in isolation and ensure that they are doing what they should do. Furthermore, you can extend these query classes and test them without worrying too much about the logic in the controller. One thing to note is that you should handle your request parameters elsewhere, and not rely on the query object to do so. What do you think, are you going to give query object a try?
Ready To Serve
OK, so we've handled ways to delegate the gathering and fetching of data into Query Objects. What do we do with the pilled-up logic between data gathering and the step where we render it? Good that you asked, because one of the solutions is to use what are called Services. A service is oftentimes regarded as a PORO (Plain Old Ruby Object) that performs a single (business) action. We will go ahead and explore this idea a bit below.
Imagine we have two services. One creates a receipt, the other sends a receipt to the user like so:
1# app/services/create_receipt_service.rb
2class CreateReceiptService
3 def self.call(total, user_id)
4 Receipt.create!(total: total, user_id: user_id)
5 end
6end
7
8# app/services/send_receipt_service.rb
9class SendReceiptService
10 def self.call(receipt)
11 UserMailer.send_receipt(receipt).deliver_later
12 end
13end
Then, in our controller we would call the SendReceiptService
like this:
1# app/controllers/receipts_controller.rb
2
3class ReceiptsController < ApplicationController
4 def create
5 receipt = CreateReceiptService.call(total: receipt_params[:total],
6 user_id: receipt_params[:user_id])
7
8 SendReceiptService.call(receipt)
9 end
10end
Now you have two services doing all the work, and the controller just calls them. You can test these separately, but the problem is, there's no clear connection between the services. Yes, in theory, all of them perform a single business action. But, if we consider the abstraction level from the stakeholders' perspective — their view of the action of creating a receipt involves sending an email of it. Whose level of abstraction is 'right'™️?
To make this thought experiment a bit more complex, let's add a requirement that the total sum on the receipt has to be calculated or fetched from somewhere during the creation of the receipt. What do we do then? Write another service to handle the summation of the total sum? The answer might be to follow the Single Responsibility Principle (SRP) and abstract things away from each other.
1# app/services/create_receipt_service.rb
2class CreateReceiptService
3 ...
4end
5
6# app/services/send_receipt_service.rb
7class SendReceiptService
8 ...
9end
10
11# app/services/calculate_receipt_total_service.rb
12class CalculateReceiptTotalService
13 ...
14end
15
16# app/controllers/receipts_controller.rb
17class ReceiptsController < ApplicationController
18 def create
19 total = CalculateReceiptTotalService.call(user_id: receipts_controller[:user_id])
20
21 receipt = CreateReceiptService.call(total: total,
22 user_id: receipt_params[:user_id])
23
24 SendReceiptService.call(receipt)
25 end
26end
By following SRP, we make sure that our services can be composed together into
larger abstractions, like the ReceiptCreation
process. By creating this 'process'
class, we can group all the actions needed to complete the process. What do you
think about this idea? It might sound like too much abstraction at first,
but it might prove beneficial if you are calling these actions all over the place.
If this sounds good to you, check out the Trailblazer's Operation.
To sum up, the new CalculateReceiptTotalService
service can deal with all the
number crunching. Our CreateReceiptService
is responsible for writing a
receipt to the database. The SendReceiptService
is there to dispatch emails
to users about their receipts. Having these small and focused classes can make
combining them in other use cases easier, thus resulting in an easier to
maintain and easier to test codebase.
The Service Backstory
In the Ruby world, the approach of using service classes is also known as actions, operations, and similar. What these all boil down to is the Command pattern. The idea behind the Command pattern is that an object (or in our example, a class) is encapsulating all the information needed to perform a business action or trigger an event. The information that the caller of the command should know is:
- name of the command
- method name to call on the command object/class
- values to be passed for the method parameters
So, in our case, the caller of a command is a controller. The approach is very similar, just that the naming in Ruby is 'Service'.
Split Up The Work
If your controllers are calling some 3rd party services and they are blocking your rendering, maybe it's time to extract these calls and render them separately with another controller action. An example of this can be when you try to render a book's information and fetch its rating from some other service that you can't really influence (like Goodreads).
1# app/controllers/books_controller.rb
2
3class BooksController < ApplicationController
4 def show
5 @book = Book.find(params[:id])
6
7 @rating = GoodreadsRatingService.new(book).call
8 end
9end
If Goodreads is down or something similar, your users are going to have to wait for the request to Goodreads servers to timeout. Or, if something is slow on their servers, the page will load slowly. You can extract the calling of the 3rd party service into another action like so:
1# app/controllers/books_controller.rb
2
3class BooksController < ApplicationController
4 ...
5
6 def show
7 @book = Book.find(params[:id])
8 end
9
10 def rating
11 @rating = GoodreadsRatingService.new(@book).call
12
13 render partial: 'book_rating'
14 end
15
16 ...
17end
Then, you will have to call the rating
path from your views, but hey, your show
action doesn't have a blocker anymore. Also, you need the 'book_rating'
partial. To do this more easily, you can use the render_async gem.
You just need to put the following statement where you render your book's rating:
1<%= render_async book_rating_path %>
Extract HTML for rendering the rating into the book_rating
partial, and put:
1<%= content_for :render_async %>
Inside your layout file, the gem will call book_rating_path
with an AJAX
request once your page loads, and when the rating is fetched, it will show it
on the page. One big gain in this is that your users get to see the book page
faster by loading ratings separately.
Or, if you want, you can use Turbo Frames from Basecamp.
The idea is the same, but you just use the <turbo-frame>
element in your markup like so:
1<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>
Whatever option you choose, the idea is to split the heavy or flaky work from your main controller action and show the page to the user as soon as possible.
Final Thoughts
If you like the idea of keeping controllers thin and picture them as just 'callers' of other methods, then I believe this post brought some insight on how to keep them that way. The few patterns and anti-patterns that we mentioned here are, of course, not an exhaustive list. If you have an idea on what is better or what you prefer, please reach out on Twitter and we can discuss.
Definitely stay tuned on this series, we are going to do at least one more blog post where we sum up common Rails problems and takeaways from the series.
Until next time, cheers!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!