Beyond Java/Ruby in the Rough

From WikiContent

< Beyond Java
Revision as of 21:49, 11 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Beyond Java

I stood on the bank of the Watauga River, looking at the 16-foot, Class V monster known as State Line Falls. It had five boulders in the current with four chutes running through them. Three of the slots were all but impassable, especially at this water level. The fourth was violent and intense. And yet, the approach was pretty easy, and I thought I could hit the line. Run this monster, or walk it. I had to choose.

Over the years, I've experienced a few moments like that one. Sometimes, I'd put my kayak on my shoulder and walk around. Other times, I decided that the line was good and my skills were up to the challenge, so I made the run. But this time, I simply stood, indecisive, with the wind and the spray from the falls washing over me.

I'm looking at a similar situation now. I do think that Java's leadership run, at least for applications, might be drawing to an end. But the stakes are unbelievably high should I decide to move. How can I know if the timing is right? Can I pick the right language? What do I risk?

I don't want this book to be an exhaustive review of programming languages. I'd like to point out one language and two frameworks (one in Ruby and one in Smalltalk) that have something special to offer. In this chapter, I introduce one possible alternative language, Ruby. I want to show you that some languages can improve on Java, but that doesn't mean that Ruby will succeed, or that it's the best possible alternative. The best that I can do, for now, is to show you one possible alternative, so you can see if the case makes sense.

Contents

About Ruby

Ruby is a dynamic, fully object-oriented language that's usually grouped with scripting languages. The scripting term, for languages like Ruby, Smalltalk, and Python, is a little too limited, so I'll use the term applications language . If you've used nothing but compiled languages like Java and C, get ready to have some fun. Ruby will turn you loose. I suggest that you install it (just go to http://ruby-lang.org ), and type along. It comes with a primitive IDE, but the command line works well. Fire up a Ruby shell by typing irb. You'll get a shell prompt:

    irb(main):001:0>

Ruby Is Fully OO

From here, you can evaluate Ruby statements. You'll frequently use irb to answer those tiny questions that come up often in programming. In Ruby, everything is an object, and if you type one alone, Ruby will return that object. Type 4 and press Enter:

    irb(main):001:0> 4
    => 4

Unlike Java, numbers are objects , not primitives. For example, you can do this:

    irb(main):008:0> 4.4765.round
    => 4

Even nil is a class, standing for nothing:

    irb(main):009:0> nil.class
    => NilClass

You don't have to worry about primitives or wrappers at all. More importantly, you don't have to deal with those cases in an API. Ruby's reflection, persistence engines, and XML frameworks are all much simpler, because you don't have to deal with all the edge cases related to primitives and arrays of primitives.

Typing

Try to do an assignment without a declaration:

    irb(main):011:0> n=1
    => 1
    irb(main):012:0> n.class
    => Fixnum

So n has an object of type Fixnum. You didn't declare n at all. That's a strong hint that Ruby is dynamically typed. Now, assign something else to n:

    irb(main):013:0> n="fish"
    => "fish"
    irb(main):014:0> n.class
    => String

Now, n has a string. We changed the type of the variable i. More accurately, the type in Ruby is bound to the object, but not the thing that contains it. So Ruby is dynamically typed. Let's try to do something strange:

    irb(main):015:0> n+4
    TypeError: cannot convert Fixnum into String
            from (irb):15:in '+'
            from (irb):15

Ruby won't break its typing rules by coercing a string to a Fixnum. That means Ruby is strongly typed.[1] You can get its length by invoking the size method on n:

    irb(main):016:0> n.size
    => 4

