2019 May 01 - 2 min read
This post explains my recent experience regarding the endevour of implementing shared custom response from multiple controller actions in Ruby On Rails.
Lets say we have a controller. Inside it lies the following actions: action_1, action_2, action_3 and action_4. All of these actions would respond with custom response and for convenience the responses will be in JSON format. We need to send the same custom response from different actions, thus the response needs to be shared.
class ExampleController < ApplicationController
def action_1
end
def action_2
end
def action_3
end
def action_4
end
end
Such scenarios my arise when:
The following text explains all the approaches which I tried for implementation of such requirements. And why one is better that the other.
This approach uses a shared method to perform extra logic and provide a Boolean response which determines the action’s response. The logic is performed in res_n method and the response is returned from controller action.
class ExampleController < ApplicationController
def action_1
if res1.eql?(false)
render json: {message: 'Failed'}, status: 400 unless res_1
return
end
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_2
if res1.eql?(false)
render json: {message: 'Failed'}, status: 400 unless res_1
return
end
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_3
if res2.eql?(false)
render json: {message: 'Failed'}, status: 400 unless res_2
return
end
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_4
if res2.eql?(false)
render json: {message: 'Failed'}, status: 400 unless res_2
return
end
# rest of the logic....
render json: {data: '...'}, status: 200
end
private
def res_1
# perform some operations
(operation1_successful?) ? true : false
end
def res_2
# perform some operations
(operation2_successful?) ? true : false
end
end
As we can see, it has a lot of code duplication. The success response may be different but the error response need to be repeated in all actions. It violates the ROR's DRY principle.
This approach minimizes the code redundancy by moving the common error response to res_n methods. As you may have noticed the built-in method performed? is used. This method returns true if render method has already been invoked earlier. This way we prevent render method being called multiple times. The code is more simpler and cleaner than the first approach.
class ExampleController < ApplicationController
def action_1
res_1; return if performed?
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_2
res_1; return if performed?
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_3
res_2; return if performed?
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_4
res_2; return if performed?
# rest of the logic....
render json: {data: '...'}, status: 200
end
private
def res_1
# perform some operations
render json: {message: 'Failed'}, status: 400 unless operation1_successful?
end
def res_2
# perform some operations
render json: {message: 'Failed'}, status: 400 unless operation1_successful?
end
end
This approach is the best among the previous two approaches. With a lot of research on code refactoring on this scenario I stumbled on this solution. It automatically prevents render method being called multiple times and make code more shorter, cleaner and easier than the 2nd approach. It uses built-in controller method before_action
. This method gets called before each time a controller’s action is executed. The control of the program never enter the action method if it is returned from before_action
method.
class ExampleController < ApplicationController
before_action :res1, only: [:action_1, :action_2]
before_action :res2, only: [:action_3, :action_4]
def action_1
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_2
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_3
# rest of the logic....
render json: {data: '...'}, status: 200
end
def action_4
# rest of the logic....
render json: {data: '...'}, status: 200
end
private
def res_1
# perform some operations
render json: {message: 'Failed'}, status: 400 unless operation1_successful?
end
def res_2
# perform some operations
render json: {message: 'Failed'}, status: 400 unless operation1_successful?
end
end
In this post, I have tried to explain the problem I faced during my ROR internship. And how I manged to solve it, trying different approaches, improving each time. Although the last attempt seems to be the best one, there may be scenarios where we need to implement the techniques of former approaches too.