There has been surprisingly little written about how Rails is implemented. If you Google for “architecture Rails” you’ll find plenty of articles that describe Rails as a web framework based on the classic Model-View-Controller design pattern. And you’ll find plenty of links about Rail’s constituent parts – ActiveRecord, ActionController, ActionView, ActiveSupport, etc. But you’ll find precious little about how Rails is put together.
Which is shame, since Rails is constructed in a very unusual manner. So let’s dig into Rails and see what makes it unusual.
What Does a Web Framework Have to Do ?
Let’s start by considering what a web framework has to do. If you were to build your own framework, the absolute minimum requirements are:
- Read requests via http/s
- Unmarshall client provided data – the data may be url encoded, gzipped, etc.
- Figure out what the client is asking
- Do what the client is asking
- Return the answer in a client specified format such as xhtml, xml or png
But such a bare bones framework would leave much to be desired. To be truly useful, you would also have to address a number of additional issues, including:
- State handling (generally via sessions)
- Error handling
- Performance (timing, caching, pooling, threading, etc)
So how can you meld all this functionality into a cohesive system that is easy to use and easy to extend?
The standard answer is use the Model-View-Controller pattern. But that still leaves many questions to be answered – particularly on the controller side. How exactly do you implement the controller piece?
A common approach is to use the Decorator pattern made famous by the Gang of Four. The basic idea is to create a chain of decorators as shown in the diagram below.
Each decorator deals with one issue. So you’d have one decorator for unmarshalling, one for authentication, one for session management, etc.
The Smallworld Internet Application Server uses this architecture. Apache Axis and Apache Cocoon use this architecture. And although I’ve never used Django, its middleware classes require a common API which is a telltale sign that they are really decorators in disguise.
So why is the Decorator pattern so popular? Because it let’s you modularize your framework, making it possible to add or remove functionality as needed based on a particular application’s requirements.
Aspect Oriented Programming
Another approach, albeit much rarer, is to use Aspect oriented programming, or AOP, to combine multiple behaviors. Like many computing technologies in use today, aspect oriented programming was invented at Xerox Parc. But it found its home in the Java world via the AspectJ project, which is now part of Eclipse.
There are plenty of articles that explain aspect oriented programming in-depth so I won’t rehash them. But the basic idea is to use a domain specific language (DSL) to combine different pieces of functionality without touching the original code.
The classic example is logging. Adding logging statements to each method is tedious and error prone. Even worse, if your logging API changes you have to go back and touch every part of your system. Thus logging is an example of a cross-cutting concern, something that affects every part of a system.
With a little thought, its easy to see why aspect oriented programming could make it easier to write a web framework. And its appears there is such a thing – the Spring Framework. Although I’ve never used it, the documentation makes it look like it relies heavily on aspect oriented programming.
The Rails Way
So how about Rails? Rails is unusual because it uses Ruby’s limited aspect oriented support to mimic the Decorator pattern.
Ruby supports aspect oriented programming? Well not really. But it does offer a very crude approximation via its alias method and support of modules. Rails leverages this basic support for all its worth – it forms the very heart and soul of Rails.
To see what I mean let’s look at an example. Here is the basic skeleton of Rails session handing functionality, found in actionpack\lib\action_controller:
module ActionController #:nodoc: module SessionManagement #:nodoc: def self.included(base) base.extend(ClassMethods) base.send :alias_method_chain, :process, :session_management_support base.send :alias_method_chain, :process_cleanup, :session_management_support end module ClassMethods # Class methods go here end # Instance methods go here end end
The important things to notice are:
- Behavior is specified using modules instead of classes
- Rails uses Ruby’s include method to add methods defined in a module as instance methods in a host class. By convention the host class is called Base – thus ActionController::Base, ActionView::Base.
- Rails uses Ruby’s extend method to add methods defined in a module as class methods (called static methods in other languages) in a host class. By convention class methods are defined in a sub-module unsurprisingly called ClassMethods.
- Rails uses a custom method called alias_method_chain, which is based on Ruby’s built-in alias method, to chain method calls together.
Rails then loads this code in actionpack-1.13.3\lib\action_controller.rb like this:
require 'action_controller/session_management' ActionController::Base.class_eval do include ActionController::SessionManagement end
The end result is that ActionController::Base now has the following method chain:
process -> process_with_session_management_support -> process_without_session_management_support
Voila. Rails has created a Decorator using Ruby’s limited aspect oriented programming support.
If you look through the Rails source code you’ll see this pattern repeated over and over.
Is This A Good Idea?
Rails implements the Decorator in such an unusual way that its worth asking if it is a good idea or not. In my opinion it isn’t. I don’t see any advantages in Rails’ implementation – do you?
But I do see several disadvantages. The biggest one is that its removes an obvious extension point from Rails. Once two methods are linked together using Ruby’s alias method you can’t break them apart. For example, if A aliases B then you can’t insert C between them. This might not seem like a big deal, but for some extensions it quite important. For example, when I wrote our REST controller implementation last year I struggled mightily to fit it into Rails aliases method calls and finally gave up and took a different approach. Its worth noting that its often possible to work around this problem since Ruby is such a flexible language. Instead of adding a new decorator, in many cases you can replace the original method to achieve what you want. Its not nearly as elegant, but it usually does the trick.
A second disadvantage is that it complicates Rails architecture. What’s the difference between a decorator and a filter? Nothing. By using the Decorator pattern you could merge Rail’s filter functionality with its method chaining functionality, thereby reducing its overall code base.
A third disadvantage, albeit a minor one, is that the method chains Rails creates are hidden. You can’t look at a single piece of code, or configuration file, and see the method chains. The closest is action_controller.rb and action_view.rb, but I can’t get a handle on a chain object at runtime and say “please print yourself on STDOUT so I can see what is happening.”
The fourth, and last, disadvantage is much more subjective and personal. I think Rails is trying to be too cute for its own good. When faced with a standard problem use a standard solution – that is the whole point of design patterns. By trying to be too cute Rails provides a solution that is less flexible and less obvious than the standard approach.
Update – Peter rightly pointed out in the comments that I mistakenly used the Chain of Responsibility Pattern instead of the Decorator pattern. There is a subtle, yet extremely important, difference between the two. In the Chain of Responsibility pattern the first handler that can process a request will do so and the rest of the chain will not be executed. In the Decorator pattern every decorator in the chain will process a request.
I’ve decided to update this entry since I think accuracy is more important the maintaining the original version. Thanks for your feedback Peter!