日経ソフトウエア2006年10月号
特集1「Rubyで仕事がラクになる!」

Part4「RubyによるJavaソースコード生成」
補足用特設ページ

 このページは日経ソフトウエア2006年10月号特集1「Rubyで仕事がラクになる!」のPart4「RubyによるJavaソースコード生成」の補足用特設ページです。本誌では誌面の都合上,説明しきれなかった「テンプレート・エンジンを利用したコード生成部分のリファクタリング」「テストの改良」「Maven2による自動生成ツールの実行自体の自動化」について説明しています。本誌の該当記事をまだご覧でない方は,まずそちらをお読みになってから,このページにお越しください。

テンプレート・エンジンを利用する

本誌の記事ではツールの機能を一通り実装しましたが,ここでは少しコードを見直してみたいと思います。

作成したツールの最終的な出力はソースコードなので,どうしてもインデントやコメントといった見た目に関するフィードバックが多くなります。

その場合,以下のように文字列処理を多用していると,見た目に関する部分がコードに埋もれてしまい,修正が困難になってしまいます。

module Klass
   include JavaElement
〜 略 〜
   def to_s
         header = <<-EOS
package #{pkg_name};

/**
 * #{klass_name}.java
 *
 * this code was generated by RubyGen
 */

public class #{klass_name} {

      EOS

      body = ""
      properties.each do |prop|
         body << <<-EOS
    private #{prop.fqcn} #{prop.prop_name};
         EOS
         end

         properties.each do |prop|
            body << prop.to_s
         end

         footer = <<-EOS
}
EOS

      header << body << footer
   end
end

こうした場合,テンプレート・エンジンを使って,見た目の部分をテンプレートとしてコードから切り離しておくと,メンテナンスがとても楽になります。

Rubyでは,標準でERBというテンプレート・エンジンが利用できます。ERBはeRubyという記法で記述されたテンプレートを処理するための実装の一つで,Ruby on Railsも採用しています。

これを使って,文字列処理を駆使している部分をテンプレートとして切り出すようにリファクタリングしてみます。

まず,クラス部分のテンプレートは以下のようになります。

JAVA_KLASS = <<EOS
package <%= pkg_name %>; -- (1)

/**
 * <%= klass_name %>.java
 *
 * this code was generated by RubyGen
 */

public class <%= klass_name %> {

% properties.each do |prop| -- (2)
    private <%= prop.fqcn %> <%= prop.prop_name %>;
% end
% properties.each do |prop|  -- (3)


<%= prop.to_s %>
% end
}
EOS

ERBでは,<%= %>で囲まれている部分を評価した結果を文字列として埋め込みます。(1)では,それを利用して,パッケージ名を埋め込んでいます。また,<% %>で囲まれた部分や行頭が%で始まる部分は直接,Rubyのコードとして処理されるので,(2)ではプロパティの定義情報から変数の宣言部分を生成しています。同様に,(3)ではそれぞれのメソッド部分の生成を行っています。

では,クラス用の出力処理をERBを利用した形に変更してみましょう。

require 'erb' -- (1)
〜 略 〜
   module Klass
      include JavaElement
      〜 略 〜

      def to_s
         ERB.new(JAVA_KLASS, nil, '%').result(binding()) -- (2)
      end
   end
〜 略 〜

ERBを利用するために,これまでと同様に(1)のように読み込みます。実際にERBを利用している部分は(2)になります。ここでは,インスタンス生成時に利用するテンプレートと%で始まる行は余計な改行を抑止するように設定し,resultメソッドを利用してテンプレートを適用しています。

ここでresultメソッドの引数で指定しているbindingメソッドは,Bindingというオブジェクトを生成する組み込み関数です。Bindingオブジェクトはクラス自身の実行時の情報を保持しており,テンプレートは,クラスに設定されている値やローカル変数の値をここから取得して値を埋めていきます。

この情報を利用することで,テンプレート内の「pkg_name」という記述がJavaSourceTemplate::Klass#pkg_nameを参照できます。

メソッドを生成する部分のテンプレートとコードは以下のようになります。

JAVA_PROP = <<EOS
    public <%= fqcn %> <%= getter_prefix %><%= method_name %>() { -- (1)
        return this.<%=prop_name %>;
    }

    public void set<%= method_name %>(<%= fqcn %> <%= prop_name %>) {
        this.<%= prop_name %> = <%= prop_name %>;
    }
EOS
〜 略 〜

