One thing that I quickly noticed about Ruby on Rails is that you can do a lot more with less code. This is one of the benefits of working in a dynamic language like Ruby that does not have as many restrictions as a static typed language. The end result is that you have smaller methods, more descriptive code, fewer chances to create bugs, and it becomes easier to get things done. Let me show you what I mean.
Let’s say that I have a database table that looks like this:
create table Users
(
id int,
name varchar(100),
user_status_id int,
created_at datetime,
updated_at datetime
)
My Ruby class looks like this:
class User < ActiveRecord::Base
belongs_to :user_status
has_many :roles, :through => :user_roles
validates_length_of :name, :maximum => 100
named_scope :active, :conditions => ["user_status.id = ?", UserStatus::ACTIVE]
end
My class only has 6 lines, but it does an awful lot. Let’s go through it line by line:
class User < ActiveRecord::Base
Because I have a table in the database called "Users", I now have the following methods on my User class:
- id, id=, name, name=, user_status_id, user_status_id=, created_at, created_at=, updated_at, updated_at=
- find, find_by_id, find_by_name, find_by_user_status_id, find_by_name_and_user_status_id, find_by_id_and_name_and_user_status_id, and any other combination of columns I might want to query on
Because I derive from ActiveRecord::Base, I inherit a lot of data access methods like save, delete, destroy, and many more.
All I've done so far is made my class declaration! Look at everything I'm getting for free.
belongs_to :user_status
My User object has a many-to-one relationship to the User_Status table (and it's corresponding UserStatus object). I now have the "user_status" and "user_status=" methods on my object. The user status will be lazy loaded, so it won't do any database queries to get the user status until I actually need it.
has_many :roles, :through => :user_roles
My User object has a many-to-many relationship with the roles table, and in the database there is a user_roles table that serves as a mapping table. I now have a "roles" method on my User class that will give me the list of roles, and I can add new roles to the collection without having to worry about creating anything for the mapping table.
validates_length_of :name, :maximum => 100
Name is required and cannot be longer than 100 characters.
named_scope :active, :conditions => ["user_status.id = ?", UserStatus::ACTIVE]
Now I can write "User.active", which will return all active users. This is equivalent to User.find_by_user_status_id(UserStatus::ACTIVE), but now I've abstracted away that concept of an active user so that other classes don't have to know how the internals work.
end
OK, well, every language has to have some ceremony.
I haven't created any methods yet! Already my object has all of the data access methods that it needs, it will validate the object before it saves, it knows about related objects, and I abstracted away the concept of an active user.
Note that not only have I been able to do all of this with very few lines of code, the code that I did write was very simple and readable. Since the code is so simple, I'm not going to write unit tests for it either (the tests would basically be the same one line of code as the implementation code in most cases).
All of this means that I can do things much easier than before. In .NET, I would've had to create finder methods by hand. I would've had to add all of the properties that match up to my database columns by hand. I don't know how I would duplicate named_scope in a .NET ORM, maybe it's possible somehow. I've never found a way to do it. Maybe I would have to write a stored procedure.
To sum it up, all of the boring crap is done for me and I can pretty much immediately start writing code that provides business value, which is what we're getting paid to do. No ADO.NET, no more mapping my database table to my entity object, no more modifying several files just to create the same kinds of methods that I have to create for every new entity object that I create.
All of this is helping me to achieve what I've always wanted in software development: the ability to write code that says what I want to do and does it without having to do lots of typing.
This is one of the things that appeals to me about object databases like db4o: just use POCOs, no O/R mapping necessary, all the CRUD is already done, and writing a Linq query is all you need for anything beyond simple CRUD.
…and check out all the responsibilities that class has! Validation/establishing object relationships/crud/querying/business logic…more?
If you crammed all that into a .Net class (which arguably you could achieve by composing a class with some Linq object references) wouldn’t you be questioning the author’s sanity?
Why do you get a free pass on SRP with Rails? (That is not rhetorical, it’s a serious question!)
As always, less code means less maintainability. You (or rails) violate the Single Responsibility Principle here: mixing your class behavior with database mapping and validation. Might be cool for a homepage, but a nightmare for a big project. Also “user belongs_to user_status” reads just *horrible*!!! Also, how can has code first?
Looks like a poor version of Linq2Sql to me.. Yes it doesn’t autogenerate the find_by_… methods, but with Linq it’s about as simple to write a Linq query.
@mgroves rails doesn’t have Linq as far as I know..
I think maybe that the violation of SRP is okay ONLY if it’s on objects that act as DTOs between the data layer and the business layer. If you cram so many responsibilities into such a small space, then you need to translate it into something more SOLID as soon as possible. But then I have had these discussions (about my discomfort with active record) with Jon before.
It’s why I don’t think I’ll ever be a Ruby fanboy, and it’s also why I think it’s “domain” is in building web presentation tiers, and not so much in the business tier.
@ulu “As always, less code means less maintainability.” This is the exact opposite of my philosophy. Why would it be easier to maintain MORE code? Seems to me five, five line classes is alot easier to maintain than ten, ten line classes.
Also, Once you are accustomed to reading the association declarations they are very easy to follow. However, the belongs_to :user_status is a little odd. Why would a user belong to a status, and not has_many :user_statuses? But this is probably besides the point of the post.
There was too much to respond to in a comment, so I just wrote up another post with my response to all of the comments here.
http://jonkruger.com/blog/2010/10/19/ruby-on-rails-and-the-single-responsibility-principle/
Well, db4o is only for Java and .NET/Mono, but I brought it up because I think it reduces “ceremonial” code in a similar way.
Micheal,
The necessity of a distinction between the “data layer” and the “business layer” is somewhat of a moot point if your data layer is so lightweight that you don’t need to think about it.
Principles are a means for experts to communicate rules to novices. They should be treated as guidlines, but it takes true prowess to be able to know when it makes sense to apply the principle or build new ones.
I don’t think you need to be a Ruby fanboy to be able to realize that various languages have their advantages/disadvantages. Ruby is definitely for the business tier; the business just hasn’t realized the full advantage yet.
Hate to mince words, but really that’s what this argument is hinging on.
The word “principle” has a much stronger meaning than this conversation is giving it.
Merriam Webster – “Principle”
Perhaps a revision is in order? How about “SOLID guidelines”.
SOLID guidelines… I like that! Too many principles and best practices are treated more like rules and laws than suggestions or ideas.
Ben,
In the case that the data layer is simply a “pass-through”, then you have a point (with caveats). As long as you are in full control of the complexity of your persistence, then it’s reasonable to eliminate the distinction. If however, you’re not in control of the database, or if you rely on external services or external databases for your data, then the indirection will protect you from leaking that complexity into your business layer.
Michael, in the cases of external data sources, you can build in a proxy layer, but I wouldn’t use ActiveRecord for that. I have many times built in a transaction data model to handle the processing of web services. That seems to provide a robustness to log status and reprocess if necessary.
Not quite as elegant as named_scope, but NH can do similar. See ayende’s post here: http://ayende.com/Blog/archive/2009/04/07/nhibernate-mapping-ltpropertygt.aspx
Search for CountOfPosts. Integer being computed here, but could be a bit.