Let me take a very simple example. Let's say you need to write code that needs to trigger something when a post shows up on a specific blog. Here are 2 approaches to this simple programming drift.
The naive approach
You need the job done, you go the procedural way. It's quick, and it works.
#!/usr/bin/env ruby
require 'open-uri'
require "rexml/document"
require 'rexml/xpath'
feed_link = "http://feeds.feedburner.com/37signals/beMH"
title, published_date = "", ""
TITLE_PATH = "//item//title"
DATE_PATH = "//item/pubDate"
while true do
open(feed_link) do |feed|
doc = REXML::Document.new feed
fetched_title = REXML::XPath.first( doc, TITLE_PATH ).text
fetched_published_date = REXML::XPath.first( doc, DATE_PATH ).text
if title != fetched_title
puts "#{fetched_title} (#{fetched_published_date})"
system 'xclock' #trigger something
title = fetched_title
published_date = fetched_published_date
end
end
#wait 5 minutes
sleep 5*60
end
The Ruby-way approach
The naive approach worked, but it does not produce Ruby code, to be a tad nagging. The Ruby-way brings proper abstractions in the picture; a sense of high-level. Props to Mats for the advice on that one.
#!/usr/bin/env ruby
require 'rubygems'
require 'open-uri'
require 'feed-normalizer'
def every seconds, &block
while true
yield
sleep seconds
end
end
class Fixnum
def minutes
self * 60
end
end
class Feed
def initialize url
@url = url
@title = nil
end
def has_recent_post? &trigger
feed = FeedNormalizer::FeedNormalizer.parse open(@url)
fetched_entry = *feed.entries.first
title = fetched_entry.title
published_date = fetched_entry.last_updated
if title != @title
puts "#{title} (#{published_date})"
@title = title
trigger.call
end
end
end
#high-level functionality
feed = Feed.new "http://feeds.feedburner.com/37signals/beMH"
every 5.minutes do
feed.has_recent_post? do
`xclock` #trigger something
end
end
Nicer! You see the style? There are a few changes, and most of them are for the better.
Observations and impacts
everymethod as a top-level construct provides for an interesting abstraction- monkey patching of
Fixnumadds code readability - the
Feedclass brings a useful model FeedNormalizergem removes the feed parsing logicFeedNormalizergem supports both RSS and Atom- the high-level functionality at the end reads like English
- more LOC is a minus (although
every,FixnumandFeedcould be reused elsewhere)
The learning beyond the basics
With Java, once you learned the basics (for example via a Sun Certified Java Programmer certification), you're pretty much all set with the language.
With Ruby, after you learned the basics (for example via the PickAxe), you're not done with the language. You need to become familiar with the idioms and discover all the sugar syntax. You need to acquire the Ruby-way.
And of course you will improve your programming skills by being receptive to best practices and patterns, but that's common to both Ruby and Java.
The art
Acquiring the Ruby-way takes time. It's not only about using OO principles. It's also about whether or not you should use monkey patching, choosing the proper abstraction (a top-level method, a module, or a class?), devising elegant closures, as well as what I could call phrasing your code (when is it enough English-like?). It feels like you have important choices to make, which requires responsibility and creativity. Mastering the basics, although necessary, is not enough.
You also need to develop your style. I would dare to say that programming style in Java is enforced by the company code standards, whereas programming style in Ruby comes from the personality and the programming experience.
Ruby programming is art. And it's part of the reasons why it's fun.
9 comments:
Great Example!
Thanks!
Keep it local:
def minutes = 5
every (5 * minutes) do
Equivalent syntax, no complications.
Yeah Ruby definitely has more flexibility than Java in a lot of ways, and it takes time to learn the best way of doing a lot of things once you have the choice. Unfortunately most don't take the time. :(
An excellent example in the 'proper' use of Ruby. Great post!
What is your opinion on libraries patching classes? For example if the FeedNormalizer gem modified minutes then by redefining it you just caused an inadvertent bug that is not easily detectable. I definitely like the ruby better in the higher level version, but I recently had a problem where two libraries conflicted! What do you think we can do to avoid that in the future?
If things keep going the way they are where every big library starts making their own version of the base classes I think we will get to a point when updating gems will cause errors in others and even though the higher level code is readable development time will be spent going through and hacking the libraries to work together which is much tougher then the procedural method.
@Greg:
There are a few things wrong with that:
- def defines a method, don't you mean minutes = 5?
- 5 * minutes would be 5 * 5 = 25 :)
- rails has a minutes method, so fixing the code would be something like
minutes = 5
every minutes.minutes do
Doesn't sound like the correct approach to me :s
This is an absolutely beautiful code.
Excellent :)
@all: Thanks for the good words.
@daicoden: you had me write a post about monkey-patching. You should thus have some of my personal insights on the issue!
Hey Martin!
Very nice article! Great to see that you are on your way to becoming a Ruby ninja! Java free for how long now? :-)
Let me know if you are back in Stockholm again. Would be nice to hook up. I have a goal to make it to Canada one day too...
Cheers
Peter
I just have two questions:
1. Why do you use:
fetched_entry = *feed.entries.first
instead of
fetched_entry = feed.entries.first
2. Why do you use:
trigger.call
instead of
yield
as you used in the minutes method.
Thank yo
Post a Comment