Engine Yard Blog RSS Feed

A year ago today, we announced that Rails and Merb would merge. At the time, there was much skepticism about the likelihood of the success of this endeavor. Indeed, The most common imagery invoked by those who learned about our plans was a unicorn. At RailsConf last year (well into the effort), both DHH and I used unicorns in our talks, poking fun at the vast expectations we'd set, and the apparent impossibility of achieving everything we'd said we wanted to achieve for 3.0.

A year has gone by, so it's a good time to reflect on how well we've done at achieving those expectations. Over the next few days, I'll take each bullet point that I provided in my original post, and go into detail about the progress we've made on that front.

I've given a few recent talks on these topics, so some of you may already have seen some of this, but I wanted to get it down in writing for those who hadn't. I've also added new information, some of which was omitted because it was difficult to explain in a talk, and some of which is too current for any of my recent talks.

Modularity

Rails will become more modular, starting with a rails-core, and including the ability to opt in or out of specific components. We will focus on reducing coupling across Rails, and making it possible to replace parts of Rails without disturbing other parts. This is exactly what Merb means when it touts "modularity".

We've spent a significant amount of time on this step which has been really fruitful. I'll give a few specific examples.

ActiveSupport

First, we've gone through ActiveSupport, making it viable to cherry-pick specific elements. This means that using ActiveSupport's inflector, time extensions, class extensions, or anything your heart desires is now possible without having to personally track the dependency graph. Here's an example of what I mean, from the to_sentence method in ActiveSupport from Rails 2.3:



module ActiveSupport #:nodoc:
  module CoreExtensions #:nodoc:
    module Array #:nodoc:
      module Conversions
        def to_sentence(options = {})
          ...
          options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
          ...
        end
        ...
      end
    end
  end
end

As you can see, to_sentence has an implicit requirement on assertvalidkeys, which means that in order to cherry-pick active_support/core_ext/array/conversions, you are forced to work through the file, find any unsatisfied dependencies, and be sure to require them as well. And of course, the structure of these dependencies could easily change in a future version of Rails, so relying on what you'd found would be unsafe. In Rails 3, the top of that same file looks like:


require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/inflector'

This is because we've gone through the entire ActiveSupport library, found the unsatisfied dependencies, and made them explicit. As a result, you can pull out the specific libraries you want for a small project, and not get the full weight of ActiveSupport.

Even better, other parts of Rails now explicitly declare the dependencies they have on ActiveSupport. So for instance, the code that adds logging support to ActionController has the following lines on top:


require 'active_support/core_ext/logger'
require 'active_support/benchmarkable'

This means that all of Rails knows what parts of ActiveSupport are needed. For simplicity, Rails 3 ships with all of ActiveSupport still provided, so you'll be able to use things like 3.days or 3.kilobytes without interruption. However, if you want more control over what gets included, that's possible. You can declare config.active_support.bare = true in your configuration and we'll pull in only the parts of ActiveSupport explicitly needed for the parts of Rails that you use. You'll still need to include the fancy parts if you want to use them - 3.days wont work out of the box with bare enabled.

ActionController

Another area that really needed an overhaul was ActionController. Previously, ActionController had a number of disparate elements all in one place. When we looked closely, we found that there were really three discrete components masquerading as one.

First, there was the dispatching functionality. This included the dispatcher itself, routing, middleware, and rack extensions. Second there was generic controller code that was meant to be reused elsewhere, and was in fact reused in ActionMailer. Finally, there was the subset of controller code that brought those two concerns together: code that handled requests and responses through a controller architecture.

In Rails 3, each of those components has been separated out. The dispatcher functionality has been moved into ActionDispatch, with the code inside tightened up and really made a conceptual component. The parts of ActionController that were meant to be reused by non-HTTP controllers was moved into a new component called AbstractController, which both ActionController and ActionMailer inherit from.

Finally, ActionController itself has gotten a significant overhaul. Essentially, we've isolated every standalone component, and made it possible to start with a stripped-down controller and pull in just the components you want. Our old friend ActionController::Base simply starts with that same stripped-down controller and pulls everything in. For instance, take a look at the beginning of the new version of that class:


module ActionController
  class Base < Metal
    abstract!

    include AbstractController::Callbacks
    include AbstractController::Logger

    include ActionController::Helpers
    include ActionController::HideActions
    include ActionController::UrlFor
    include ActionController::Redirecting
    include ActionController::Rendering
    include ActionController::Renderers::All
    include ActionController::Layouts
    include ActionController::ConditionalGet
    include ActionController::RackDelegation
    include ActionController::Logger
    include ActionController::Benchmarking
    include ActionController::Configuration

