The problem we're going to discuss is writing repeated code in your controllers to perform validation on ActiveRecord objects (making sure they're non-nill most of the time), and then keep performing the same validation on all derivates of this non-nil object.
And it does not end there, you have to perform the same validation in all actions that use the same active record object and its derivatives.
To give you a better idea of what I'm talking about, here's a small snip that suffers from the very same problem:
Code from a BadPolicy controller:
class BadPolicyController < ActionController::BaseEek! we're performing the same validations on a user, and on his policy (derived from a user object) twice, that's 4 validations in 2 actions, and the situations gets worse when we add more actions to the controller. 8 validations in 4 actions, 16 in 8 and so on!
def getpolicy
if user = User.find_by_name(params[:name])
if policy = user.policy
render :text => "Policy for user: #{policy.attributes}"
else
render :text => "Policy does not exist" # VALIDATION 1
end
else
render :text => "User doesn't exist" # VALIDATION 2
end
end
def deletepolicy
if user = User.find_by_name(params[:name])
if policy = user.policy
policy.destroy
render :text => "Policy for deleted succesfully"
else
render :text => "Policy does not exist" # REPEATED
end
else
render :text => "User doesn't exist" # REPEATED
end
end
end
Patterns are a bad sign in programming, not only in aesthetic terms but in terms of hard-to-debug programs and increased development time.
Here's what you could do, or rather should do:
Before that, let me talk a bit about around filters which we'll be using to solve the above problem.
Around filters allow us to define methods that wrap around every action that rails calls. So if I had an around filter for the above controller, I could control the execution of every action, execute code before calling the action, and after calling it, and also completely skip calling the action under certain circumstances if I wanted to.
Here's an example of the around filter from the Rails API documentation.
around_filter :catch_exceptionsYou can learn more about Around filters on the same page if you will, but my explanation here should suffice.
private
def catch_exceptions
yield
rescue => exception
logger.debug "Caught exception! #{exception}"
raise
end
Ok now lets get to work. In order to kill redundant validation code in every controller action, we will:
(1) Wrap all the action invocations with our Around filter.
(2) Define new methods in the model code that wrap auto-generated rails methods such as "policies" and "find_by_name".
(3) Raise exceptions from within our newly created methods whenever required.
(4) Catch these exceptions in our Around filter, and process them.
First lets create a few custom exceptions that we'll be raising and catching:
Create a new file in the lib with any meaningful name, mine's called 'exception.rb' and add the following code to it:
# This our custom exceptionNow lets wrap a few auto-generated rails methods in the User model with our own.
class InvalidDataEx < RuntimeError
end
Code for the User model, is as below. Note that exceptions are raised wherever required.
class User < ActionController::BaseCode for a Good policy Controller:
def self.named(name)
find_by_name(name) or raise InvalidDataEx, "User doesn't exist"
end
def get_policy(name)
self.policy or raise InvalidDataEx, "Policy doesn't exist"
end
end
class GoodPolicyController < ActionController::BaseOr even better, we can completely do away with the imperative programming style and write our actions in a functional style:
def getpolicy
user = User.named(params[:name])
render :text => "Policy for user: #{user.get_policy.attributes}"
end
def getpolicy
user = User.named(params[:name])
user.get_policy.destroy
render :text => "User policy deleted succesfully"
end
end
class GoodPolicyController < ActionController::BaseImperative programs are about HOW things are done - "get user, validate user, get his policy, validate policy, render"
def getpolicy
render :text => "Policy for user: #{User.named(params[:name]).get_policy.attributes}"
end
def deletepolicy
User.named(params[:name]).get_policy.destroy
render :text => "User policy deleted succesfully"
end
end
Functional programs are about WHAT has to be done - "render a user's policy"
Lovely !! We've come a long way.
Now lets wrap both the actions in our around filter:
add the following to the controller class:
around_filter :catch_exceptionsAwesome! That's about it.
private
def catch_exceptions
yield
rescue InvalidDataEx => err
render :text => "#{err}"
logger.debug "Exception #{err} occurred."
end
What you should be left with after reading:
Controller actions are expected to fetch data from models/lib, and render them, its not their job to perform data validation (most of the time).
By centralizing logic, making sure that its in the right place, and reusing it whenever possible, developing complex applications can become fun, intuitive and easy. Also this style produces programs that have fewer bugs, are easier to maintain and that require lesser development time.
When you see patterns in your code, then there's something wrong somewhere and a language as powerful and flexible as Ruby will leave you with options to identify and elliminate such patterns - as you saw in the example above. All it takes is a bit of thought.
Thanks for reading, comments are welcome!
1 comments:
Thanks for this article, very interesting!
Post a Comment