JRubyでJavaBeansを簡単に生成する方法

jruby-users MLでJavaBeansのクラスを簡単に生成する方法はない?という質問が出ていました(http://www.ruby-forum.com/topic/1398824)。Groovyだと、def book = new Book(title: "Foo", author: "Bar") でできるみたいだ。GroovyはJavaのためのスクリプティング言語だから、このあたりはやはり便利にできていますねぇ。で、JRubyの場合。Nick Sieger氏が書いていますが、特にビルトインされているラッパのようなものはありません。ただ、Rubyはフレキシブルだから、そんな感じでインスタンスを生成できるようにする方法はいろいろとあるはずです。Sieger氏が提案していたのは*new*という名前のメソッドを定義したモジュールを作って、*extend*でそのモジュールを既存のクラスに追加する方法でした。さすがです。さっそく試してみました。

まずはこんなディフォルトコンストラクタ(implicit)、setter/getterメソッドだけのJavaBeansを定義して、、、

public class Book {
    private String title;
    private String author;

    public void setTitle(String title) {
        this.title = title;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }
}

javacでコンパイル。Book.classができました。次に、同じディレクトリにSieger氏がMLに投稿していたRubyコードをコピペして、doが抜けているところを足して、確認用のメソッドを追加したのがこちらです。

require 'java'
java_import 'Book'

module BeanLikeConstructor
  def new(attrs = {})
    super().tap do |obj|
      attrs.each do |k,v|
        obj.send("#{k}=", v) if obj.respond_to?("#{k}=")
      end
    end
  end
end

class Book
  extend BeanLikeConstructor
end

book1 = Book.new(:title => "Foo1", :author => "Bar1"); puts "#{book1.title} by #{book1.author}"
book2 = Book.new(:title => "Foo2", :author => "Bar2"); puts "#{book2.title} by #{book2.author}"

これを実行すると、

Foo1 by Bar1
Foo2 by Bar2

と無事表示されました。上記のコード中obj.send("#{k}=", v)とあるのは、BookクラスがJRubyにやってきたときに、setTitle(String title)というメソッドから、title=というメソッドが生成されるようになっているからです。ちなみに、BookクラスがJRuby上でどんメソッドを持っているか調べると、

irb(main):004:0> Book.new.methods
=> ["author", "set_author", "set_title", "title=", "setTitle", "author=", "__jcreate!", "getTitle", "get_title", "__jsend!", "title", "setAuthor", "get_author", "getAuthor", "get_class", "notifyAll", "notify", "toString", "notify_all", "clone", "finalize", "to_string", "hash_code", "equals", "getClass", "wait", "hashCode", "equals?", "initialize", "equal?", "java_send", "marshal_dump", "marshal_load", "java_method", "to_s", "java_object=", "synchronized", "java_class", "java_object", "==", "to_java_object", "hash", "inspect", "eql?", "handle_different_imports", "__jtrap", "include_class", "java_kind_of?", "java_signature", "methods", "freeze", "extend", "nil?", "object_id", "method", "tainted?", "is_a?", "instance_variable_get", "instance_variable_defined?", "instance_variable_set", "display", "send", "private_methods", "enum_for", "com", "to_java", "type", "instance_of?", "id", "taint", "class", "java_annotation", "instance_variables", "org", "to_a", "__send__", "=~", "protected_methods", "__id__", "java_implements", "tap", "frozen?", "java", "respond_to?", "instance_eval", "===", "java_package", "untaint", "java_name", "to_enum", "singleton_methods", "instance_exec", "dup", "kind_of?", "java_require", "javax", "public_methods"]

のようになっています。title=とかauthor=とかありますね。getTitle()やgetAuthor()はシンプルにtitle, authorが使えるようになっています。


Sieger氏のサンプルコードは使うときにとてもいい感じなのですが、見てすぐに思ったのは"長い"。たかがJavaBeansのインスタンス生成。そこまでしなくてもいいんじゃないか、と思ったのでした。そこで、どうすればもうちょっとシンプルにできるか考えてみました。

require 'java'
java_import 'Book'

book_ = lambda {|data| b = Book.new; data.each do |k,v| b.send("#{k}=", v) end; b}

book1 = book_.call(:title=>"Foo1", :author=>"Bar1"); puts "#{book1.title} by #{book1.author}"
book2 = book_.call(:title=>"Foo2", :author=>"Bar2"); puts "#{book2.title} by #{book2.author}"

不器用な感じのコードですが、ちょこっと書くだけですむ、かも。

るびまゴルフあたりの方々なら、きっともっとかっこいい、短いコードを書いてくれるのでしょうねぇ。



[追記] もう一歩
よりGroovyらしく見えるように callメソッドのエイリアス newを定義してみました。

book_ = lambda {|data| b = Book.new; data.each do |k,v| b.send("#{k}=", v) end; b}
class Proc
  alias new call
end

すると、

book3 = book_.new(:title=>"Foo3", :author=>"Bar3"); puts "#{book3.title} by #{book3.author}"

でいける。さらに、1.9モードで使う (jruby --1.9 code.rb ) と、

book4 = book_.new(title: "Foo4", author: "Bar4"); puts "#{book4.title} by #{book4.author}"

でいける。これで、Groovy の def book = new Book(title: "Foo", author: "Bar") にさらに近づいたかも。