module Property
   include JavaElement
   〜 略 〜

   def to_s
      getter_prefix = (type_name == "boolean" ? "is" : "get") -- (2)
      method_name = prop_name.initcap -- (3)

      ERB.new(JAVA_PROP, nil, '%').result(binding).chomp
   end
end
〜 略 〜

ここでは,(1)で処理しているgetter_prefixとmethod_nameの値を(2)と(3)で用意しています。

また,ここではERBのテンプレートを定数として記述しましたが,外部ファイルにするのも難しいことではありません。

ERBを利用することでコード自体がシンプルになり,保守性や拡張性も向上しました。このように,Rubyではテンプレート・エンジンを簡単に導入できるので,メンテナンスのコストを下げるのも難しくありません。

テスト再び

次にテストについて,もう少し補足したいと思います。

今回のツールを実装するのに,いくつかのテスト・クラスを実装しました。筆者は,テストに関してはクラスごとにファイルを用意するのが可読性の面からも良いと思っているので,四つのテストをファイルを分けて作成しました。

ただし,複数のファイルだと実行が面倒なので,すべてのテストを実行するための工夫が必要です。そこで,以下のようなテスト実行用のスクリプトを用意しました。

# test_all.rb
test_files = "test/*_test.rb"

Dir.glob(test_files) {|file| require file.sub(/\.rb$/, '')}

また,テスト実行時に毎回,テスト対象となるコードをロードするためのパスをコマンド・オプションで記述するのは面倒なので,筆者は以下のようなファイルを用意して,テスト・クラスで読みこんで利用しています。

# test_setup.rb
require 'test/unit'

$KCODE = 'sjis'
$LOAD_PATH.push(File.dirname(__FILE__) + '/../bin')

require 'rubygen'

このファイルをそれぞれのテスト・クラスで「require 'test/unit'」と書いて読み込むようにすれば,わずらわしいコマンド・オプションを書く必要がなくなります。

# rubygen_test.rb
require 'test/test_setup'

class RubyGenTest < Test::Unit::TestCase
〜 略 〜

> ruby test\java_template_test.rb
Loaded suite test/java_template_test
Started
.......
Finished in 0.010039 seconds.

7 tests, 17 assertions, 0 failures, 0 errors

こうした簡単な工夫でテストは楽になります。

また,最近ではRakeというビルド・ツールを利用してテストを実行するのが一般的になっています。RakeはRuby on Railsでも使われているツールです。詳細な説明は省略しますが,興味のある方は調べてみてください。

ビルド・プロセスに組込もう

最後に,コード自動生成ツールの実行自体も自動化できないか?ということを考えていきましょう。

ツールを適宜実行するのは面倒な作業なので,つい実行を忘れるというミスはどうしても起こります。開発者が普段行う一連の作業(ビルド・プロセス)に連携してツールを実行するようにすれば,この問題を防げます。

Javaには,ビルド・プロセスを支援するツールがあります。こうしたツールと連携することで,コード自動生成ツールをビルド・プロセスに組み込むことを目指します。

Maven2とは

ここでは,Mavenというビルド・プロセスを自動化するツールを紹介します。

