Home>Articles>
Ruby off the Rails
Ruby on Rails is just one facet of what makes Ruby great, just like EJB is only part of the Java™ enterprise platform. Andrew Glover digs beneath the hype for a look at what Java developers can do with Ruby, all by itself.
Before I can even begin this article, I need to clarify something. First, this is not an article about Ruby on Rails. If you want to read about Rails, articles and blogs are published weekly (maybe hourly) extolling the manifold features of this exciting framework; see Resources for a list to start from. Second, this article does not foretell the collapse of the Java platform in the face of better languages, tools, and frameworks like Ruby on Rails. So this article is about neither of the subjects most commonly associated with Ruby of late.
Don't get me wrong -- I think Rails is fabulous! It's amazingly powerful and has clearly changed the face and pace of Web development. My only point is that there's more to Ruby than Rails, especially from a Java developer's perspective.
The specialty of Rails is Web site development; however, I don't find myself building Web sites all that often. Most of the Web sites I work on have already been built using Struts, Tapestry, or some other technology. Primarily, when I utilize Ruby, I use it as part of a development practice that hinges on the Java platform. So in this article, I'll write about how to develop in Ruby if you're primarily a Java developer.
|
Ruby's syntax is different from that of the Java language. First, Ruby has no brackets or semicolons, and it makes types completely optional. Some might say that Ruby's syntax is terse, and it's that way with a purpose: this language lets you create concise working code in short order.
You can see this for yourself by comparing the same classes, defined first in the Java language and then in Ruby. I'll start with two classes -- Word and Definition (like in a dictionary) -- in the Java language. In the simple class diagram of Figure 1, you can see that the two classes share a few relationships (just bear with me if all this complexity seems contrived: it serves a purpose!):
- A
Wordcan have a collection of synonyms (which are instances ofWords). - A
Wordalso has a collection ofDefinitions. - A
Definitionhas an aggregation association to aWord.
Figure 1. A simple dictionary with words and definitions
Class definition in the Java language
In Listing 1, I define the Word class in the Java language. Note the relationship validation I had to do with respect to my collection of Definitions and synonyms. This is necessary because in the example (as coded), Definitions can be created without a Word relationship initially and Words can be defined without Definitions initially, too.
Listing 1. A Word class in the Java language
|
The Word class in Listing 1 is fairly simple -- it's a JavaBean with a constructor chain allowing users to create Words with various properties set. Also note that both its synonyms and definitions properties are intended to be read-only (that is, there is no setter for them). You can only add an instance of a Definition or another Word for a synonym.
In Listing 2, you see the related Definition class, which is similar to the Word class in that its exampleSentences property doesn't have a corresponding set() method:
Listing 2. A Definition class in the Java language
|
In Listing 3, you can see the same two classes defined in Ruby. Listing 3 does look quite different, doesn't it?
Listing 3. The same classes in Ruby
|
If there's one thing you'll notice from Listing 3, it's that Ruby's syntax is quite terse. But don't let its brevity fool you -- there's a lot going on in that code! First, both classes are defined in a module, which is essentially a package in the Java language. Moreover, I was able to define the classes in one file -- not two -- as required in the Java language. You'll also note that Ruby's constructors are named initialize, whereas in the Java language, constructors are named using the class name.
Creating new object instances is different in Ruby. Rather than the new ObjectInstance() syntax used in Java code, Ruby actually supports calling a new method on an object, which internally calls the initialize method. In Listing 4, you can see how I create an instance of a Word and some corresponding Definitions in Ruby:
Listing 4. Creating a new object instance in Ruby
|
In Listing 4, I "imported" the dictionary module with Ruby's require method (which can be found in the Kernel class). I then proceeded to create a new instance of a Word (ebullient) via the Object.new syntax. Even though I imported the dictionary module, I still need to qualify object instances, hence the Dictionary::Word qualification. I could have also dropped the Dictionary:: prefix code if I had written include Dictionary after the require clause.
Did you notice how I didn't specify a collection of Definitions or synonyms when I created the happy_wrd instance shown in Listing 4? I only passed in values for spelling and part_of_speech. I got away with that omission because Ruby supports default values for parameters. In Word's initialize method defined in Listing 3, I've specified definitions = [] and synonyms = [] as parameters, which basically says to Ruby if they are not included by the caller, then default them to empty collections.
Note also in Listing 3 how Definition's initialize method supports default parameters by setting example_sentences to an empty collection (word's default value of nil is Ruby's version of null in the Java language). Back in Listing 1, I had to create three constructors to get that same flexibility from the Java language!
Now watch as I create a different Word instance with my flexible initialize() method, in Listing 5:
Listing 5. Flexibility is the name of the game!
|
After I define two Definitions, I add them to a collection (which looks just like an array in the Java language). I then pass that collection to Word's initialize() method.
Ruby's collections handling is amazingly simple too -- see the add_definition and add_synonym methods in the Word class? The << syntax is overloaded to mean add. If you look back to the Definition class in Listing 2, you'll see that the corresponding code in the Java language is a much bigger mouthful: this.exampleSentences.add(exampleSentence).
|
Ruby's collection handling is extremely concise. In Listing 6, you can see how easy it is to combine collections (using the + operator) and access members (via [position]) -- and do so without the fear of things blowing up on you!
Listing 6. Shorthand collections
|
The code in Listing 6 only scratches the surface of Ruby's collection handling!
You may have noticed in both classes of Listing 3 that Ruby supports a shortcut notation for defining properties: attr_reader and attr_writer. Because I used this notation, I can set and get corresponding properties in my Word class, as shown in Listing 7:
Listing 7. attr_reader and attr_writer in action
|
Both attr_reader and attr_writer are not keywords but are actual methods in Ruby (found in the Module class) that take symbols as arguments. A symbol is any variable that is preceded by a colon (:), and what's even neater is that symbols themselves are objects!
Note that because I made synonyms read-only in Listing 3, Ruby denied my attempt in the last line of code in Listing 7. Also, I could have written the property declaration code using the attr_accessor method to indicate that a property was both readable and writeable.
Flexible iteration is one of the joys of coding in Ruby. Look at Listing 8, where I've singled out Word's initialize() method:
Listing 8. Closures are handy
|
Something different is going on in the fourth line of Listing 8, for sure. For starters, I used brackets when I called the each method on the definitions instance. The each method is essentially like an Iterator in the Java language, but it's a bit more concise. In Listing 8, the each method handles the details of iteration and allows the caller to focus on the desired effect. In this case, I passed in a block stating the following: for each value in the collection -- that is, idef, which is an instance of Definition -- set its word property to self (which is the same as this in the Java language).
Listing 9 shows essentially the same line of code in the Java language (taken from Word's constructor shown in Listing 1):
Listing 9. Ruby's each method is like Java's Iterator
|
Let me acknowledge right now that Java 5's generics and new for loop syntax is much, much nicer than what's shown in Listing 9. Ruby does support Java's familiar looping constructs such as for and while; however, in practice, they are rarely utilized since most everything in Ruby supports the notion of iteration. For example, in Listing 10, look how easy it is to iterate over the contents of a file:
Listing 10. Iteration made simple
|
Any class in Ruby that supports the each method (like File) lets you iterate this way. By the way, Ruby's puts method (seen in Listing 10) is the same as the Java language's System.out.println.
While I'm on the subject of looping, let's take a closer look at a conditional statement found in the Word class of Listing 3. In Listing 11, I've singled out the add_definition() method:
Listing 11. Nifty conditionals
|
Look closely at the second line of code. See how the if statement follows the expression? You certainly could write it normal-style, as shown in Listing 12, but isn't Listing 11 nicer?
Listing 12. More than one way to skin a conditional
|
In the Java language, if the body of a conditional is a single line, you can drop the brackets. In Ruby, if the body of a conditional is a single line, you can write expressions like the one shown in Listing 11. Also note that the same conditional could also be written as definition.word = self unless definition.word == self, which uses Ruby's unless feature. Nice, eh?
Because Ruby is a dynamically typed language, it doesn't require interfaces. Mind you, the power of interfaces is completely present in Ruby, but in a much more flexible manner. Affectionately known as "duck typing" (i.e., if it waddles like a duck and quacks like one, then it must be a duck!), polymorphism in Ruby is just a matter of matching method names. Let's compare polymorphism in Ruby and the Java language.
One way to capture the power of polymorphism in the Java language is to declare an interface type and have other types implement that interface. You can then refer to implementing objects as that interface type and call whatever methods exist on that interface. As an example, in Listing 13, I've defined a simple interface called Filter:
Listing 13. A simple Java interface
|
In Listing 14, I've defined an implementing class, called RegexPackageFilter, that applies a regular expression for filtering purposes:
Listing 14. RegexPackageFilter implements Filter
|
Now, imagine that there are multiple implementations of the Filter interface (such as the RegexPackageFilter, a ClassInclusionFilter type, and perhaps a SimplePackageFilter type). To maximize flexibility in an application, other objects can now refer to the interface type (Filter) and not the implementers, as shown in Listing 15:
Listing 15. Polymorphism is so beautiful ...
|
In Ruby, interfaces don't matter! So long as method names match, you have polymorphism. Watch.
In Listing 16, I've recreated the Java Filter types in Ruby. Note how each class isn't related (other than that they share the same method apply_filter). Yes, these two classes beg to be refactored into extending a base Filter class; however, I want to show polymorphism in action without the classes sharing a type.
Listing 16. Filter me, Ruby!
|
Note how in Listing 16, I can create a regular expression matcher in RegexFilter's apply_filter() method via the =~ syntax. (If you're a Groovy user, you should be smiling right about now, because Listing 16 shows how heavily Groovy has been influenced by Ruby!)
In Listing 17, I've used Ruby's Test::Unit (which is like Java's JUnit) to demonstrate duck typing in action. Making an automated test in Ruby, by the way, is as easy as extending Test::Unit and adding methods that start with test. Similar to JUnit, right?
Listing 17. Filtering Ducks!
|
Notice how in the test_filters() method, I've created a collection containing my two classes SimpleFilter and RegexFilter. These classes do not share a common base class, yet when I iterate over the collection, I can easily call the apply_filter() method.
Also note how easily Ruby supports regular expressions. To create one, you simply use the /regex/ syntax. Hence, my regular expression in Listing 17's RegexFilter is a capital G followed by one or more o's followed by gle.
While Ruby doesn't have interfaces, it does have mix-ins. You can think of mix-ins as multiple inheritance without the multiple-inheritance headaches. Mix-ins are modules (which cannot be instantiated) that contain methods that a class can chose to include. Those module methods then become instance methods of the including class.
In JUnit, for example, the Assertion class is a concrete class with a boatload of static assert methods, which the all-too-familiar TestCase class extends. Hence, any implementing class of TestCase can refer to an assert method within its own defined methods.
Ruby's unit testing framework is a bit different. Rather than defining an Assertion class, it defines an Assertions module. This module defines a cornucopia of assertion methods, but rather than being extended, Ruby's TestCase class includes the assertion as a mix-in. Therefore, all those assert methods are now instance methods on TestCase, as you can see back in Listing 17.
As you've seen, Ruby's syntax is quite different from that of the Java language, but it's amazingly easy to pick up. Moreover, some things are just plain easier to do in Ruby than they are in the Java language.
The nice thing about learning new languages, as programming language polyglots will tell you, is that nothing says they can't play together. Being able to code in multiple languages will make you more versatile in the face of both humdrum programming tasks and more complicated ones. It will also greatly increase your appreciation for the programming language you call home.
As I said at the beginning of this article, I'm mainly a Java developer, but I've found plenty of ways to include Ruby (and Groovy, and Jython ...) in my bag of tricks too. And I've been able to do it without using Rails! If you've written off Ruby on Rails because you don't actually need to build a shopping cart application in just four hours, take a closer look at Ruby on its own. I think you'll like what you see.












