A little DSL on your Spaghetti (code)?
November 15th, 2007
In one of our current projects, it is required that certain pages show different articles depending on the profile and network of contacts of the logged-in user.
In order to have a working prototype as soon -and Agile- as possible, we defined the logic for those viewing rules directly into our ActiveRecord models..
def article_list( user )
articles = []
if user.is_a? Admin #Admin can see everything
articles += find(:all)
# public articles, written by the user or his contacts
elsif user.is_a? User
articles += find(:all, :conditions=>{:public=>true})
articles += find(:all, :conditions=>{:author=>user})
articles += find(:all,
:conditions=>["author_id IN (?)", user.friends.collect(&:id)])
else # anonymous user can only see public articles
articles += find(:all, :conditions=>{:public=>true})
end
articles.uniq.sort_by(&:published_at)
end
It's a prototype. No one said it had to scale.
Even though we could optimize the method and reduce the amount of hits to the database, the real risk with this approach lies in that we're trying to declare our business logic (who sees what) and our data storage strategy in the same place. One block of code attempting to deal with two separate concerns at once. Should our viewing rules change, we would have to modify our conditional logic both in the if / else structures and in the arguments we pass to ActiveRecord finders. If, on the other hand, our app becomes a success and we decide to resort to Memcached for quick data access, we must change our data storage strategy with extreme caution to avoid breaking the conditions that support our rules.
Expressing our rules in plain SQl is not a sensible option, as it probably would make the code even more intricate.
After a coffee (or ten) and a bit of fresh air things are clear. Viewing and privacy rules for the app's articles might change, or new user profiles can be added to the mix. The core concept of the project is sustained by those rules. The how and where we store those data is a detail of implementation that has little to do with the former.
We should be able to freely define rules and permissions in one place, and handle the heavy lifting elsewhere.
With a little refactoring and Ruby's transparency we came up with something like this:
Class RegisteredUser < User
...
def visible_articles
set_rules do |articles|
articles.add :from=>self, :to=>:anybody, :status => :all
articles.add :from=>:anybody, :to=>self, :status => :public
articles.add :from=>contacts, :to=>:anybody, :status => :all
end
end
...
end
With this solution, the set_rules method can be reused in different contexts to define relevant rules. After processing a block of rules, it returns a simple array of options that we can pass to ActiveRecord finders or some other query-building utility that helps us minimize database hits. Yes, Ambition, I'm looking at you!
This simple DSL separates the definition of our business rules from data access -which we handle in a different layer of the application. As a side effect, this gives us much more readable code that is easy to maintain and test. Also, we get closer to the core domain of the project.
Where are the conditionals? With the proper use of inheritance -by class or modules- we can have every User profile declare their own set of straight-forward rules without the need of confusing conditions (you know it's a good time to re-think the architecture when those sneaky ifs and elses start creeping in all over the place).
Rails is in itself a DSL and suitable for most of web scenarios. Still, sometimes it makes sense to step back from your tools for a bit so you can get closer to the actual problem.






Sorry, comments are closed for this article.