分享

Ruby off the Rails

 chenge 2005-12-17

Andrew Glover (aglover@), CTO, Vanward Technologies

20 Dec 2005

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.

In praise of the polyglot

If you‘ve ever envied the ability of multilingual friends to bridge language gaps wherever they travel or gained new appreciation for your native language by learning a new one, then you can probably see the advantage of being a programming language polyglot. Developer polyglots travel the IT world more freely than do monolinguists (sure that they can apply their skills in any environment), and they also tend to better appreciate the programming language called home, because among other things they know the roots from which that home is sprung. Isn‘t it time you became a polyglot?

Feels so different

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 Word can have a collection of synonyms (which are instances of Words).
  • A Word also has a collection of Definitions.
  • A Definition has an aggregation association to a Word.

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


package com.vanward.dictionary;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Word {	

  private String spelling; 
  private String partOfSpeech;
  private Collection definitions;
  private Collection synonyms;

  public Word(String spelling, String partOfSpeech) {
    this.spelling = spelling;
    this.partOfSpeech = partOfSpeech;
    this.definitions = new ArrayList();
    this.synonyms = new ArrayList();
  } 

  public Word(String spelling, String partOfSpeech, 
    Collection definitions) {
      this(spelling, partOfSpeech);
      if(definitions != null){
        for(Iterator iter = definitions.iterator(); iter.hasNext();){
          this.validateRelationship((Definition)iter.next());
        }
        this.definitions = definitions;
      }		
  }

  public Word(String spelling, String partOfSpeech, 
    Collection definitions, Collection synonyms) {
      this(spelling, partOfSpeech, definitions);
      if(synonyms != null){
        this.synonyms = synonyms;
      }
  }

  private void validateRelationship(Definition def){
    if(def.getWord() == null || def.getWord() != this){
      def.setWord(this);
    }
  }

  public Collection getDefinitions() {
    return definitions;
  }	

  public void addDefinition(Definition definition) {
    this.validateRelationship(definition);		
    this.definitions.add(definition);
  }

  public String getPartOfSpeech() {
    return partOfSpeech;
  }

  public void setPartOfSpeech(String partOfSpeech) {
    this.partOfSpeech = partOfSpeech;
  }

  public String getSpelling() {
    return spelling;
  }

  public void setSpelling(String spelling) {
    this.spelling = spelling;
  }

  public Collection getSynonyms() {
    return synonyms;
  }

  public void addSynonym(Word synonym) {
    this.synonyms.add(synonym);
  }	
}

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


package com.vanward.dictionary;

import java.util.Collection;

public class Definition {

  private Word word;
  private String definition;
  private Collection exampleSentences;

  public Definition(String definition){
    this.definition = definition;
	this.exampleSentences = new ArrayList();
  }
  
  public Definition(String definition, Word word) {			
    this(definition);
    this.word = word;
  }

  public Definition(String definition, Word word, 
    Collection exampleSentences) {
      this(definition, word);
      if(exampleSentences != null){
        this.exampleSentences = exampleSentences;
      }
  }

  public String getDefinition() {
    return definition;
  }

  public void setDefinition(String definition) {
    this.definition = definition;
  }

  public Collection getExampleSentences() {
    return exampleSentences;
  }

  public void addExampleSentence(String exampleSentence) {
    this.exampleSentences.add(exampleSentence);
  }

  public Word getWord() {
    return word;
  }
  
  public void setWord(Word word) {
    this.word = word;
  }
}

Class definition ala Ruby

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


module Dictionary

  class Word

    attr_reader :spelling, :part_of_speech, :definitions, :synonyms
    attr_writer :spelling, :part_of_speech

    def initialize(spelling, part_of_speech, definitions = [], synonyms = [])
      @spelling = spelling
      @part_of_speech = part_of_speech      
      definitions.each{ |idef| idef.word = self}       
      @definitions = definitions      
      @synonyms = synonyms
    end 

    def add_definition(definition)
      definition.word = self if definition.word != self
      @definitions << definition    
    end

    def add_synonym(synonym)
      @synonyms << synonym
    end

  end


  class Definition

    attr_reader :definition, :word, :example_sentences
    attr_writer :definition, :word

    def initialize(definition, word = nil, example_sentences = [])       
      @definition = definition
      @word = word
      @example_sentences = example_sentences
    end

  end

end

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.



Back to top


Fast moving objects

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


require "dictionary"

happy_wrd = Dictionary::Word.new("ebullient", "adjective")
		
defin_one = Dictionary::Definition.new("Overflowing with enthusiasm")
defin_two = Dictionary::Definition.new("Boiling up or over")

happy_wrd.add_definition(defin_one)
happy_wrd.add_definition(defin_two)

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.

Default parameter values

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!


require "dictionary"

defin = Dictionary::Definition.new("Skill in or performance of tricks")
defin_two = Dictionary::Definition.new("sleight of hand")
        
defs = [defin, defin_two]
        
tricky_wrd = Dictionary::Word.new("prestidigitation", "noun", defs)

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.



Back to top


Collections done right

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).

