Rails 3: The Great Decoupling
In working on Rails 3 over the past 6 months, I have focsed rather extensively on decoupling components from each other.
Why should ActionController care whether it's talking to ActionView or just something that duck-types like ActionView? Of course, the key to making this work well is to keep the interfaces between components as small as possible, so that implementing an ActionView lookalike is a matter of implementing just a few methods, not dozens.
While I was preparing for my talk at RubyKaigi, I was trying to find the smallest possible examples that demonstrate some of this stuff. It went really well, but I noticed a few areas that could be improved even further, producing an even more compelling demonstration.
This weekend, I focused on cleaning up those interfaces, so we have small and clearly documented mechanisms for interfacing with Rails components. I want to focus on ActionView in this post, which I'll demonstrate with an example.
$:.push "rails/activesupport/lib"
$:.push "rails/actionpack/lib"
require "action_controller"
class Kaigi < ActionController::Http
include AbstractController::Callbacks
include ActionController::RackConvenience
include ActionController::Renderer
include ActionController::Layouts
include ActionView::Context
before_filter :set_name
append_view_path "views"
def _action_view
self
end
def controller
self
end
DEFAULT_LAYOUT = Object.new.tap {|l| def l.render(*) yield end }
def _render_template_from_controller(template, layout = DEFAULT_LAYOUT, options = {}, partial = false)
ret = template.render(self, {})
layout.render(self, {}) { ret }
end
def index
render :template => "template"
end
def alt
render :template => "template", :layout => "alt"
end
private
def set_name
@name = params[:name]
end
end
app = Rack::Builder.new do
map("/kaigi") { run Kaigi.action(:index) }
map("/kaigi/alt") { run Kaigi.action(:alt) }
end.to_app
Rack::Handler::Mongrel.run app, :Port => 3000
There's a bunch going on here, but the important thing is that you can run this file with just ruby, and it'll serve up /kaigi
and /kaigi/alt
. It will serve templates from the local "/views" directory, and correctly handle before filters just fine.
Let's look at this a piece at a time:
$:.push "rails/activesupport/lib"
$:.push "rails/actionpack/lib"
require "action_controller"
This is just boilerplace. I symlinked rails to a directory under this file and required action_controller. Note that simply requiring ActionController is extremely cheap -- no features have been used yet
class Kaigi < ActionController::Http
include AbstractController::Callbacks
include ActionController::RackConvenience
include ActionController::Renderer
include ActionController::Layouts
include ActionView::Context
end
I inherited my class from ActionController::Http. I then included a number of features, include Rack convenience methods (request/response), the Renderer, and Layouts. I also made the controller itself the view context. I will discuss this more in just a moment.
before_filter :set_name
This is the normal Rail before_filter. I didn't need to do anything else to get this functionality other than include AbstractController::Callbacks
append_view_path "views"
Because we're not in a Rails app, our view paths haven't been pre-populated. No problem: it's just a one-liner to set them ourselves.
The next part is the interesting part. In Rails 3, while ActionView::Base remains the default view context, the interface between ActionController and ActionView is extremely well defined. Specifically:
- A view context must include ActionView::Context. This just adds the compiled templates, so they can be called from the context
- A view context must provide a _render_template_from_controller method, which takes a template object, a layout, and additional options
- A view context may optionally also provide a _render_partial_from_controller, to handle
render :partial => @some_object
- In order to use ActionView::Helpers, a view context must have a pointer back to its original controller
That's it! That's the entire ActionController<=>ActionView interface.
def _action_view
self
end
def controller
self
end
Here, we specify that the view context is just self
, and define controller
, required by view contexts. Effectively, we have merged the controller and view context (mainly just to see if it could be done ;) )
DEFAULT_LAYOUT = Object.new.tap {|l| def l.render(*) yield end }
Next, we make a default layout. This is just a simple proc that provides a render method that yields to the block. It will simplify:
def _render_template_from_controller(template, layout = DEFAULT_LAYOUT, options = {}, partial = false)
ret = template.render(self, {})
layout.render(self, {}) { ret }
end
Here, we supply the required _render_template_from_controller. The template object that is passed in is a standard Rails Template which has a render method on it. That method takes the view context and any locals. For this example, we pass in self
as the view context, and do not provide any locals. Next, we call render on the layout, passing in the return value of template.render. The reason we created a default is to make the case of a layout identical to the case without.
def index
render :template => "template"
end
def alt
render :template => "template", :layout => "alt"
end
private
def set_name
@name = params[:name]
end
This is a standard Rails controller.
app = Rack::Builder.new do
map("/kaigi") { run Kaigi.action(:index) }
map("/kaigi/alt") { run Kaigi.action(:alt) }
end.to_app
Rack::Handler::Mongrel.run app, :Port => 3000
Finally, rather than use the Rails router, we just wire the controller up directly using Rack. In Rails 3, ControllerName.action(:action_name) returns a rack-compatible endpoint, so we can wire them up directly.
And that's all there is to it!
Note: I'm not sure if I still need to say this, but stuff like this is purely a demonstration of the power of the internals, and does not reflect changes to the public API or the way people use Rails by default. Everyone on the Rails team is strongly committed to retaining the same excellent startup experience and set of good conventional defaults. That will not be changing in 3.0.