Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; his 9-to-5 home is at the startup he founded, Tilde Inc.. There he works on Skylight, the smart profiler for Rails, and does Ember.js consulting. He is best known for his open source work, which also includes Thor and Handlebars. He travels the world doing open source evangelism and web standards work.

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.

24 Responses to “Rails 3: The Great Decoupling”

Looking nice, dude. Certainly more merby.

Meant to say, also; it’s great that you can wire controllers directly upto Rack like that.

@AJ it’s actually a significant improvement on the Merb architecture :) But yeah — I’ve been about clean public interfaces for a couple of years.

Nice! Thanks!

What about ActiveRecord stuff like validations? Will it still be a feature of AR or extracted so you can use it to add validations to just any class (somethign I find to be useful).

I agree with the Merby Feel.
Since I derailed and started my learning with Merb rather than Rails, it perfectly resonates.
Great job, wycats

One thing I’m looking to do is set a fall-back view directory. So if I’m in PostsController#index, the template in app/views/default/index.html.erb will be used if app/views/posts/index.html.erb isn’t found. In Rails 2.x, I wasn’t able to find a hook that would allow doing this very cleanly. Is there a good place in Rails 3 that I can hook this in?

@craig So you’re saying you want to use a fallback *prefix* in the same view path? In Rails 3, you can override _determine_template to provide the fallback path. I have to think about how that would impact layouts and get back to you with the right solution, but that would be the right hook point.

@Christian I talked about this some at RubyKaigi, and the answer is yes. Josh has been working on ActiveModel, which lets you use Rails callbacks, validations, state machine, etc. in plain old Ruby objects. You can check out my slides at http://files.me.com/wycats/o2snu8

Is there an example of a layout file somewhere?

Great work!

Do you have any plans to use modules instead of inheritance for the basic ActiveRecord / ActionController functionality?

@wycats,

http://files.me.com/wycats/o2snu8

Your link gives me a corrupted pdf file. Pl. provide a fresh link

Thanks

@wycats @railsgirl

I have uploaded the new file at Rapidshare.

Here’s the link:- http://bit.ly/QyuPK

Hope this helps all.

Frameworks and CMS like zena will greatly benefit from this kind of clear internal interfaces. Guessing and discovering how to sneak into view context while in the controller was a pain and subject to break as any new version of rails came out.

Thanks to this decoupling, it seems things will get much easier.

G.

So I am looking to start a new website and I’m going to join the web revolution. But I’m stuck in the middle.

Do I learn Merb or Rails?

@Peter, go for Rails. It’s easier to learn, there are plenty examples, good documentation and books available. After you are familiar with Rails and want to go more advanced, Rails 3.0 is probably available to you. :-)

Great work. Many thanks to you and everyone in the core team. Got MEAP for Rails 3 in Action just now. I’m trying to learn to the concepts behind Merb. I really don’t know the difference, honestly.

yehuda -

you say

“In order to use ActionView::Helpers, a view context must have a pointer back to its original controller”

but why is that? frameworks like ramaze have always have had features like Session.current, Controller.current, View.current, which are immensely useful (they are Thread local vars btw). i personally always have a before filter that sets the current controller in my rails app. why no make the interface even the smaller – it’s the controller that renders the view so why shouldn’t it provide a context (current_controller) under which the view could be rendered?

if you think about it this elegantly also elimnates the need for the _render…controller and _render_…partial interfaces since the template merely need ask (where context==@controller || context==@object)

if context.partial?

or

if context.controller?

or some such. put another way, if simple public interfaces are good, then recursively simple public interfaces are better. aka – the view interface can be collapsed if the context itself has a simple interface and the template can ask it’s context questions.

i think one can make a very, very strong argument that the minimal/best interface to any stack component is ‘.call(*args)’ and it’s certainly possible here too.

food for thought. i am very, very happy about the direction rails is going these days – keep up the good work.

@Christian – check out the validations file in the sequel dist for extraction of that feature. i use it in several of my projects. ‘sudo gem install assistance’

Hello!! I’m from Brazil and I’m reading an article yours translated to portuguese by a student brazilian named Helton Duarte. This is excellent! It’s a great begin! Congratulations!I’d like to see more translations like that.
http://heltonduarte.com/2009/09/12/minhas-10-coisas-favoritas-sobre-ruby/

:)

I’m getting my rails addiction back… Nevr thought of rails 3 will be this good

Leave a Reply

Archives

Categories

Meta