I've been taking a look at ruby for a few month, I am finally getting
to a stage where (I think/hope) I can actually say something
intelligent about it. Last time I had an "aha moment" about a language
was the fist time I saw Java. C++ was oh-so-powerful, but Java was
(is) much more elegant and nice. Now Ruby changes the rules of the
game again.
When I first heard about ruby I thought it was just a
fad, something that the cool kids are using but its just another
language. I getting more and more convinced that it isn't so. I am
trying not to get too "silvery-bulletey" here but working in ruby
seems to actually increase productivity.
Let's
take Dependency Injection(DI) as an example. DI is one of the most
important and powerful tool I've learned in regard to Object Oriented
development. Instead of classes depending directly on other classes
classes depends on interfaces. And external classes (assemblers)
provide them with their dependencies. This allows for loose coupling,
increase testability and a lot of other such goodies (you can read a
concise explanation in a paper I wrote on
OO principles or get a more thorough explanation in a paper Martin Fowler wrote on
"Inversion of Control Containers and the Dependency Injection pattern".
The .NET and Java worlds are filled with a lot of frameworks to help
solve this elegantly. Spring (and Spring.Net) is probably the most
known one.
How do you do DI in Ruby? in two words - you don't
If I am to join the
"Define DI in one sentence" challenge by Jim Weirich I would say that
"DI
is a powerful and good workaround to the collaboration coupling
problem between objects which is best addressed at the language level"
Why doesn't ruby need DI?
Well,
I would say that it all starts at the basics. I remember when I learned
OO, I was told objects communicate using messages. I never really
understood why they call "method invocation" messages - it doesn't make
any sense. The point is that in ruby you really don't "call a method"
you "send a message"*. When you make a call like
someVariable.SomeMessage - the ruby interpreter doesn't really care
about the type of someVairable just that the object it holds (and
everything is an object) has some entry which can handle SomeMessage.
Let's start with a simple example
consider the following code:
class Foo
def bar
puts "Foo-bar"
end
end
class Foosa < Foo
end
class Baruser
def baruse(b)
b.bar #dependency
end
end
bu= Baruser.new
bu.baruse(Foo.new)
bu.baruse(Foosa.new) # sub-class
Well, nothing particularly exciting here. if you run this you get
foo-bar printed twice. That's very much like the dependency injection
you see in .NET or Java
It gets a little more interesting when we consider that the following classes would all work as well
class Foz
def bar
puts "Foz-bar"
end
end
class NoBar
def method_missing(methodname, *args)
puts "NoBar" if "bar" == methodname.to_s
end
end
class MakeBar
define_method(:bar) {puts "madebar"}
end
bu= Baruser.new
bu.baruse(Foz.new) # another type altogether
bu.baruse(NoBar.new) # a class that doesn't have bar method
bu.baruse(MakeBar.new) # a class where the bar method is created programatically
We can see from the examples that what ruby does is searching for a
handler for the bar message. The handler can be a method (symbol)
called bar or a generic handle like Missing_Method - ruby doesn't care
as long as the message get handled
I think that's pretty nice, we have a lot of flexibility on the
dependency side but the depending class still essentially gets the
dependency by injection (the call to baruse)
Well, ruby can help us flex the dependent side as well. The answer is
in the last example which uses (an overly simple an uninteresting
example of) meta-programming Consider the following code example using
Mocha which is a Mock object library for ruby
Lets say we modify our Baruser class to the following.
class Baruser
def initialize
buildfoo()
end
def baruse
foo = buildfoo()
foo.bar #dependency
end
private
def buildfoo
Foo.new #dependency
end
end
Note that buildfoo is private - if we wanted to test this in.NET we
need to have Foo around. i.e. we can no longer test Baruser by itself
if we use Mocha in ruby we can do the following:
foo = mock('foo')
foo.expects(:bar).at_least_once.
bu = Baruser.new
bu.stubs(:buildfoo).returns(foo) # basically what happens here is that the instance we have is changed
bu.baruse
I can't believe they invented it
- You can get ruby for just the price of download and if you call
within the next 15 minutes we'll throw in a copy of gems free of charge
:)
In any event what we see here that using meta-programming and other
ruby constructs we can forgo using DI altogether - no wonder
Neil Ford
defined DI as :
"Dependency Injection enables a vitally important but nevertheless
weak, limited, syntactically confounding, and dauntingly complex form
of one of the kinds of meta-programming that should exist in the
language."
Not
to mention that the resulting code is much more elegant. which is
actually what I like best about ruby, the code is much cleaner (but
that's for another post)
Some closing thoughts
-
.NET 3.5 bring some of the ruby goodness to C# - but as the previous
post demonstrated it is just a move in the right direction but not the
whole thing
-
While I am on the subject of the previous post the whole interface vs.
class thing is, of course, a moot point in Ruby since there are no
interfaces. Interfaces, like DI, are another thing that is very
important in C# and Java and not needed in Ruby
-
I've read some complaints on Ruby's performance. performance is
important but there are two things to remember. First, the fact that a
solution is not the fastest doesn't mean that it isn't fast enough.
Second, I can still vividly recall the performance benchmarks for our
Java code before we got the first hot spot compiler installed. The
point is, that if it is important to enough people it will get better.
* I know, I know, Smalltalk had it since the beginning of time.
However in Smalltalk everything is an object, in ruby you can also
write plain scripts and (more importantly for me) - I never really took
more than a cursory look at smalltalk so I never saw that.