Friday, February 8, 2008

Rails: Aspect-Oriented Programming Use Case

You won't hear a lot of people talk about aspect-oriented programming in Rails, but it's worth learning about. Here's why.

Yesterday I wrote my third ActiveResource plugin since December. I'll probably release it in a few days.

This plugin had to kind of rewire ARes. As anyone who builds ARes apps knows, the first thing you do when you start is you define the resource's base URL.



If you want to do your authentication with HTTP Basic Authentication, you just add a username and password to the URL.

self.site = "http://user:pass@localhost:12345/"

But what if you need to do that authentication for more than one user? Not such an uncommon use case, when you think about it. What if your Web service parcels out resource access on a user-by-user basis? Each logged-in user will have different access priveleges, so that the username/password info varies at an instance level, but it's specified at a class level.

Problem.

You can't just dynamically reassign self.site on the class. If you modify class-level information on an instance-by-instance basis, you basically set yourself up for a race condition the minute two instances of the same class need to modify the same info. This means you've got to modify ActiveResource in a pretty fundamental way.

Of course, this is easy with Ruby. But it isn't necessarily pretty.



Although my plugin is working well, and will probably not need any difficult work to get working perfectly, the design could be better. Basically, my client programmer(s) needed me to preserve the original behavior but enable instance-level auth in many cases. So I have this kind of hybrid thing going on.



You can use the usual class-level site config, or pass a connection object, and Rails knows whether to use the run-of-the-mill class-level config, or the more specific config.

Implementing this involved a lot of duplication.



Additionally, the connection isn't actually on the instance, it's just this loose-floating connection object, unanchored to any particular model or controller or anything.

I want to change that, and probably will, but even when I do, it won't alter the fundamental hackiness of the code. Either way, it's going to involve a certain amount of repetition. The reason is I'm working against a fundamental aspect of ARes' design, and doing so necessarily requires a fair amount of work, because I'm going against the grain.

Faced with a situation like that, of course, I want to fix it, and this is where I kind of get to the point, which is always a good thing to do, sooner or later. There are two ways to solve this particular problem, and each of them has its tradeoffs.

Redesign

The obvious answer is to simply redesign ActiveResource to store this information on an instance basis, or possibly to design a layer of abstraction where you separate your Web service's HTTP auth from the auth on your ARes app. In either case, redesign is a lot of work. It also kind of blurs the line between redesign and refactoring, because if you look at ARes from the outside, it looks like a refactoring - a change in design with no change in behavior - but if you've got a bunch of ARes code, in practical terms, it's a redesign.

Aspect-Oriented Programming

This is exactly the kind of problem AOP was designed for. Authentication is what AOPers call a cross-cutting concern - it's something many different objects in your system will need to do, but putting it on all the objects in the system is a terrible failure of separation of concerns. In this specific plugin, I'm moving the authentication from a model's class to its instances, but you could make a pretty strong argument that neither the class nor the instance actually has anything to do with authenticating, and therefore neither of them should even have that information in the first place.

The AOP answer is to use "stand-alone modules called aspects" to isolate behavior required across several business models in such a way that they are available to all those objects without being contained within them. This prevents tight coupling of logically independent functionality, and drives you towards a good design.

Aspect-oriented programming is very much a product of the Java world, and the Rails community has taken a pretty antagonistic attitude towards that community in the past.



In my opinion, it was certainly a fight worth picking, but it got way out of hand. Certainly, a generic knee-jerk hostility towards Java ideas is still to be found in the Rails world, but throwing that same kind of attitude at aspect-oriented programming could be a "throwing the baby out with the bathwater" kind of mistake. It's entirely possible that aspect-oriented programming inevitably becomes useful for any sufficiently complicated framework used for at least a certain threshold number of different applications.