All we're doing here is pulling in every available module, so the default experience of Rails is the same as before. However, the real power of what we've done here is the same as what we've done in ActiveSupport: every module declares its dependencies on other modules, so you can pull in Rendering, for instance, without having to wonder what other modules need to be included and in what order.

The following is a perfectly valid controller in Rails 3:


class FasterController < ActionController::Metal
  abstract!

  # Rendering would be pulled in by layouts, but I include
  # it here for clarity
  include ActionController::Rendering
  include ActionController::Layouts

  append_view_path Rails.root.join("app/views")
end

class AwesomeController < FasterController
  def index
    render "so_speedy"
  end
end

And then, in your routes, it would be perfectly valid to do:


MyApp.routes.draw do
  match "/must_be_fast", :to => "awesome#index"
end

Essentially, ActionController::Base has become just one way to express your controllers. Think of it like Rails Classic, with the ability to roll your own if you're not so into that taste. It's really easy to mix and match too: if you wanted to pull in before_filter functionality to FasterController, we could simply include AbstractController::Callbacks.

Note that without doing anything else, including those modules pulled in AbstractController::Rendering (the subset of rendering functionality shared with ActionMailer), AbstractController::Layouts, and ActiveSupport::Callbacks.

This makes it really possible to trivially pull in just the specific functionality you need in performance-sensitive cases without having to use an entirely different API. If you need additional functionality, you can easily just pull in additional modules or eventually upgrade to the full ActionController::Base without needing to rip anything apart along the way.

This, in fact, is a core idea of Rails 3: there are no monolithic components, only modules that work seamlessly together in a great package of defaults. This allows people to continue using Rails as they have used it successfully in previous versions, but really leverage the codebase for alternative uses. No more functionality locked away in non-reusable forms.

One nice immediate benefit of all of this is that ActionMailer gets all of the functionality of ActionController in a clean, intentional way. Everything from layouts and helpers to filters is using the identical code that ActionController uses, so ActionMailer can never again drift away from the functionality of ActionController (as ActionController itself evolves).

Middleware gets a helping hand too. ActionController::Middleware, which is middleware with all of the powers of ActionController, allows you to pull in whatever ActionController features you want (like Rendering, ConditionalGet, robust Request and Response objects, and more) as needed. Here's an example:


# The long way
class AddMyName < ActionController::Middleware
  def call(env)
    status, headers, body = @app.call(env)
    headers \["X-Author"\] = "Yehuda Katz"
    headers \["Content-Type"\] = "application/xml"

    etag = env \["If-None-Match"\]
    key = ActiveSupport::Cache.expand_cache_key(body + "Yehuda Katz")
    headers \["ETag"\] = %["#{Digest::MD5.hexdigest(key)}"]
    if headers \["ETag"\] == etag
      headers["Cache-Control" = "public"]
      return [304, headers,  \[" "\]]
    end

    return status, headers, body
  end
end

# Using extra Rack helpers
class AddMyName < ActionController::Middleware
  include ActionController::RackDelegation

  def call(env)
    self.status, self.headers, self.response_body = @app.call(env)

    headers \["X-Author"\] = "Yehuda Katz"

    # but you can do more nice stuff now
    self.content_type = Mime::XML # delegates to the response
    response.etag = "#{response.body}Yehuda Katz"
    response.cache_control[:public] = true

    self.status, self.response_body = 304, nil if request.fresh?(response)

    response.to_a
  end
end

# Using ConditionalGet helpers
class AddMyName < ActionController::Middleware
  # pulls in RackDelegation
  include ActionController::ConditionalGet

  def call(env)
    self.status, self.headers, self.response_body = @app.call(env)

    headers \["X-Author"\] = "Yehuda Katz"

    self.content_type = Mime::XML
    fresh_when :etag => "#{response.body}Yehuda Katz", :public => true

    response.to_a
  end
end

In all, I really think we've delivered on our promise to bring significant modularity improvements to Rails (and then some). In fact, I think the level of success that we've had with this version exceeds most people's expectations of a year ago, and is solidly in golden unicorn territory. Enjoy!

Next, I'll talk about bringing performance improvements to Rails 3. Hopefully it won't go by too fast. :)


Tagged:

comments powered by Disqus