Slick squares
Speaking of collections, isn‘t Ruby‘s [] syntax slick? If you‘re a Groovy user, it should look familiar to you as well.

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


require "dictionary"

idef_1 = Dictionary::Definition.new("Sad and lonely because deserted")
idef_2 = Dictionary::Definition.new("Bereft; forsaken")

defs = [idef_1, idef_2]

idef_3 = Dictionary::Definition.new("Wretched in appearance or condition")
idef_4 = Dictionary::Definition.new("Almost hopeless; desperate")

defs_2 =  [idef_3, idef_4]

n_def = defs + defs_2  #n_def is now [idef_1, idef_2, idef_3, idef_4]

n_def[1]    # produces idef_2
n_def[9]    # produces nil
n_def[1..2] # produces [idef_2, idef_3]

The code in Listing 6 only scratches the surface of Ruby‘s collection handling!



Back to top


RubyBeans?

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


require "dictionary"

wrd = Dictionary::Word.new("turpitude", "Noun")

wrd.part_of_speech  # "Noun"
wrd.spelling        # "turpitude"

wrd.spelling = "bibulous"

wrd.spelling        # "bibulous"

syns = [Dictionary::Word.new("absorptive", "Adjective"), 
  Dictionary::Word.new("imbibing", "Noun") ]

# Danger! 
wrd.synonyms = syns = syns #Exception: undefined method `synonyms=‘...

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.



Back to top


Watch Ruby iterate

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


def initialize(spelling, part_of_speech, definitions = [], synonyms = [])
  @spelling = spelling
  @part_of_speech = part_of_speech      
  definitions.each{ |idef| idef.word = self}       
  @definitions = definitions      
  @synonyms = synonyms
end 

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


for(Iterator iter = definitions.iterator(); iter.hasNext();){
  this.validateRelationship((Definition)iter.next());
}

Yes, yes, yes ...!

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

count = 0
File.open("./src/dictionary.rb").each { |loc| puts "#{count += 1}:" + loc }

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.



Back to top


Conditionally yours

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


def add_definition(definition)
  definition.word = self if definition.word != self
  @definitions << definition    
end

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


def add_definition(definition)
  if definition.word != self
    definition.word = self 
  end
  @definitions << definition    
end

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?



Back to top


Ducks love Ruby!

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.

Java-morphism

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


package com.vanward.filter;

public interface Filter {  
  boolean applyFilter(String value);
}

In Listing 14, I‘ve defined an implementing class, called RegexPackageFilter, that applies a regular expression for filtering purposes:


Listing 14. RegexPackageFilter implements Filter


package com.vanward.filter.impl;

import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;

import com.vanward.filter.Filter;

public class RegexPackageFilter implements Filter {
  private String filterExpression;
  private PatternCompiler compiler;
  private PatternMatcher matcher;

  public RegexPackageFilter() {   
    this.compiler = new Perl5Compiler();
    this.matcher = new Perl5Matcher();
  }

  public RegexPackageFilter(final String filterExpression){
    this();
    this.filterExpression = filterExpression;
  }

  public boolean applyFilter(final String value) {
    try{		
      Pattern pattrn = this.getPattern();
      return this.matcher.contains(value, pattrn);
     }catch(MalformedPatternException e){
       throw new RuntimeException("Regular Expression was uncompilable " +
	      e.getMessage());
     }
  }  
  
  private Pattern getPattern() throws MalformedPatternException{
    return compiler.compile(this.filterExpression);		
  }
}

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 ...


private boolean applyFilters(final String value, final Filter[] filters){
  boolean passed = false;
  for(int x = 0; (x < filters.length && !passed); x++){
    passed = filters[x].applyFilter(value);
  }
  return passed;
}

Ruby-morphism

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!


class RegexFilter

  attr_reader :fltr_exprs

  def initialize(fltr_exprs)
    @fltr_exprs = fltr_exprs      
  end 

  def apply_filter(value)
    value =~ @fltr_exprs
  end
end


class SimpleFilter

  attr_reader :fltr_exprs

  def initialize(fltr_exprs)
    @fltr_exprs = fltr_exprs      
  end 

  def apply_filter(value)
    value.include?(@fltr_exprs)
  end
end

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!)

Duck typing in action

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!


require "test/unit"
require "filters"

class FiltersTest < Test::Unit::TestCase
  
  def test_filters
    fltrs = [SimpleFilter.new("oo"), RegexFilter.new(/Go+gle/)]     
    
    fltrs.each{ | fltr | 
      assert(fltr.apply_filter("I love to Goooogle"))
    }

  end 

end

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.



Back to top


Mix-in it up

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.



Back to top


In conclusion

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.



Back to top


Resources

Learn

Get products and technologies

Discuss


Back to top


About the author

Andrew Glover is the CTO of Vanward Technologies, a JNetDirect company. Vanward helps companies address software quality early with effective developer testing strategies and frameworks, software metric analysis and correlations, and continuous integration techniques that enable development teams and management to monitor code quality continuously. He is the co-author of Java Testing Patterns (Wiley, September 2004).


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多