Continuing his interesting train of thought, DHH posted on his blog yesterday that Rails is, in fact, not monolithic. In reality, he says, it is quite modular. In the post, he finally fully articulated the rationale behind the excessive use of aliasmethodchain in Rails, which he says is to keep the code even more "modular".
Let's take a look at some of the claims:
Rails is not actually that large
They count all lines including comments and whitespace in Ruby files, thus punishing well-documented and formatted codeAs I said yesterday, I did not actually include comments and whitespace. I specifically provided the command that I used, which removed whitespace-only lines and comment-only lines. In fact, the comment by bitsweat (a member of the Rails Core team) which started this back-and-forth erroneously included comments in Merb's count, when merb-core has about 1 line of comment per line of code. This is why Jeremy incorrectly thought that merb-core was over 15,000 lines of code.
They count tests, thus punishing well-tested codeNeither Jeremy nor I counted tests. I'm not sure which LOC-count he's referring to.
They count bundled dependencies, thus punishing dependency-free codeI did in fact make this mistake, but I disagree with the assertion that bundling dependencies makes code more "modular". For example, bundling xml-simple in Rails causes a conflict with gems that require a newer version of xml-simple than the one bundled with Rails. Conversely, merb-haml has a haml dependency, which means that users can use newer versions of Haml that are released between Merb releases. This statement by David actually teases out a fundamental difference of opinion between Rails and Merb. In effect, Rails prefers to bundle everything to reduce dependencies, while Merb prefers to use the existing Rubygems system so that applications can use different versions of the "bundled" dependencies. Additionally, we don't want to be maintaining bitrotted versions of things like tmail. We'd prefer to rely on gems created by experts in their niche who can maintain, and more importantly, fix bugs in, their code. We fully admit that our approach pushes the limits of Rubygems, but that has forced us to work with rubygems to improve a core piece of Ruby infrastructure.
Rails is actually pretty modularThe arguments made here almost defy reason, but let's go through some of them.
First, Rails can include almost as much or as little of the six major pieces as you prefer.Absolutely. I've referred to this in the past as the Lego vs. Duplo philosophy. Rails has added in a feature to allow you to remove entire blocks of functionality, but isn't built on an architecture that lets you granularly opt-out. Granular opt-out allows you to reuse foundational code without buying into the full set of opinionated defaults. One example of this is our auth system, which allows you to reuse the base auth code, which simply allows you to define strategies inside of a framework, even if you don't want to use our built-in strategies or login views. Granular opt-out builds a community around chunks of code that can be swapped in; having an auth core makes it easy to share small, simple authentication strategies between users of Merb. The next part is the part that makes me incredulous. According to David, because Rails is spread across many files, it is "modular". He describes how you would go about granularly removing certain features:
All these optional parts can actually very easily be turned off as well, if you so please. If you look at actionpack/lib/action_controller.rb, you'll see something like the following:Read that carefully. If you want to opt out of certain parts of Rails, you need to:This is where all the optional bits are being mixed into Action Pack. But they didn't need to be. If you really wanted to, you could just edit this 1 file and remove the optional bits you didn't need and you'd have some 3,500 lines of optional goodies to pick from.
ActionController::Base.class_eval do include ActionController::Flash include ActionController::Benchmarking include ActionController::Caching ...
- Read the source, and figure out which files contain the features you want
- Figure out where the modules in question have been mixed in
- Fork Rails
- Modify your own personal version of Rails to remove modules that have been mixed in
- Never upgrade Rails again (or upgrade Rails and hope they haven't made any changes to the part of the file you've modified)
alias_method_chain makes Rails modularAs I've said many times before, I don't like alias_method_chain. But Rails' philosophy around it is a perfect example of its problems. Superficially, it seems like it divides up responsibilities neatly into their own modules. And again, this is perfectly true from the perspective of developers working on the framework itself. For consumers of the framework, it is simply maddening. Here's a code snippet David provided in his post:
module Benchmarking def self.included(base) base.extend(ClassMethods) base.class_eval do alias_method_chain :perform_action, :benchmark alias_method_chain :render, :benchmark end end
This is an extremely common idiom in Rails. Unfortunately, it makes Rails methods extremely opaque. It is nearly impossible, without reading through a dozen files and putting together the puzzle, to figure out what the performaction method actually does. This is evident in a Rails stack trace, which includes close to 10 frames for different parts of performaction.
And this aliasmethodchain isn't even greppable. Effectively, it's up to the user to divine that including a module modifies methods in the class, since it's done via metaprogramming in an included hook. Again, this all makes perfect sense for developers on the Rails framework. But for consumers of the framework, it leads to many frustrating days trying to track down all the pieces of a particular method.
But why bother?
Again, this question makes perfect sense coming from the perspective of using Rails as an internal library. Don't need something, like pagination? Simply move it out of the core framework. From the perspective of trying to develop something that can be used for many different purposes, in many different situations, we have different priorities.
Merb has already been used as the base for SproutCore, which generates static HTML out of a Merb base. It has been used for sinatra-like web services. And it's being used by several large companies who don't want to use DataMapper (and are using Sequel instead).
As people use Merb for more kinds of things, decisions like hardcoding features of Merb to a particular ORM (like Rails has done), requiring a certain directory structure, or bundling in dependencies become much harder to justify. People using Merb actually do use other gems that conflict with Rails bundled dependencies. People using Merb want to be able to upgrade their versions of Haml, ParseTree, or MailFactory without expensive surgery to the framework itself.
For the moment, these differences are the reason that Rails will continue to dominate amongst developers seeking to build apps similar in scope to apps built by 37Signals. I suspect that Merb will pick up steam amongst developers looking to build innovative apps leveraging the latest and greatest Ruby techniques and libraries.
I, for one, spent several years building large Rails applications, and am happy to leave the "just keep your own frozen, patched copy of Rails" philosophy behind.
UPDATE: Just to be clear, I know that Rails tries to use more recent versions of gems if they're available on the system. However, that is a very naïve dependency approach, that in our experience, produced issues. It's much better to be warned that you have dependency conflicts up front than to have them manifest in running production code.