Sometimes your controllers and models start growing exponentially as your application starts getting bigger and bigger. After a while, your application will be so complex that to implement a feature or maintain existent ones will take so much time and effort that you will lose the love about the project or even your work. So, for your sake and your project’s, you actually don’t want that to happen. What could you do when you start feeling that’s going to be the case? Or what could you do to prevent that?
With the following strategics you will be able to refactor and/or start your application with a really good set of patterns and a great way to develop Ruby on Rails applications.
Controllers
Are your controllers too complex? Are they too big with too much business logic? If so, you should extract some of the logic to some patterns that I will present next.
Service Objects
Let’s say you have a controller that does a user registration, sends a welcome email and give credits to the user. That’s too much business logic to be in a controller. Models are not the place as well, because you want to go far away as possible from fat models. So what can we do?
Service Objects are the way to go. They are a simple Ruby Object that implements the user’s interactions logic.
In Rails they usually live in app/services
folder and you don’t need to install anything else in your application, it’s just Plain Old Ruby Objects (PORO).
To learn a detailed way to implement Services Objects in your Rails application, take a read here.
Presenters
Does your controllers instantiates too many global variables with different models to just render one view? Presenters are for you then.
Let’s say you have the following action and you will have some other pages that use the exactly same variables, you would probably copy and paste it.
def show
raw_project = Project.find(params[:id])
@project = raw_project.decorate
@contributions = raw_project.contributions.random.decorate
@rewards = raw_project.rewards.decorate
end
As you can see, it would be kind of hard to test it and make sure all the places you use them will be maintained equal. So, with Presenters you would be able to do something like this:
class ProjectPresenter
def initialize(project_id)
@project = Project.find(project_id)
end
def project
@decorated_project ||= @project.decorate
end
def contributions
@contributions ||= @project.contributions.random.decorate
end
def rewards
@rewards ||= @project.rewards.decorate
end
end
Presenters are just Plain Old Ruby Objects, so to test it is really easy.
With presenter, your controller would look like this:
def show
@presenter = ProjectPresenter.new(params[:id])
end
And your tests should make sure to just instantiate the presenter.
SQL queries
Somethings you find your self writing SQL queries to filter some data from your model in your controllers. In time to time, it will repeat and you will end up with too many different ways to do the same thing, with the long run, it will probably break the consistence of your filters. So, how do you solve it? Extract it to scopes on models.
Let’s use the following controller as example.
class CustomersController < ApplicationController
# …
def appointments
@appointments = current_customer.appointments.joins(:user_payment)
.where("user_payments.status = 'COMPLETED' AND starts_at >= ? AND ends_at <= ?",
Time.at(params[:start].to_i).to_datetime, Time.at(params[:end].to_i).to_datetime)
respond_with @appointments
end
# …
end
You can see in the code above that we have two filter logic in the appointments model. First is filtering by completed appointments and other is filtering between starts and ends at.
We can extract these filters to two scopes to the appointment’s model.
class Appointment < ActiveRecord::Base
# …
scope :completed, -> do
joins(:user_payment).where("user_payments.status = 'COMPLETED'")
end
scope :between_starts_and_ends_at, ->(starts_at, ends_at) do
where("starts_at >= ? AND ends_at <= ?",
Time.at(starts_at.to_i).to_datetime,
Time.at(ends_at.to_i).to_datetime)
end
# …
end
With this change, you will be able to test more easily and maintain more consistency across your application.
Your controller would look like this after this refactoring:
class CustomersController < ApplicationController
# …
def appointments
@appointments = current_customer.
appointments.
completed.
between_starts_and_ends_at(params[:start], params[:end])
respond_with @appointments
end
# …
end
Models
There are several strategics to avoid fat models, but I will not talk about that, I will talk about some small changes that you can do and get a great return from it.
Use scope instead of self.method_name
Scopes exists for one reason, to specify a filter method for your data. Why do you use self.method_name
if we have scope :scope_name
?
I know that in the end, scope does the same thing that self.method_name
does, but it makes clearly to look in your
model and see what are the methods to filter data.
Here an example of refactoring you could do.
class Hint < ActiveRecord::Base
# …
def self.approved
where(approved: true)
end
# …
end of
Using scopes would be like this:
class Hint < ActiveRecord::Base
# …
scope :approved, -> { where(approved: true) }
# …
end
Also you should note that scopes are always chainable, different from class methods, that depending of how you create your methods, they might not be. To read more about it, check this awesome article.
Why not delegate?
Why do you don’t use delegate? Sometimes you will write something like the following code and will not realize that you could use Ruby’s delegate to obtain the same thing.
class Hint < ActiveRecord::Base
# …
def user_name
user.name if user.present?
end
# …
end
With delegate you would have this:
class Hint < ActiveRecord::Base
# …
delegate :name, to: :user, prefix: true, allow_nil: true
# …
end
Use Observers, not callbacks.
You should avoid using callbacks, they usually go back to hunt you after a while. Instead you can use Observers, that provide you a good API and easy testing. To read more about why callbacks are bad, go over here.
Check the rails-observers gem out: rails/rails-observers.
Decorators
Decorators are a way to extract record related logic from views or even models to an object.
Let’s assume that you have the following view:
<div class="publication-status">
<% if article.published? %>
Published at <%= article.published_at.strftime('%A, %B %e') %>
<% else %>
Unpublished
<% end %>
</div>
You can find this logic in several places and if you need to change something, you will need to do it over and over again. You may end up with inconsistence. Also there is the testing factor, using decorators, will help you test this kind of things.
Here how the decorator would look like:
class ArticleDecorator < Draper::Decorator
delegate_all
def publication_status
if published?
"Published at #{published_at}"
else
"Unpublished"
end
end
def published_at
object.published_at.strftime("%A, %B %e")
end
end
In this example I used the draper gem.
Wrapping up
There are several patterns that you can use to improve the code base of your application and keep the harmony. I just presented some with just a little bit of information, each one of these patterns could be a different article with a lot more details. But instead, I chose to just present a summary of them so you can go and learn more on each of them.
Take some time off and research for more posts/articles about these patterns and what else could you use to help your application. Enjoy your reading time.