In Charlie Nutter's blog post last week he talked about how easy it was to access Java classes from within JRuby. In this post, I am going to expand on that theme and demonstrate a few things you can do to give Java classes and APIs a Ruby face lift.
There are thousands of useful Java libraries out there that are usable from JRuby, but just importing and consuming them directly is not always very appealing. This is largely in part because these Java APIs do not use any Ruby features. In other words, the API feels like a Java API (duh!). So as you've probably already guessed, the rest of this entry will be about pimping out Java APIs with the workhorse Ruby features we all know and love.
What You Get by Doing Nothing
Even if you don't try and make a better API, JRuby does several things to Java classes to help narrow the gap:
All methods can be snake-cased:
setSize 200, 200 can be:
set_size 200, 200
Getters can omit the get:
my_height = frame.getHeight can be:
my_height = frame.height
Setters can be replaced with assignment:
event.source.setText("Yeeeeehaw") can be:
event.source.text = "Yeeeeehaw"
Last arguments which are interfaces can just be a block instead:
This is much more common in Java APIs than you would think:
Common Java types have been infused with some common sense Ruby behavior:
For example, all Lists and Maps have an each() method and mixes in Enumerable.
We try our best to make Java classes not look alien in Ruby, but they're still basically a Java API with a minor face lift. You need to put a little work into Ruby API design to take Java classes to the next level.
Before I explain decoration, I need to give one detail about how Java classes get imported into JRuby. When a java_import is done:
This will construct a Ruby Class in the current namespace called
ArrayList or a Ruby Module if we were importing a Java interface. The Ruby class/module defined will have one method per distinct name in the Java class/interface which will dispatch to the appropriate Java method via lots of magic for Java signature overrides and static-type resolution. From this we can say:
'Anything imported from Java is actually fronted by a Ruby class/module that can be manipulated like any other Ruby class/module'
With this knowledge, decoration is just manipulating these Ruby proxies like any other Ruby class/module. If you want to add a method which has optional parameters, then just open the class and add the method you want. You can do anything you can think of including things like: aliasing, removing methods, adding instance variables, or even add a method_missing.
So let's look at a concrete example. In JRuby source itself, we decided that anyone who uses any class derived from the interface java.util.Map should be Enumerable. We do it with decoration:
So because of this we can do things like:
So this makes
ArrayList feel just like Ruby's Array sans construction. Decorating is a nice way of bridging the gap with a little bit of Ruby code. It also helps encapsulate the use of Java API code into one place.
BEEP BEEP BEEP
The Java people reading this may be excited thinking "oh wow! I can add methods to Java classes." Realize that anything you add will only be seen on the Ruby side of the fence. For Java classes, type signatures are frozen when the class is compiled. No amount of JRuby magic can change this aspect of Java.
Additionally, if you want to change behavior of an existing method and have Java see that change, then you need to extend the Java class (e.g.
class MyArray < ArrayList). In a future blog entry, we'll talk about implementing Java interfaces and extending Java classes within JRuby, so keep an eye out.
It should be old hat for most Rubyists to make nice APIs using blocks. I just wanted to point out just how helpful they are when trying to change a blockless API to one which uses blocks.
In Java, one of the most hated aspects of coding is large number of try/catch/finally statements. Ruby has the equivalent with begin/rescue/ensure but have you ever noticed how much less we see the Ruby-equivalent constructs in Ruby? try/catch literally riddle the landscape of Java. Encapsulating these exceptional paths into to a method which accepts a block is a clear winner and should be used when you encounter this in a Java API. This is especially true for APIs which need to demarcate something transactional in nature.
In fact, there is lots of repetitive code in Java which can be DRY'd up with some simple block idioms.
Another problem in Java that festers because of lack of blocks is poor syntax for anonymous classes. It is so verbose that most people will opt to write one-off named classes. This ends up causing lots of extra files or extra lines for inner classes. Blocks make it really easy to kill off one-off classes. Here's a code snippet I used in the JrME library:
In one small demo game which I ported from Java, I ended up removing three separate application-level classes because I made this one tiny library-level one. It's surprising how nice some Java libraries seem after adding a few strategic block-accepting methods.
Delegation as a concept is not specific to Ruby, but is worth bringing up. Why decorate a Java class and get all of that Java class's methods exposed when you can hide them behind another Ruby class that only exposes what you want? This is especially powerful when you want to write a common API that works across multiple Ruby implementations (e.g. JRuby, MRI, and Rubinius), but has different native backends.
Domain Specific Language (DSL)
Ruby provides the capability to make code soooo much more readable by doing some syntax gymnastics and making a DSL. Java has absolutely no chance in this area. I made a simple DSL in JrME for physics object descriptions which I thought turned out well. In fact, let's look at a before and after...
This abbreviated Java snippet creates a cube that is made of ice with a texture map at a particular location:
Caveat: a good Java programmer would organize this code and make abstractions that make sense for Java. They would not vomit it into one stream like this. However, I just wanted to show how much work the DSL does, so I think it is okay to show it this way.
Here's what this code looks like using the Ruby DSL:
DSLs are not for everyone and certainly not for every occasion, but when they fit, nothing can beat them.
Scratching the Surface
I've just provided but a few examples of the many ways to Rubify Java APIs. Beyond the explanation of
java_import, this was all just basic Ruby programming. If you can write a good Ruby API, then you'll have no problems wrapping a Java API. I prefer to use pure-Ruby libraries when I can, but I cannot ignore all the cool and useful libraries that the Java ecosystem has produced over the last 15 years.
Keep an eye out for more JRuby content in the coming weeks, and feel free to comment with any requests!