Tuesday, October 23, 2007

Rant: Rails Lacks Accessor Bottlenecks

Being a longtime user of (and zealot for) Ruby, I have a made a name around the office for my unusually strong understanding of its details. Thus my employers decided that, out of our team of developers, I should be the one to write the security sub-layer for our Ruby on Rails based chiropractic applications (as it would require digging into, and understanding the internals of Rails.

Now before I proceed to beat Rails around a littl bit, I don't want you to get me wrong. I love using Ruby on Rails and would easily chooose to do this project (and future projects) in it again, but sometimes I find some of their design decisions to be, well, web-developerish, while we need a more robust enterprise-developerish solution.

A Bottle With Too Many Openings Can't Hold Water

One of the design patterns that I have seen over and over again in Object Oriented APIs is the use of bottle-necking. That is to say, even within your own class, you choose to use a set of accessors to get at instance variables instead of poking at them directly.

The big benefits to this approach are two fold: It allows you to be more agile with changes (as you only have edit the accessors to change behavior), and it allows third part developers to easily augment or override the default behavior of your code.

An simple example of this may be a simple vector class that stores a magnitude and an angle. Now this angle, in all reality, needs to be between 0 and 360 degrees. So if you have two ways to set this data (from rectangular or polar coordinates), you would have to enforce these limits, probably by using a modulus operator. (Which isn't very DRY, now is it?) But what happens when you have to change the behavior? Maybe the angle needs to be stored in radians, maybe the range needs to be changed to -90 to 90, or maybe it needs to be compass oriented instead of right-handed-axis oriented? Or maybe you just want to add a way to set the angle without changing the magnitude?

All of these requires refactoring your code, which requires duplicating and/or altering your code in several locations! But if you were to make a single accessor for getting, and a single accessor for setting the internal value, then all your other code could go through this accessor (including possible intermediate accessors) to enable fast, agile, and DRY code changes!

Playing With Steam Engines

So, now, let's imagine your employer gives you the spec that your enterprise Rails driven software must have a security system where you can restrict read and write ability to different models, on a per attribute basis. This is what I faced over a year ago.

The way I really wanted to solve this was to override the accessors to the basic hash that stores, as key/value pairs, all the data for the table row the objects represents. That way I could add my own code that would allow or deny access at this fundamental level.

Unfortunately this basic and simple idea (based on past experience with other APIs) exploded in my face. You see, the @attributes hash instance variable that ActiveRecord uses to store these key/value pairs is directly poked and prodded by many separate methods throughout the ActiveRecord class. Thus I had to override all of these methods, making them call the same friggin security check method, to decide if they could continue onto their default implementation or if I should restrict access. (I'm only lucky that the runtime generated accessors utilized standard accessors, or else I'd be re-writing the code-generation routines as well.)

Of course, all this work meant that besides sinking a lot of the company's time and money into implementing a task that should have been simple, maintaining our release against newer versions of Rails is more costly because of all the hacks I've had to do to meet the requirements.

Which is why I'm so peeved at their code. If only they had followed good design practices and bottle-necked all calls to @attributes through a read and write accessor method, it would have been easy to implement customized behavior at this point in the program flow, and thus would have made my security requirements a trivial task.

No comments: