Rails and Merb Merge: The Anniversary (Part 1 of 6)
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 assert_valid_keys, 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. :)
Share your thoughts with @engineyard on Twitter