In this post I'm going to talk about how to manage your application as it grows and discuss ways in which you can keep the code base clean.
The first thing I want to talk about is decorators in Rails. Ideally you should decouple your Rails models from any kind of code that deals with presentation otherwise you end up with a bloated model full of code that shouldn't really belong there. Think of it this way, if you persist data one way in the database but present it in a different way then that is a good signal to use a decorator. You may be thinking that you could just use a helper for this but there is a distinction between a helper and a decorator. A helper is a more generic method which can be used for different kinds of objects whereas a decorator should act only with one model and shouldn't take parameters if at all possible. Here's an example snippet from view in the MyFLiX application I have been building in the Tealeaf Academy course:
%article.video
.container
.row
.video_large_cover.col-sm-7.col-sm-offset-1
%img(src="#{@video.display_large_video_image}")
.video_info.col-sm-3
%header
%h3= @video.title
%span Rating: #{@video.average_rating}
%p= @video.description
.actions
%a.btn.btn-primary(href="#{@video.video_url}") Watch Now
= link_to_my_queue(@video)
You can see that I have two methods in here, display_large_video_image
and average_rating
. These methods are primarily concerned with the presentation in my view and initially I had moved this into the video.rb
model:
class Video < ActiveRecord::Base
...
# If large_cover is present then return image url, otherwise display dummy image
def display_large_video_image
self.large_cover.present? ? self.large_cover_url : "http://dummyimage.com/665x375/000/fff.png&text=No+Preview+Available"
end
# calulate average rating for video to 1 decimal point
def average_rating
self.reviews.average(:rating) ? "#{reviews.average(:rating).round(1)}/5" : 'N/A'
end
end
A better way to deal with this is to create a new file under app/decorators/video_decorator.rb
and move the code into that file:
class VideoDecorator < Draper::Decorator
delegate_all
# If large_cover is present then return image url, otherwise disply dummy image
def display_large_video_image
object.large_cover.present? ? object.large_cover_url : "http://dummyimage.com/665x375/000/fff.png&text=No+Preview+Available"
end
# calculate average rating for video to 1 decimal point
def average_rating
object.reviews.present? ? "#{reviews.average(:rating).round(1)}/5" : 'N/A'
end
end
I am using the draper gem to make it easy to use this VideoDecorator in my controller. All I have to do is just use the .decorate
method in my controller:
class VideosController < ApplicationController
...
def show
@video = Video.find(params[:id]).decorate
@review = Review.new
end
...
end
The one issue this creates is that my view will expect all method calls on @video
to be on the VideoDecorator
whereas @video.description
in my view needs to be called on the video model. To get around this I have used delegate_all
in my VideoDecorator
class. This allows all the method calls that are not defined in the VideoDecorator
class to be passed through to the video model.
Draper additionally accesses the decorated model as object
as you can see in my VideoDecorator
class.
This all works well but I came across this article which made me see there was an alternative way to do this that didn't require a gem and also makes it more obvious where the method was called without really requiring much extra code - use a plain old Ruby object (PORO). I'll show the changes below:
The view:
%article.video
.container
.row
.video_large_cover.col-sm-7.col-sm-offset-1
%img(src="#{@video_presenter.display_large_video_image}")
.video_info.col-sm-3
%header
%h3= @video.title
%span Rating: #{@video_presenter.average_rating}
%p= @video.description
.actions
%a.btn.btn-primary(href="#{@video.video_url}") Watch Now
= link_to_my_queue(@video)
The presenter class (no longer using decorator terminology):
class VideoPresenter
def initialize(video)
@video = video
end
# If large_cover is present then return image url, otherwise display dummy image
def display_large_video_image
video.large_cover.present? ? video.large_cover_url : "http://dummyimage.com/665x375/000/fff.png&text=No+Preview+Available"
end
# calculate average rating for video to 1 decimal point
def average_rating
video.reviews.present? ? "#{video.reviews.average(:rating).round(1)}/5" : 'N/A'
end
private
attr_reader :video
end
The controller:
class VideosController < ApplicationController
...
def show
@video = Video.find(params[:id])
@video_presenter = VideoPresenter.new(@video)
end
...
end
As you can see there is a little extra code but that only takes an extra 30secs to write so it's hardly an issue and I feel I have extra clarity in my code by doing it this way.
An example of when to use policy objects is when you want to enforce different access control policies. For example you could have users that sign up for bronze, silver and gold access to your application. Here's a simple example where I create a new model class in app/policies/user_level_access.rb
class UserLevelAccess
attr_reader :user
def initialize(user)
@user = user
end
def bronze?
user.email_confirmed? && user.plan.bronze?
end
def silver?
user.email_confirmed? && user.plan.silver?
end
def gold?
user.email_confirmed? && user.plan.gold?
end
end
Then in a controller action I can decide what happens depending on the level of access the user has. Say for example I'm running an online project management application and I want to charge different amounts for each new project depending on the plan the user has purchased. Without the policy object class my controller action could look like this example:
class ProjectController < ApplicationController
...
def create
@project = Project.new(project_params)
if current_user.email_confirmed? && current_user.plan.gold?
...
elsif current_user.email_confirmed? && current_user.plan.silver?
...
else
...
end
end
...
end
but with the UserLevelAccess
class I now have:
class ProjectController < ApplicationController
...
def create
@project = Project.new(project_params)
user_level_access = UserLevelAccess.new(current_user)
if user_level_access.gold?
...
elsif user_level_access.silver?
...
else
...
end
end
...
end
Designing it this way means my code is re-useable if I need to set the policy in multiple places and also easily maintainable because if I need to make any changes to the the access then I only have to do it in one place, which is especially beneficial if the logic becomes quite complicated.
Domain objects in Rails typically inherit from ActiveRecord::Base
and are responsible for persisting or saving data attributes to a relational database table. However there will be cases in applications where the models do not map to database tables. I'll extend my example from above to explain when this might be useful:
class ProjectController < ApplicationController
...
def create
@project = Project.new(project_params)
user_level_access = UserLevelAccess.new(current_user)
if user_level_access.gold?
new_credit_balance = current_user.current_credit_balance -1
elsif user_level_access.silver?
new_credit_balance = current_user.current_credit_balance -2
else
new_credit_balance = current_user.current_credit_balance -3
end
current_user.current_credit_balance = new_credit_balance
current_user.save
if new_credit_balance < 0
...
elsif current_user.current_credit_balance < 10
...
end
end
...
end
Here you can see that a user is having their balance deducted depending on their current plan. Each time I want to get the current balance I have to go through current_user and if I want the data to persist I have to do a current_user.save
. However, I would like to extract the concept of credit into it's own model so I can operate on that directly rather than having to go through users:
class Credit
attr_accessor :credit_balance, :user
def initialize(user)
@credit_balance = user.current_credit_balance
end
def -(number)
credit_balance = credit_balance - number
end
def save
user.current_credit_balance = credit_balance
user.save
end
def depleted?
credit_balance < 0
end
def low_balance?
credit_balance < 10
end
end
Now I can change my controller to be this:
class ProjectController < ApplicationController
...
def create
@project = Project.new(project_params)
credit = Credit.new(current_user)
user_level_access = UserLevelAccess.new(current_user)
if user_level_access.gold?
credit = credit - 1
elsif user_level_access.silver?
credit = credit - 2
else
credit = credit - 3
end
credit.save
if credit.depleated?
...
elsif credit.low_balance?
...
end
end
...
end
Doing things this way not only makes the code easier to read but also everything based around credit is located in one place so it will be easy to test, easy to reuse in other parts of the application and also easy to change the business logic in the future.
I can use service objects to make the business process of credit deduction in the above example more explicit. Create a new file under app/services/credit_deduction.rb
and it will look like this:
class CreditDeduction
attr_accessor :credit, :user
def initialize(user)
@credit = Credit.new(user)
@user = user
end
def deduct_credit
if UserLevelPolicy.new(user).premium?
credit = credit - 1
else
credit = credit - 2
end
credit.save
if credit.depleted?
...
elsif credit.low_balance?
...
end
end
end
You will notice that I have pulled out all of the code related to credit deduction from the controller into this service object. With this done my controller has been cleaned up considerably and is much more explicit:
class ProjectController < ApplicationController
...
def create
@project = Project.new(project_params)
CreditDeduction.new(current_user).deduct_credit
end
...
end
That's all for this post, I've gone through some examples of how to extract code and hopefully this will give you some ideas of how to manage your code as your application grows.