Merging Rails and Merb: A Year Later (Part 1 of 6)

Original author: Yehuda Katz
  • Transfer
Six consecutive articles on the merger of Rails and Merb were published on www.engineyard.com from December 2009 to April 2010. This is the first of them.

Today is exactly one year from the day we announced the merger of Rails and Merb. Then there were many skeptical reviews regarding the success of this enterprise. The most common association among those who heard about our plans was a unicorn. At RailsConf last year, and DHH (David Heinmeier Hansson, author of Rails. - approx. Transl. ), And I mentioned the unicorn in my reports, causing laughter about our enormous expectations and the apparent impossibility of fulfilling them for version 3.0.

A year passed, it was time to reflect how well we worked hard to achieve our goals. Over the next few days, I will describe in detail the progress made towards each of the points in my original post ( http://www.engineyard.com/blog/2008/rails-and-merb-merge/ ).

I have already made several presentations on these topics, so that some of you could already see individual things, but I also wanted to write this down for those who have not had time. I also added information omitted earlier due to its difficulty in giving an oral report, and too new to be told anywhere.

Modularity


Rails will become more modular, starting with the implementation of the kernel itself, including the ability to enable or disable individual components as desired. We will focus on reducing code repetition within Rails to make it possible to replace parts of Rails without interfering with the rest of the parts. This is what Merb's vaunted “modularity” is.

We spent a lot of time at this stage, which really brought us a lot of fruit. I will give some typical examples.

Activesupport

First, we went through ActiveSupport, making it adapted to select the right items. This means that using inflector (translating words from singular to plural and vice versa), time extensions, class extensions, and everything else is now possible without independently studying the dependency graph. Here's what I mean (using the example method to_sentencein ActiveSupport from Rails 2.3):
Copy Source | Copy HTML
module ActiveSupport
  module CoreExtensions
    module Array
      module Conversions
        def to_sentence(options = {})
          ...
          options.assert_valid_keys :words_connector, :two_words_connector, :last_word_connector, :locale
          ...
        end
      ...
    end
  end
end

As you can see, to_sentencethere is a call assert_valid_keysin another module, which means that to connect only active_support/core_ext/array/conversions, you would have to go through the file, find all the implicit dependencies, and connect the corresponding modules separately. And of course, the structure of these dependencies could easily change in a future version of Rails, so hoping for the result would be unsafe. In Rails 3, the same file starts with:
Copy Source | Copy HTML
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/inflector'

This is because we ourselves went through the entire ActiveSupport library, found implicit dependencies, and made them explicit. As a result, you can pull out the specific libraries you need for a small project, and not the entire ActiveSupport.

Better still, parts of Rails now explicitly declare their dependencies on ActiveSupport. So, for example, the code that adds a log entry to the ActionController received the following lines at the beginning:
Copy Source | Copy HTML
require 'active_support/core_ext/logger'
require 'active_support/benchmarkable'

This means that all parts of Rails now know which parts of ActiveSupport they need. For simplicity, Rails 3 comes with a full set of ActiveSupport modules, so you can use things with 3.daysor 3.kilobyteswithout problems. At the same time, if you want more control over which modules are connected to the application, this is possible. You can declare config.active_support.bare = truein the configuration, and only those parts of ActiveSupport that are explicitly specified in the project files will be included. You still have to include different trinkets, if you want to use them, 3.daysit won’t work right out of the box with the flag turned on bare.

Actioncontroller

Another area that needed processing was the ActionController. Previously, the ActionController contained many fundamentally great elements within itself. Upon closer inspection, we found that these were actually three different components.

Firstly, the functionality of the dispatcher. It included the dispatcher itself, routing, middleware and expansion racks. Secondly, it contained a large amount of controller code, designed to be used elsewhere, and actually reused in ActionMailer. Finally, there was code that acted as an intermediary between the two, managing requests and responses through the entire controller architecture.

In Rails 3, each of these components is separate from the other. The functionality of the dispatcher was made in ActionDispatch, its code is shrunk and really turned into a conceptual component. Parts of the ActionController intended for use by non-HTTP controllers have been moved to a new component called AbstractController, from which both ActionController and ActionMailer inherit.

Finally, the ActionController itself has undergone a thorough redesign. In fact, we isolated every single component and made it possible to start with a minimum and complement it with components of your choice. Our old friend ActionController::Basejust starts from there and adds everything you need. For example, look at the start of a new version of this class:
Copy Source | Copy HTML
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 do here is add all available modules, so the initial way to use Rails is the same as before. The really strong side of what we have done here is the same as in ActiveSupport: each module declares its dependencies on other modules, so you can enable it, for example, Renderingwithout having to figure out which other modules should be connected and in which okay.

Here is a fully working Rails 3 controller:
Copy Source | Copy HTML
class FasterController < ActionController::Metal
  abstract!
  # Rendering будет включен лейаутами, но я включу 
  # его тут для понятности
  include ActionController::Rendering
  include ActionController::Layouts
  append_view_path Rails.root.join("app/views")
end
 
class AwesomeController < FasterController
  def index
    render "очень_быстро"
  end
end

And further in the routing file with full success you can do:
Copy Source | Copy HTML
MyApp.routes.draw do
  match "/must_be_fast", :to => "awesome#index"
end

In essence, it has ActionController::Basebecome just one way to create controllers. This is the same as classic Rails, but with the ability to make your own if the original is not to your liking. It is really easy to fit your requirements: if you want to add functionality before_filterto FasterController, you can simply add it AbstractController::Callbacks.

Note that adding these modules automatically added AbstractController::Rendering(rendering functionality is common with ActionMailer), AbstractController::Layoutsand ActiveSupport::Callbacks.

This makes it possible to simply add only the functionality you need in performance-sensitive places without having to use a completely different API. If additional functionality is required, you can easily add additional modules or in the end use the fullActionController::Basewithout having to give up something along the way.

This is, in fact, the idea of ​​the Rails 3 kernel: there are no monolithic components, only modules without complex relationships that work by default in large packages. This allows people to continue to use Rails in the same way as in previous versions, but reinforces the code with the possibilities of alternative options. No more functionality enclosed in inaccessible forms.

The immediate benefit of all this is that ActionMailer intentionally gets all the functionality of an ActionController in a simple way. Everything from layouts and helpers to filters uses the identical code used in ActionController, so that ActionMailer can never slip from the functionality of ActionController (in the process of how the ActionController will develop).

Middleware also receives a helping hand. ActionController::Middleware, a middleware with all the advantages of an ActionController, allows you to add any features of the ActionController as you see fit (like Rendering, ConditionalGet, Request and Response objects, etc.). Here is an example:
Copy Source | Copy HTML
# Длинный способ
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
 

Copy Source | Copy HTML
# Использование дополнительных Rack хелперов
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"
 
    # вы можете делать теперь больше хороших вещей
    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

Copy Source | Copy HTML
# Использование хелперов ConditionalGet
class AddMyName < ActionController::Middleware
  # подключение 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

I think we really fulfilled the promise of modularity in Rails. I think that the level of what happened in the new version exceeds the level of expectations many years ago, and this is definitely the territory of the golden unicorn. Enjoy it!

Next time I’ll talk about performance improvements in Rails 3. I hope this isn’t too fast. :)

Also popular now: