Sunday, October 07, 2007

Ruby: Convention is Important

Good programmers follow convention. Rails teaches that convention over configuration can result in great productivity gains. The two previous statements can apply to any language, but there are 2 other reasons why convention is even more important in Ruby applications.
  • Following conventions can reveal opportunities for metaprogramming.
  • Conventions make it easier to find where behavior is defined. This reason strongly applies as long as we have lacking IDE support.
Metaprogramming can make you more productive. Used correctly, metaprogramming can take a complex relationship and reduce it to something simple, yet intention revealing. The class methods has_many and belongs_to are perfect examples of something that needs to be done in several locations within a codebase, but can be succinctly done thanks to metaprogramming.

Metaprogramming
Metaprogramming isn't exclusive to Rails. I recently joined a project where the class under test needed to be required from within the test. I've been spoiled by Rails, so having to write require statements was annoying to me. To solve the problem, I convinced the team to build their test directory structure as a mirror to the lib directory structure. I also convinced them that using dust to define tests was a good idea. Once we got all the tests using the unit_test do .. end syntax it was easy for me to use the binding of that block to get the path to the test. Since our test directory structure mirrored the lib directory, I was able to gsub the path to the test and require the class under test behind the scenes.

Removing require statements is nice, but it's also obviously a small win. However, it doesn't take long for several small metaprogramming wins to greatly increase the effectiveness of a team. A few projects ago, our small wins ended up becoming a domain specific framework built on top of Rails. A consequence of combining all the small wins was that adding to the application was very trivial. That team saw tremendous effectiveness gains with every story that required adding new a new screen.

Finding Behavior
Last week while pairing with Fred George I asked what he thought of adding a not method to improve readability. He liked the readability, but was concerned with being able to find the implementation of the not method. I think that's a valid concern, but one that can be mitigated by following good conventions. On our project we have a folder called "core_extensions" which groups files such as string_extension.rb, object_extension.rb, etc. I've found this convention helpful because I can generally find behavior after looking in a few classes. For example, I may stumble upon the following code.

payment_connection.establish(:paypal) if payment_connection.not.active?

To find the implementation of not I'll look in the connection class first since the connection is the receiver of the not message.

class Connection
def establish(symbol)
...
@active = true
end

def active?
@active
end
end

Looking in Connection will not provide the implementation of not, but it does provide other valuable information. Notably, Connection is a PORO; therefore the ancestors of Connection are Object and Kernel.

Connection.ancestors # => [Connection, Object, Kernel]

At this point, life is easy. I only need to look in object_extensions.rb and then kernel_extensions.rb if necessary.

But, life was only easy because we follow conventions. I can easily see creating a readability.rb file seeming like a good idea. After all, object_extensions.rb doesn't do much in the way of describing intent. I am a big fan of software that focuses on intent, but I actually think it's destructive in this case if it means breaking convention. Of course, if my IDE could easily find method definitions I would favor an intentful filename.

People new to Ruby are generally afraid of Open Classes. Convention can go a long way to mitigating the risks of Open Classes. In January, I listed a few Class Reopening Hints. These days we follow roughly the same conventions and I still find myself spending very little time looking for implementation definitions; thanks mostly to convention.

5 comments:

  1. Yeah, I definitely agree that convention is much better. From a programming perspective, it's important regardless of language and although you have much better meta programming in Ruby, you can still do some amazing things even simple things such reflection, proxies and delegates.

    More importantly, over a long term, it's much healthier for a system to follow a set of conventions as it helps other people to understand the codebase much faster. When somethings different it should be important, not just because somebody thought it was clever.

    ReplyDelete
  2. This is something I try to hammer into the heads of everyone I work with: conventions (Rails or not) are there to help not hinder.

    I don't know about the whole "not" method thing though. I find that using unless in that case is just as readable without adding any sort of magic:

    payment_connection.establish(:paypal) unless payment_connection.active?

    ReplyDelete
  3. Anonymous4:04 PM

    Sorry, this is bothering me -- and what really bothers me the most is when people don't validate an acronym! What is PORO? In the future could you explain an acronym for those who may not know what it is? Thanks!

    ReplyDelete
  4. Anonymous3:35 AM

    PORO - Plain Old Ruby Object

    ReplyDelete
  5. Anonymous1:43 PM

    `rcov -a` creates defsite/callsite cross references.

    ReplyDelete

Note: Only a member of this blog may post a comment.