Rails Edge Architecture
I've talked a bunch about the Rails 3 architecture in various talks, and it's finally come together enough to do a full blog post on it. With that said, there's a few things to keep in mind.
We've done a lot of work on ActionController, but have only mildly refactored ActionView. We're going to tackle that next, but I don't expect nearly as many internal changes as there were for ActionController. Also, we've been working on the ActionController changes for a few months, and have focused quite a bit on maintaining backward compatibility (for the user-facing API) with Rails 2.3. If you try edge and find that we've broken something, definitely let us know.
One of the biggest internal changes is creating a base superclass that is separated from the notions of HTTP. This AbstractController handles the basic notion of controllers, actions, and action dispatching, and not much else.
At that level of abstraction, there are also a number of modules that can be added in, such as Renderer, Layouts, and Callbacks. The API in the AbstractController is very normalized, and it's not intended to be used directly by end users.
For instance, the template renderer takes an
ActionView::Template object and renders it. Developers working with Rails shouldn't have to know about Template objects; this is simply an internal API to make it easier to work with the Rails internals and build things on top of Rails.
Another example is related to action dispatching. At its core, action dispatching is simply taking an action name, calling the method if it exists, or raising an exception if it doesn't. However, other parts of Rails have different semantics. For instance, method_missing is used if the action name is not a public method, and the action is dispatched if no action is present but a template with the same name is present.
In order to make it easy to implement these features at different levels of abstraction, AbstractController::Base calls
method_for_action to determine what method to call for an action. By default, that method simply checks to see if the action is present. In
ActionController::HideActions, that method is overridden to return nil if actions are explicitly removed using hide_action. ActionController::Base further overrides it to return "default_render", which handles the rendering.
That's, in a nutshell, how the new architecture works: simple abstractions that represent a bunch of functionality that you can layer on if you need it.
ActionController::Http layers a Rack abstraction on top of the core controller abstraction so that actions can be called as Rack endpoints. On top of the core rendering functionality available in
AbstractController::Base, ActionController provides rendering functionality that builds on top of HTTP constructs. For instance, the ActionController renderer knows how to set the content type based on the template it rendered. Additionally, ActionController implements more developer-friendly APIs. Instead of having to know about the
ActionView::Template object, developers can just use the name of the template (pretty much the Rails 2.3 rendering API).
ActionController normalizes the developer's inputs into the inputs that AbstractController is expecting, before calling super (
ActionController::Http inherits from
AbstractController::Base). Additional functionality is added via the
ActionController::Rails2Compatibility module, which provides support for things like stripping a leading "/" off of template names, and ActionController::Base, which provides a final layer of normalization (for instance, normalizing render :action => "foo" and render "foo"). As a result,
ActionController::Base ends up being identical to the fully-featured
ActionController::Base that you used in Rails 2.3, and none of your app code needs to change.
As an example, let's look at rendering. If you call render :template => "hello", the first thing that happens is the normalization pass on
ActionController::Base. Since we used a relatively normalized form, not much happens, and the options hash is passed up into the compatibility module, which checks to see if there's a leading "/" in the template name. Since there isn't, it passes the options hash up again into the
There, Renderer checks to see if we've already set the response body. If we have, a DoubleRenderError is raised. If not, render_to_body is called with the options hash. The first place in the chain we find render_to_body is in
ActionController::Renderer, where it processes options like
It then calls super, passing control back into AbstractController, which promptly calls
_determine_template, again with the options. The job of
_determine_template is to take in some options and return the template to render. This hook point is provided so that other modules, like the Layouts module, can use the template that was looked up to control something they care about. In this case, the Layouts module wants to limit the search for a layout to the formats and locale of the template that was actually rendered.
This solves a long-standing problem in Rails where templates and layouts were looked up separately, so it was possible for an HTML layout to be implicitly wrapped around an XML template. No longer :)
The Layouts module gets called first with _determine_template. It calls super, allowing the default template lookup to occur, which populates options[:_template]. It then uses options[:_template] to look up the layout to use, populating options[:_layout]. I hadn't mentioned it before, but AbstractController uses options with leading underscores, leaving undecorated options for subclasses like
After the template to render is determined, Renderer calls _render_template, which makes the call into ActionView.
I know it sounds rather complicated, but a graphic showing the relationships is forthcoming, which should clean things up. The nice thing is that there are several layers of abstraction, and while the final system is complex (mostly because the functionality is complex), it's reasonably easy to understand the higher levels of abstraction on their own. It's also easy to put various kinds of normalization into standard places, so the code you're reading at any given point is code that expects normalized inputs. Since the normalization that Rails does can sometimes be quite gnarly (in the service of making the user experience really pleasant), separating that stuff out can reduce the amount of gnarliness in the internals themselves.
Finally, an important part of an architecture like this is making sure that there is great internal documentation (which we've already started with), and some visualizations that show what's going on. If you were forced to track down the control flow above on your own for the first time, it would probably be non-trivial. So a key part of this architecture is making sure you never have to do that. I would also note that I'm not particularly good at expressing code reading in blog posts, so the process definitely sounds a lot more complex than it actually is ;)