Maven(http://maven.apache.org)はApache Software Foundationで開発されているプロジェクト管理ツールです。ソフトウエア開発に必要な標準的な作業を自動的に実行してくれます。ここ数年で,かなり人気のあるツールになっているので,利用している方も多いのではないでしょうか。

Mavenは現在,1.x系と2.0系の二つが開発されています。ここでは,これから主流になるであろうMaven 2.0を取り上げます。

それでは,まずインストールしてみましょう。

Webサイト(http://maven.apache.org/download)から最新版をダウンロードします。2006年8月下旬時点の最新版は2.0.4なので,maven-2.0.4-bin.zipを利用します。

ファイルをダウンロードしたら,任意の場所(以降,MAVEN_HOMEと呼びます)に展開します。これで,インストールは完了です。

それでは,早速,実行してみましょう。MAVEN_HOME\bin以下にコマンドが提供されているので,あらかじめパスを設定しておきます。また,実行するには,Java開発環境とJAVA_HOME環境変数の設定が必要なので,これらも用意しておいてください。

以下のコマンドで正常にインストールできたかどうかを確認できます。

>mvn -v
Maven version: 2.0.4

次に,コード自動生成ツールと連携するための,簡単なJavaのプロジェクトを作成してみます。この作業も提供されている機能を利用できるので,以下のコマンドを実行するだけで完了です。

>mvn archetype:create -DartifactId=sample -DgroupId=com.example

ここで指定しているartifactIdというプロパティ(最初の-Dの後の部分)は,プロジェクトを識別するためのユニークなIDになり,プロジェクトが格納されるディレクトリ名にもなります。また,groupId(次の-Dの後の部分)はパッケージ名になります.

コマンドの実行が完了すると,以下のようなディレクトリとファイル群が作成されます。

sample
|-- pom.xml       <-- プロジェクトの設定ファイル
`-- src
    |-- main
    |   `-- java  <-- Java ソースコード
    |       `-- com
    |           `-- example
    |               `-- App.java
    `-- test
        `-- java  <-- Java テストコード
            `-- com
                `-- example
                    `-- AppTest.java

コマンドを実行すると,実行に必要な依存関係があれば,ライブラリなどをダウンロードします。もし,ファイアウォール内の環境で実行する場合には,プロキシの設定を行う必要があります。MAVEN_HOME\conf\settings.xmlで設定できるので,適宜,修正してください。

該当個所は以下の通りです。

<proxies>
<!--  proxy
   | Specification for one proxy, to be used in connecting to the network.
   |
  <proxy>
    <id>optional</id>
    <active>true</active>
    <protocol>http</protocol>
    <username>proxyuser</username>
    <password>proxypass</password>
    <host>proxy.host.net</host>
    <port>80</port>
    <nonProxyHosts>local.net,some.host.com</nonProxyHosts>
  </proxy>

--> 
</proxies>

Maven2と連携しよう

コード自動生成ツールとの連携を考える前に,Mavenで規定されている,開発者が行う作業を簡単に見てみましょう。

以下はデフォルトで規定されている作業の一部です。

Mavenでは,各作業をフェーズと呼んでおり,一連の流れをライフサイクルと呼んでいます。

例えば,テストを実行する作業はtestフェーズ,といったように規定されています。これらのフェーズをコマンドで指定することで,各作業を実行できます。

>mvn test

testフェーズを実行するときには,それ以前に実行すべきフェーズは自動的に実行されます。testフェーズを実行した場合,その前に規定されている,ソースコードをコンパイルするcompileフェーズやテストコードをコンパイルするtest-compileフェーズは自動的に実行されます。

これらのフェーズで実際に行われる処理は,すべてプラグインの機構を利用しています。例えば,compileフェーズでは,compilerプラグインが必要な処理を実行します。

こうしたプラグインは,任意のフェーズで実行するように設定することが可能です。例えば,testフェーズで,コーディング規約遵守ツールを実行するプラグインを同時に適用するといったことができます。

それでは,Mavenで規定されているフェーズをもう一度見てみましょう。Mavenでは,デフォルトでgenerate-sourcesというコード生成のためのフェーズが用意されています。これを実行してみましょう。

>mvn generate-sources
[INFO] Scanning for projects...
[INFO] ---------------------------------------------
[INFO] Building Maven Quick Start Archetype
[INFO]    task-segment: [generate-sources]
[INFO] ---------------------------------------------
[INFO] No goals needed for project - skipping
[INFO] ---------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ---------------------------------------------
[INFO] Total time: < 1 second
[INFO] Finished at: Wed Jul 12 02:19:14 JST 2006
[INFO] Final Memory: 1M/2M
[INFO] ---------------------------------------------

残念ながら,generate-sourcesでは実行されるプラグインが設定されていないので,特に何の処理も行われずに終了してしまいます。そこで,generate-sourcesフェーズにコード自動生成ツールを実行するプラグインを指定してみましょう。これで目的を達成できそうです。

ここでは,プラグインを自作するのではなく,単純に外部コマンドを実行するプラグインを利用するアプローチを採ります。

外部コマンドを実行するプラグインは,Maven本家ではなくMojo(http://mojo.codehaus.org/)というCodehausがホストするプロジェクトが提供しています。

MojoはMaven2で利用できる有用なプラグインを開発しているプロジェクトです。この記事では「可用性」を優先したので対象外としましたが,実は,Rubyスクリプトを直接実行するプラグインも提供されています。興味のある方はぜひ利用してみてください。

では,Mojoが提供しているプラグインの中から外部コマンドを実行するExecプラグイン(http://mojo.codehaus.org/exec-maven-plugin/)を利用してみましょう。

その前に,先ほど作成したsampleプロジェクトにコード自動生成ツールと入力になるドキュメントを配置しておきます。ツールはsample\toolの下に,ドキュメントはsample\docの下に置きました。

ディレクトリ構成は以下のようになります。

sample
|-- doc
|   `-- rubygen_input.xls
|-- pom.xml
|-- src
|   |-- main
|   |   `-- java
|   |       `-- com
|   |           `-- example
|   |               `-- App.java
|   `-- test
|       `-- java
|           `-- com
|               `-- example
|                   `-- AppTest.java
`-- tool
    `-- rubygen.exe

次にExecプラグインを利用するための設定を行いましょう。編集するファイルは,プロジェクトを格納しているディレクトリの直下にあるpom.xmlです。ここでは,sample\pom.xmlになります。

設定は以下の通りです。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         〜 略 〜>
  〜 略 〜
  <!-- build -->
  <build>
    〜 略 〜
    <plugins>
      <!-- exec -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.0.1</version>
        <configuration>
          <executable>${basedir}/tool/rubygen.exe</executable>
          <arguments>
            <argument>-d</argument>
            <argument>${basedir}/src/main/java</argument>
            <argument>${basedir}/doc/rubygen_input.xls</argument>
          </arguments>
        </configuration>

        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>exec</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      〜 略 〜

設定した内容を簡単に見てみます。

<!-- exec -->
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>1.0.1</version>

この部分で,利用したいプラグインのIDとバージョンを指定します。

次にプラグイン特有の設定を行っています。設定はconfigurationタグの中に記述します。ここでは,コード自動生成ツールの場所と引数を指定しています。

また,設定中に出現している${basedir}${project.compileSourceRoots}といった記述は,Mavenが定義している変数の一部です。実行時にそれぞれ定義されている値に置換されます。${basedir}はプロジェクトのトップ・ディレクトリを表しており,実行時にsampleディレクトリのパス情報に変換されます。また,${project.compileSourceRoots}はコンパイル対象のソースが格納されているディレクトリを表わしており,ここではsample\src\main\javaディレクトリのパス情報に展開されます。これら以外にも様々な情報を表す変数が定義されているので,Mavenのドキュメントなどを参考に調べてみてください。

そして,プラグインをどのフェーズで実行するかを指定します。

<executions>
  <execution>
    <phase>generate-sources</phase>
    <goals>
      <goal>exec</goal>
    </goals>
  </execution>
</executions>

今回は,generate-sourcesフェーズで実行するので,phaseタグに指定しています。また,Mavenのプラグインに行わせたい作業は,それぞれのプラグインでgoalという単位で提供されています。ここでは外部コマンドを実行するためのgoalであるexecを指定しています。

これで,Mavenとコード自動生成ツールを連携させるための準備は終了です。再度,generate-sourcesフェーズを実行してみましょう。

>mvn generate-sources
[INFO] Scanning for projects...
[INFO] ----------------------------------------------
[INFO] Building Maven Quick Start Archetype
[INFO]    task-segment: [generate-sources]
[INFO] ----------------------------------------------
[INFO] [exec:exec {execution: default}]
[INFO] ----------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ----------------------------------------------
[INFO] Total time: 1 second
[INFO] Finished at: Thu Jul 13 02:14:05 JST 2006
[INFO] Final Memory: 2M/4M
[INFO] ----------------------------------------------

ちゃんと,Execプラグインが実行されました。

ディレクトリの中には,以下のように自動生成されたソースコードがソース・ディレクトリ内に保存されています。

sample
|-- doc
|   `-- rubygen_input.xls
|-- pom.xml
|-- src
|   |-- main
|   |   `-- com
|   |       `-- example
|   |           |-- App.java
|   |           `-- Person.java
|   `-- test
|       `-- java
|           `-- com
|               `-- example
|                    `-- AppTest.java
`-- tool
    `-- rubygen.exe

先ほど述べたように,compileやtestといったgenerate-sourcesフェーズの後の作業として規定されているフェーズを実行したときにも,generate-sourcesは自動的に実行されます。

これで,開発者がツールの実行を忘れるというミスは防げるようになります。加えて,入力になるドキュメントをdocディレクトリに置くといったルールも規定すれば,SubversionやCVSといったソースコード管理ツールで,ソースコードとともにドキュメントも管理できるようになり,開発者が最新のドキュメントに更新するのを忘れることもなくなります。

最後はRubyの話ではなくなってしまいましたが,コード自動生成ツールとMavenを連携させることで,ツールとしての利用価値が高まります。このような便利な仕組みを構築することは,実際の業務にRubyを導入する良いきっかけになるでしょう。


Copyright (c)1998-2006 Nikkei Business Publications, Inc. All Rights Reserved.
このWebサイトに掲載されている記事・写真・図表などの無断転載を禁じます。詳しくはここをクリックして下さい。
Nikkei BP Networkにおける個人情報保護について