How do you know what methods a string supports? Just ask one:

    irb(main):017:0> n.methods
    => ["send", "%", "rindex", "between?", "reject", "[  ]=", "split", "<<",
    "object_id", "strip", "size", "singleton_methods", "downcase", "gsub!",
    ...and so on...

So, String supports a whole bunch of methods. Try to count them with the size method. If you've always used statically typed languages, you will probably underestimate the benefits. You've read that dynamic typing lets you focus on the right part of the problem at the right time. It eases your refactoring burden, and reduces the amount of code that you have to write and maintain.

Conditionals

Ruby's conditionals will remind you more of C than Java. In Ruby, nil and false evaluate to false, and everything else (including true) means true. Read that sentence again. Unlike C, 0 is true. You should also notice that false and "false" are different. One is the Boolean constant for false, and one is a string. For example, puts "It's false." unless "false" returns nil, but puts "It's false." unless false will print It's false.

Ruby also has a few more conventions that you should know about. ? and ! are both valid in method names. By convention, methods ending in ? are tests. For example, nil? would test to see if a value is Nil. Methods ending in ! are potentially dangerous, because they have side effects. For example, a method called replace(in_string, sub_string, replacement) might return a string with the substring replaced, while replace!(in_string, sub_string, replacement) would modify the input string.

Like Java, Ruby has an if statement. Ruby also supports an unless statement that works the same way. You can use if or unless in block form, as you do in Java. You can also tack them onto the end of a line, to conditionally execute a single line of code. So, you can do something like this:

    irb(main):099:0> def silence?(b)
    irb(main):100:1>   puts   "SCREAM!" unless b
    irb(main):101:1> end
    => nil
    irb(main):106:0> silence? "False"
    => nil
    irb(main):107:0> silence? "false"
    => nil
    irb(main):108:0> silence? 0
    => nil
    irb(main):109:0> silence? "quit kicking the cat"
    => nil
    irb(main):110:0> silence? false
    SCREAM!
    => nil
    irb(main):111:0> silence? nil
    SCREAM!
    => nil

Take a look at the silence? method. Ruby returns the value of the last statement, unless a method explicitly returns something. In this case, the statement puts "SCREAM!" unless b always returns nil. More importantly, the method prints SCREAM unless you pass it a true value.

Looping

Ruby has two conditional loops. You'll notice that many of Ruby's libraries help you by returning nil when they're done. If you're reading from standard input, you might do this:

    irb(main):010:0> puts line while line=gets
    one
    one
    two
    two
    ^Z
    => nil

The loop continued until I entered the end-of-file character. Of course, you can also direct the input stream to a file. Plus you can use while at the beginning of a line, as long as you terminate it with an end:

    irb(main):013:0> while line=gets
    irb(main):014:1>   puts line
    irb(main):015:1> end
            

You've already seen Until, the other looping construct. It works in exactly the same way, but it will fire the loop while the expression is false. You'll also see a for loop later, but that's just syntactic sugar.

Ranges

Java programmers typically will specify a range using an arithmetic expression, like this:

    class Range {
      public static void main (String args[  ]) {
        int i = 4;
        if (2 < i && i < 8) System.out.println("true");
      }
    }

You can do something similar in Ruby, but you've got another alternative. Ruby supports first-class range support. x..y represents values from x to y, inclusive. For example, 1..3 represents 1, 2, 3. You can include the 3 with a third period. As you can imagine, ranges in Ruby are objects:

    irb(main):004:0> range=1..3
    => 1..3
    irb(main):005:0> range.class
    => Range

You can also check to see if something is in a range, using the = = = operator:

    irb(main):010:0> ('a'..'z') =  =  = 'h'
    => true
    irb(main):011:0> ('a'..'z') =  =  = 'H'
    => false
    irb(main):012:0> (1..10) =  =  = 5
    => true

You get more convenient syntactic sugar. Now, a for loop turns into this:

    irb(main):021:0> for c in 'g'..'k'
    irb(main):022:1>   puts c
    irb(main):023:1> end
    g
    h
    i
    j
    k

for/in loops also work with Arrays and Hashes. Ranges introduce = = =, another type of comparison. Next, you'll see a third type of comparison, called match, which you'll use with regular expressions .

Regular Expressions

Java has an API that supports regular expressions. Ruby builds regular expressions into the syntax. Some like regular expressions and others do not. To me, they're a critical part of dealing with strings. Just like any other type of programming, you can take them too far. If you've got 16 consecutive backslashes, it's probably time to refactor. Still, they can be much more useful than similar code, handwritten to recognize certain patterns.

In Ruby, you'll define a regular expression between slashes. You'll match regular expressions like this:

    irb(main):027:0> regex = /better/
    => /better/
    irb(main):028:0> regex.class
    => Regexp
    irb(main):029:0> "Mine is bigger" =~ regex
    => nil
    irb(main):030:0> "Mine is better" =~ regex
    => 8

Ruby returns the index of the character at the match. Ruby regular expressions are much more powerful than I can show you here. I'll just say that Java developers spend at least half of their time dealing with strings. When you think about it, servlets, XML strings, configuration files, deployment descriptors, and application data can all be strings. To parse them effectively, you need first-class pattern matching, such as regular expressions and ranges. Java 1.5 closes the gap some, but not completely.

Containers

Ruby containers are like Java's collections. You just saw an array. Like Java, arrays are objects: [1,2,3].class returns Array. Unlike Java, everything in an array is also an object. Ruby also has a Hash. Like Java's HashMaps, a Ruby Hash is an object. Unlike Java's HashMap, a Ruby Hash also has some syntactic sugar. You use braces instead of brackets, and you use key=>value to define one key-value pair, like this:

    irb(main):011:0> numbers={0=>"zero", 1=>"one", 2=>"two", 3=>"three"}
    => {0=>"zero", 1=>"one", 2=>"two", 3=>"three"}
    irb(main):012:0> 4.times {|i| puts numbers[i]}
    zero
    one
    two
    three

Like Java collections, Ruby containers hold objects, and they need not be homogeneous. In version 1.5, Java's generics let you build type-safe collections. You could modify Ruby's Array or Hash to make them type safe. (Remember, you can modify any of Ruby's classes directly. It's a dynamic language.) While Ruby doesn't have dozens of types of containers like Java does, you will notice some benefits immediately:

  • Since there's no distinction between primitives and other objects, you can put literally anything into any given container, and you can nest them easily.
  • Since everything inherits from object, everything has a hash code.
  • The language gives you the same syntactic sugar for hashes as for arrays.
  • Code blocks make iteration tighter and easier.

If you're a big Java collections user who's used a dynamic language before, you probably noticed that Java collections often feel wrong. You have to circumvent static type checking, because you're adding something to a collection as an object, and you're forced to cast it to something else when you retrieve it. Iteration is painful and awkward. A collection doesn't feel like a standard array, which can possibly contain primitives.

Ruby containers will feel altogether different. You won't have to deal with the maddening type casts or generic syntax. Code blocks simplify iteration. You don't see too many types of collections, but don't let that fool you. Using the rich methods, you can use Array as a list, queue, stack, or any other type of ordered collection. For instance, let's use Array as a stack:

    irb(main):001:0> stack=[1,2,3]
    => [1, 2, 3]
    irb(main):002:0> stack.push "cat"
    => [1, 2, 3, "cat"]
    irb(main):003:0> stack.pop
    => "cat"
    irb(main):004:0> stack
    => [1, 2, 3]

Similarly, you can use Hash whenever you need a set, dictionary, or any type of unordered collection. You'll find yourself doing more with collections, and less customized iteration.

Files

Iterating through a file works much like iterating through a collection. You'll create a new file and pass it a code block. For example, here's a simple GREP:

    File.open(ARGV[0]) do |file|
      rx = Regexp.new(ARGV[1])
      while line=file.gets
        puts line if line =~ rx
      end
    end

To use it, type it into a file called grep.rb. Then, you can call it (outside of irb) like this:

               ruby grep.rb filename regex
            

Notice what you don't see. You don't have to close the file or manage exceptions. This implementation makes sure the file will be closed if an exception occurs. You're effectively using a library that specifies everything on the outside of a control loop that iterates through a file. Ruby does the repetitive dirty work for you, and you customize the inside of the control loop with a code block.

Why Should You Care?

By now, you should be getting a feel for the power and simplicity of Ruby. You can probably see how the lines of code go down and the abstraction goes up. You might think it doesn't make any difference. You could lean ever harder on your development environments and on code generation tools like XDoclet , and shield yourself from some of the problem, but let me tell you: lines of code matter!

  • You still have to understand anything that your tools generate. I work with dozens of people every year that don't understand the SQL that Hibernate cranks out, and others who have to maintain generated code, after they tailor it for their needs.
  • The more code you have, the more bugs it can hide. Unit testing can take you only so far. You'll still need to inspect code to enhance it or maintain it.
  • Writing code is not the only cost. You also need to consider the cost of training, maintaining, and extending your code.
  • Each code generation technique that you use limits your flexibility. Most Java developers now depend on tools to do more and more. Each tool that you adopt carries a cost. I'm an IDEA man, but some of my customers use Eclipse. I'm nowhere nearly as effective on it, so my customer loses something when I am forced to use it. XDoclet increases the feedback cycle.
  • Java developers rely increasingly on XML for configuration. Remember, configuration is still code. Developers from other languages often find Java's over-reliance on XML configuration annoying. We use so much configuration outside of the language because configuration in Java is painful and tedious. We do configuration in XML rather than properties because...well, because overuse of XML in Java is a fad. Meanwhile, configuration in Ruby is usually clean and comfortable.

You may be willing to pay the costs related to lines of code, but you should also consider higher abstractions. With Java, you must use unsightly iterators. With Ruby, you wind up building the iteration strategies into your containers and reusing that logic.

Said another way, Java customization usually happens with an outside-in strategy. You build big chunks of reusable code that fill out the inside of your applications. But that's only one kind of customization. For many jobs, you'd like to keep a generic implementation of a job, and customize a few lines of code on the inside of a method. Iterating through a JDBC loop, processing a file, and iterating through a collection are only a few examples of this strategy. Some Java developers call this strategy inversion of control .

Ruby lets you program with both styles, as shown in Figure 6-1. Code written with that strategy is a joy to maintain, and it hides repetition from you. To be fair, some Java frameworks, like Spring, do some of this for you as well, but it's not as easy in Java, and this style of programming is not nearly as common, since you have to use the heavyweight anonymous inner class to do so. In dynamic languages like Ruby and Smalltalk, this programming strategy gives you tremendous intellectual freedom, both in the frameworks that you use and in the frameworks that you build.

Applying Some Structure

Both Ruby and Java are object-oriented languages. Both support object models with single inheritance. Still, you're going to see some differences between Ruby and Java:

Figure 6-1. Java programmers refactor the inside of a loop; code blocks let Ruby developers refactor the outside of a loop, too

Java programmers refactor the inside of a loop; code blocks let Ruby developers refactor the outside of a loop, too

  • In Java, the smallest application is a class. In Ruby, everything is an object, so you can evaluate primitives, expressions, code blocks, and scripts. They all are objects, and all are valid Ruby.
  • In Java, class definitions are static. In Ruby, you can modify your classes on the fly. When you see a class definition, if the class already exists, the new definition will modify the class that's already there.
  • Ruby supports mixins and Java does not. Think of a mixin as an interface, plus an implementation, that you can attach to a class.
  • In Ruby, everything returns some value, and that value is typed dynamically, so you won't see a return in the method definition.
  • In Ruby, method parameters and instance variables are not typed; but the instances themselves are typed.

For the most part, you can still use your OO design skills in Ruby as you did in Java. You'll also see some common design patterns, like model-view-controller.

Classes

Ruby is object-oriented. I've shown you how to use Ruby objects , but not yet how to create one. Let's make a class called Calculator. Create a file called calculator.rb that looks like this:

    class Calculator

      def initialize
        @total=0
      end

      def add(x)
        @total += x
      end

      def subtract(x)
        @total -= x
      end

    end

You've declared three methods. Ruby will call initialize when it creates a new object, such as this calculator. Notice that initialize defines an instance variable called @total. In Ruby, instance variables start with @, class variables start with @@, and global variable start with $. Now, in irb, you can load the file and use the calculator.

    irb(main):005:0> require 'Calculator'
    => true
    irb(main):006:0> c=Calculator.new
    => #<Calculator:0x28b4a98 @total=0>
    irb(main):007:0> c.add 100

    => 100
    irb(main):008:0> c.subtract 40
    => 60

And it works, just like you'd expect. Ruby developers take advantage of open classes . I'm going to change the definition of Calculator, but keep in mind that we still have c, an instance of Calculator. I actually open up the definition of the class again like this:

    irb(main):009:0> class Calculator
    irb(main):010:1>   def reset
    irb(main):011:2>     @total = 0
    irb(main):012:2>   end
    irb(main):013:1> end
            

I just added a method called reset. I also could have changed an existing method.

    irb(main):014:0> c.reset
    => 0

That's amazing. I changed the class definition of an existing class. That's a useful capability for debugging, iterative programming, and metaprogramming. Ruby also lets you subclass. To subclass, you use the < operator:

    irb(main):015:0> class IrsCalculator < Calculator
    irb(main):016:1>   def add(x)
    irb(main):017:2>     x = x / 2 if x>0
    irb(main):018:2>     super
    irb(main):019:2>   end
    irb(main):020:1> end
    => nil

You can use it, and IrsCalculator will take a little off the top for you:

    irb(main):027:0> c=IrsCalculator.new
    => #<IrsCalculator:0x28b6b80 @total=0>
    irb(main):028:0> c.add 100
    => 50

These concepts should look familiar to you. Classes package instance data and methods together. An instance of a class is an object. All classes have single parents, and eventually inherit from Object, with the exception of Object:

    irb(main):031:0> Class.superclass
    => Module
    irb(main):032:0> Module.superclass
    => Object
    irb(main):033:0> Object.superclass
    => nil

Using Mixins

To implement a mixin, Ruby uses a concept called a module. A module lets you group together methods and classes. You can't instantiate a module, and a module doesn't stand alone. A module isn't a class, but it does have its own namespace. Modules form the foundation of classes and mixins .

Mixins are not new. Smalltalk supported them back in 1971. Recall that a mixin is an interface with an implementation. That means you can group together a set of methods that many classes may need to use.

Look at this contrived little example. To build the friendliest possible application, you may want to build a mixin to greet any object by name. You'd code it like this:

    irb(main):021:0> module Greetable
    irb(main):022:1>   def greet
    irb(main):023:2>     puts "Hello, " + self.name
    irb(main):024:2>   end
    irb(main):025:1> end
    => nil

Then, you can include this code in a class called Person:

    irb(main):011:0> class Person
    irb(main):012:1>   include Greetable
    irb(main):013:1>   def initialize(name, age)
    irb(main):014:2>     @name=name
    irb(main):015:2>     @age=age
    irb(main):016:2>   end
    irb(main):017:1>   attr_reader :name
    irb(main):018:1> end
    => nil

You can use this code in Person:

    irb(main):039:0> person=Person.new("Bruce",40)
    => #<Person:0x2a970a0 @age=40, @name="Bruce">
    irb(main):040:0> person.greet
    Hello, Bruce
    => nil

While mixins seem interesting, this code probably smells wrong to you. Unless you could better integrate the Person methods in the mixin, it's just a recipe to make bad design decisions: you can include stuff that doesn't really have anything to do with Person into Person. But it's more powerful than that. You can separate an aspect, or a capability, into a mixin. What makes mixins so powerful is this: you can also access Person's class methods in your module. In fact, we used Person.name, in the module, before we had even defined Person. If it sounds confusing, just look at the following module. inspect is a class method that puts the contents of an object in string form:

    irb(main):147:0> module Reversible
    irb(main):148:1>   def inspect
    irb(main):149:2>     super.reverse
    irb(main):150:2>   end
    irb(main):151:1> end
    => nil

Note that you haven't defined a class yet, but you're still using the inspect class method. That may seem strange until you include the module in the Calculator class that we made before:

    irb(main):152:0> class Person
    irb(main):153:1>   include Reversible
    irb(main):154:1> end
    => Person

Now you've included the module, and it has a class. It's now a mixin. You can call any new instance methods that it defines. It will assume the class that you add it to. Look at what happens when you instantiate it:

    irb(main):155:0> p=Person.new("Bruce", 40)
    => >"ecurB"=eman@ ,04=ega@ 0711c82x0:nosreP<#

irb actually calls inspect when you instantiate an object. Did you see the garbled line at the bottom? It's actually "Person:0x28c1170 @age=40, @name=\"Bruce\" in reverse. That's impressive. Now, you can add a mixin that can inspect the class, and integrate the most intimate details of the class into the mixin. And you can do all of this integration before a class even exists. I can use mixins for things like security or persistence. Java developers often resort to AOP to get the capability of mixins.

Interceptors

I've said that Java framework developers these days place an ever-increasing value on techniques that change the behavior of an existing class, without changing its code. One such technique is method interception . JBoss and Spring use method interception to attach arbitrary services to a POJO. With Ruby, interception is easy. You simply take a method, rename it, and put another method in its place (see Figure 6-2).

For example, let's say that my friend, Dave Thomas, asks me to watch his laptop for a few minutes before his big Ruby presentation. I could go to his Ruby shell and enter this little gem based on an example from his book,

Figure 6-2. In Ruby, to do method interception, you simply rename and replace a method, with the new implementation calling the old

In Ruby, to do method interception, you simply rename and replace a method, with the new implementation calling the old

Programming Ruby (Pragmatic Bookshelf). This version intercepts new, as you can see in Figure 6-2. I simply rename the original and call it from the replacement new. The interceptor will print out a message whenever Ruby creates a new object. Here's how easy it is:

    class Class
      alias_method :original_new, :new
      def new(*args)
        result = original_new(*args)
        print "Unattended laptop error. "
        return result
      end
    end

And when Dave gets back to teach his class, he'll get a nice surprise when he does anything that creates an object (which is pretty much anything in Ruby):

    irb(main):009:0> i=[1,2,3]
    Unattended laptop error. Unattended laptop error. Unattended laptop error.
    Unattended laptop error. Unattended laptop error. Unattended laptop error.
    Unattended laptop error. Unattended laptop error. Unattended laptop error.
    Unattended laptop error. Irb(main):010:0>

That's an interceptor in eight lines of code. You get extra credit if you know which 10 objects get created. You don't have any Java proxies, code generation, or aspect-oriented programming. Of course, you'll not want to try this with the real Dave. That would be like throwing a firecracker under Albert Einstein's car. Like Albert and the atom, you don't want to unleash this kind of power without knowing where all the energy is going to go.

AOP

Java developers depend on AOP with increasing frequency. AOP lets you add services to your POJO without modifying any code. AOP helps you control the flow of your application, such as adding custom methods at interesting points—for instance, before or after a method executes. In particular, you'll often see AOP for:

Debugging or logging
AOP lets you add debugging or logging code everywhere that you need it, with very little syntax.
Declarative services
EJB used a container to provide services. You would specify the service with configuration rather than code. Lightweight containers do the same thing with AOP. You'll often see interceptors manage transactions, security, and remoting.
Mixins
Java doesn't provide mixins, but you can simulate them with AOP.

Of course, AOP is a much broader tool, and if it is successful, the typical use cases obviously will grow in scope and power. Right now, though, Java developers most frequently use the power of AOP through frameworks like Spring.

You can look at interceptors as a more primitive tool to accomplish the same sorts of things. The JBoss framework and containers like HiveMind use interceptors to provide a wide range of services, like transactions. For Ruby developers, AOP is not quite as urgent, because you've already got robust tools to deal with these kinds of concerns:

  • You can use interceptors. These let you add services to any object at any time. It's as easy as renaming one method and introducing another.
  • You can use mixins, even attaching them at runtime. You could easily make all of the methods on a domain model secure, for example.
  • You can use hooks . Ruby provides hooks so that you can inject custom code at certain well-defined locations. The next version of Ruby will support hooks called _ _before, _ _after, and _ _wrap.

In short, Ruby can already solve many AOP-like problems without AOP, and will add AOP-like features in the very near future. Some Ruby programmers are concerned that AOP code may be more difficult to maintain. The core value of AOP that's not yet supported in Ruby is the ability to specify a point cut quickly and efficiently, which lets you use regular expressions to define interceptors wherever you need them. Ruby already has the core features that should make point cuts easy to implement:

  • You can quickly query for the methods that an object supports.
  • You can match regular expressions.
  • You can invoke a method with a string.
  • You'll soon (Ruby 2.0) be able to hook Ruby methods with before, after, and wrap.
  • Ruby is very friendly to configure. You can specify the point cuts in Ruby, without requiring XML or a whole new syntax, like AspectJ.

Given these capabilities, AOP becomes a very lightweight feature. Right now, Ruby developers prefer to implement AOP-like features, piecemeal, in a style that best fits the architecture.

Dependency Injection

The difference dependency injection in Java and Ruby is a little tougher to understand for Java developers. In Java, dependency injection is rapidly changing the way that we build applications. It's a relatively simple concept:

    class Speaker {
      void speak(String words) {
        System.out.println(words);
      }
    }
    class Consumer {
      Speaker mySpeaker;
      void saySomething() {
        mySpeaker.speak("something");
      }
    }

Notice Consumer. It doesn't instantiate Speaker. That job goes to a third party. We'll call it Container:

    class Container {
      public static void main(String[  ] args) {
        Speaker speaker=new Speaker();
        Consumer consumer=new Consumer();
        consumer.mySpeaker = speaker;
        consumer.saySomething();
      }
    }

You can make some simple improvements. You can encapsulate mySpeaker with a getter and setter. You can then extract an interface called Speaker, and provide implementations for FrenchSpeaker, EnglishSpeaker, and SpanishSpeaker. You can also make a configuration file, in Java or XML, describing all the objects that you want to treat this way.

You'd then have most of what you'd need for a basic dependency injection container: configuration, third-party life cycle control, and the ability to loosen the coupling between Speaker and Consumer. With a dependency injection container, you could change implementations of Speaker without changing any code in any of the consumers. You could also inject a test implementation of Speaker without impacting the base code, a critical technique in Java for test-first development. You'd also have a consistent strategy for configuration.

A few things come up right off the bat when you look at dependency injection in Ruby. First, Java's not very good at configuration, but Ruby lets you represent structured data quite well, often with far less invasive syntax than XML. You also can solve many of the coupling problems by changing the definition of a class on the fly. It's easier, for example, to inject those mock objects into hard-to-reach places.

Some developers in Ruby seem to think dependency injection is important and that the idea will have a place in the Ruby mainstream, given time. It should come as no surprise to you that Ruby has an outstanding dependency injection framework called Needles.

Others tend to think that dependency injection should happen in spots, instead of with a single, general-purpose framework. Since it's easy to change a class definition on the fly, you can easily inject the behavior that you need without adding another layer of complexity, across the application. Most of the Ruby programming community seems to be converging on the idea that Ruby's overall dynamic design makes dependency injection unnecessary for all but the most complex applications (see the sidebar, "Jim Weirich, Jamis Buck, and David Heinemeier Hansson, Three Ruby Experts: Does Ruby Need Dependency Injection?").

Breaking It Down

That's a 30-minute tour through Ruby. I'm not saying that Ruby is the next great language, but rather, that Ruby makes some of the hard things in Java easy. More and more of the top independent consultants are looking for ways to make more money working in Ruby, or other languages that are more dynamic. The Java community is spending an amazing amount of money and brainpower on making Java more dynamic. Dependency injection and aspect-oriented programming are groundbreaking ideas for Java, and they are only now getting serious commercial traction. For Java developers, these ideas represent better transparency and simpler application development.

Collapsing Under the Weight of Abstraction?

My playtime in Ruby makes another, more powerful idea, clearer. As we stretch Java in increasingly unnatural directions, there's a cost. AOP and dependency injection are near-trivial exercises in Ruby, but they force Java developers to learn new programming models, deal with XML, and introduce increasingly complex syntax. With each new metaprogramming concept that we bake into Java, it's looking more and more like all of that complexity is trying to drive us somewhere. The net effect is to push Java further and further into the enterprise niche, and make it less and less accessible to the average Joe. Contrast that situation with Ruby, where dependency injection and AOP don't consume your focus; you're free to apply those ideas in spots right where you need them.

I do think that Ruby, with Rails, is a near-ideal solution for that sweet spot that we've pushed: a web-based frontend for a relational database. I've already said that I'm using Ruby in a commercial application. My customer demanded productivity and a price point that I couldn't achieve in any other way. I also still recommend Java to many of my clients. They need complex frameworks that Ruby does not yet support, or they depend on a core set of developers that have already been trained, or they have so much legacy code in Java that change would be impractical.

In the next chapter, I'll make these arguments real. I'll show you how to build a web-based application, from scratch, to access a relational database with a web application. Then, I'll show you what another killer app might be, for another language.

Notes

  1. Actually, strongly typed is an oversimplification. Since you can change Ruby types indiscriminately, some might consider Ruby to have weaker typing. I'll stick with the oversimplified definition for this chapter.
Personal tools