認定試験問題のリファクタリング結果をGroovyにとりあえず移植してみました

最近Groovyから遠ざかっていたため、勘を取り戻す練習も兼ねて、先日Javaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その3) - 達人プログラマーを目指してでご報告したサーティファイJavaプログラミング認定試験リファクタリング結果をGroovyに移植してみました。また、使い始めたばかりで良くは理解できていないのですが、ビルドもMavenからGradleに切り替えてみました。
現時点ではテストも不十分ですし、十分にGroovyらしいコードにも修正できていませんが、一応コードは以下に登録してあります。(ビルド方法は後述)
リファクタリングGroovy版
一方、もともとのJavaリファクタリング結果は以下にあります。
リファクタリングJava版

Javaからの基本的な修正手順

Java版のリファクタリング結果に対して、基本的には以下の手順で修正を加えました。

  • ファイルの拡張子を.javaから.groovyに変更
  • 行末のセミコロンを除去(正規表現検索でほぼ一括で変換可能。s/;$//)
  • publicは暗黙なので除去
  • アクセサ(getter、setter)のあるフィールドのprivateを除去してアクセサを削除*1
  • 文字列の引用符は「"」から「'」に変更("を使うとGStringが生成されて動作が変わるため一旦こうしておく)
  • メソッドの最後のreturnは削除(残してもよい)
  • System.out.printlnはprintlnに変更し括弧を省略*2
  • java.lang、java.util、java.ioなどは暗黙インポートのためimport文を削除

Groovyらしいかどうかはおいておいて、以上のような形式的な修正を行うだけで、ほぼJavaの文法を残したままGroovyのコードとしてコンパイルすることができます。実際MavenからGradleへの移行に多少手間取りましたが、以上のような形式的な変換だけであれば、実質1時間程度で完了しました。なお、もともとのJava版はSpring Frameworkを使ったDIを利用していたのですが、Groovyに変更してもそのまま問題なく利用することができました。このように既存のJavaフレームワークとの抜群の相性の良さはGroovyの最大の利点の一つであると思います。

でも、いくつか注意が必要な落とし穴もある

ただし、実際に移植の作業をしてみてわかったのですが、細かいところを調べてみるといくつかの注意点もありました。

配列の初期化子の記述方法の違い

Javaだと配列の初期化は

(TempHRManagementProgram.java)

	/**
	 * 機能一覧
	 */
	private static final String[] MENU_LIST = {
			"_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/",
			"            人材管理システム",
			"                メニュー",
			"  [1].人材検索(S)",
			"  [2].人材管理(JI:追加 JU:更新 JD:削除)",
			"  [3].稼働状況管理(KI:追加 KD:削除)",
			"  [4].終了(E)",
			"_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/"};

のように、中括弧を使って行うことができます。しかし、Groovyでは中括弧は配列の初期化子として認識されず、上記の部分はコンパイルが通りません。正しくは以下のように角括弧で初期化データを囲む必要があります。

(TempHRManagementProgram.groovy)

	/**
	 * 機能一覧
	 */
	private static final def MENU_LIST = [
			'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/',
			'            人材管理システム',
			'                メニュー',
			'  [1].人材検索(S)',
			'  [2].人材管理(JI:追加 JU:更新 JD:削除)',
			'  [3].稼働状況管理(KI:追加 KD:削除)',
			'  [4].終了(E)',
			'_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/']
コレクションに対するプラス演算子の意味の違い

Javaの場合型ごとに演算子に独自の意味を持たせることができないため、文字列とオブジェクトを「+」で連結するとtoString()により文字列化される動作となります。しかし、Groovyの場合、+演算子が再定義されている場合があります。実際、配列やリストに対しては要素の追加という意味になっています。これは通常大変に便利な仕様なのですが、以下のJavaコードは正しく動作しなくなってしまいますね。

(ConsoleImpl.java)

	@Override
	public String acceptFromList(List<String> selectList, String message) {
		String result = null;
		while (true) { 
			System.out.println(message);
			System.out.print(selectList + promptString); // JavaとGroovyで動作が異なる。
			
			result = doAcceptChars();
			if (selectList.contains(result)) return result;
		}
	}

とりあえず、現状のGroovy版では以下のように修正してみました。ちょっとしたことですが、こうした細かい違いは移植する際には盲点になりがちですね。

(ConsoleImpl.groovy)
	@Override
	String acceptFromList(List<String> selectList, String message) {
		String result = null
		while (true) {
			println message
			print selectList.toString() + promptString // リストを先に文字列化

			result = doAcceptChars()
			if (selectList.contains(result)) return result
		}
	}
charリテラルの違い

もともとJavaで文字定数('A'など)を使っているコードが多い場合、気をつけなくてはならないのはGroovyでは文字定数という概念はないということです。Groovyでは文字数がいくつでも、シングルクウォートで囲んだ文字はStringのインスタンスになります。したがって、Javaの以下のコードはそのままではGroovyで正しく動作しませんでした。

(ConsoleImpl.java)

	/**
	 * キーボードからの入力受取り
	 * 
	 * @return 入力文字列
	 */
	private String doAcceptChars() {
		int c;
		StringBuilder sb = new StringBuilder();
		try {
			InputStreamReader isr = new InputStreamReader(System.in,
					System.getProperty("file.encoding"));
			do {
				c = isr.read();
				if (c == '\n') {
					return sb.toString();
				} else if (c == '\r') {
					c = isr.read();
					if (c == '\n') {
						return sb.toString();
					} else {
						sb.append('\r');
						sb.append((char) c);
					}
				} else {
					sb.append((char) c);
				}
			} while (c != -1);
			
		} catch (IOException e) {
			System.err.println("入力受取りエラー:" + e.getMessage());
		}
		
		return null;
	}

Groovyでどうしても文字定数を表現したい場合'A' as charという宣言をする必要があるみたいです。ただし、上記でやっていることは標準入力から一行入力するということができればよいので、アルゴリズムの取替えリファクタリングで丸ごと以下のように修正してしまうのが実は簡単な解決策でした。

(ConsoleImpl.groovy)
	/**
	 * キーボードからの入力受取り
	 * 
	 * @return 入力文字列
	 */
	private String doAcceptChars() {
		new BufferedReader(new InputStreamReader(System.in)).readLine()
	}
do-while文がGroovyでは使えない

通常めったに使う必要はないのですが、Javaにあるdo-while構文はGroovyでは使えません。もともとのJava版にはこのdo-while文があったため、修正が必要でした。

Groovyにしたことで簡易化された部分

エンティティクラスのプロパティアクセス

この例題だとエンティティは単なるデータの入れ物であり、もともとgetterとsetter以外のロジックはあまり持っていませんでした。Groovyにすると既に説明したようにアクセス指定なしのフィールドがデフォルトでgetter、setterの振る舞いをするため、手動でこうしたメソッドを作成する必要がなくなります。以下のようにgetter、setterがないため、Java版と比較してかなりすっきりとした実装となっています。

(HumanResource.groovy)

package sample.domain

import java.util.ArrayList
import java.util.Arrays
import java.util.List

/**
 * 人材エンティティ
 */
class HumanResource extends Party {

	/** 誕生日 */
	String birthDay

	/** 性別 */
	String genderType

	/** 業種ID */
	long occupationId

	/** 経験年数 */
	String yearOfExperience

	/** 最終学歴 */
	String schoolBackground

	/** 希望単価 */
	String requestedSalary

...
}

また、エンティティのプロパティにアクセスする側もメソッド呼び出しの形式ではなく、フィールドのアクセスの形で記述することが可能です。

(InputWorkFunction.java)

work.setHrId(hrId);
work.setPartnerId(Long.valueOf(console.acceptFromNameIdList(partnerRepository.findAll(), "取引先を選択してください。")));
work.setStartDate(console.accept("稼動開始日を入力してください。"));
work.setEndDate(console.accept("稼動終了日を入力してください。"));
work.setContractSalary(console.accept("契約単価を入力してください。"));

return work;
(InputWorkFunction.groovy)

work.hrId = hrId
work.partnerId = console.acceptFromNameIdList(partnerRepository.findAll(), '取引先を選択してください。')
work.startDate = console.accept('稼動開始日を入力してください。')
work.endDate = console.accept('稼動終了日を入力してください。')
work.contractSalary = console.accept('契約単価を入力してください。')

両者を比較して、それほど劇的には単純になっているわけではありませんが、Groovyの方は括弧の数などで多少見通しのよいコードになっていることがわかると思います。

GStringの使用による文字列結合の簡易化

Javaでは文字列中に動的な値を埋め込みたい場合、以下のようにプラス演算子で結合することが普通です。(あるいはStringBuilderを使って生成することもできますが。)

console.display "人材ID: " + selectedHumanResource.getId() + "で登録されました。"

GroovyではGStringという便利な機能があり、二重引用符で囲まれた文字列はJavaのStringではなくGStringとして処理されます。この場合、以下のように$記号を使ってGroovyの式を埋め込むことができます。GStringを使うことで複雑な文字列を生成するのが楽になります。

console.display "人材ID: ${selectedHumanResource.id}で登録されました。"
クロージャーを使って無名内部クラスを削除

もともとのJava版では、入力値をチェックするために、以下のインターフェースを定義していました。

package sample.common.console;

public interface ValidInput<T> {
	boolean isValid(T input);
}

そして、実際に入力値を端末から入力する処理で、上記のインターフェースを実装した無名内部クラスを以下のように使用していました。

// 人材ID入力
long hrId = console.acceptLong("人材IDを入力してください。", new ValidInput<Long>() {
	@Override
	public boolean isValid(Long input) { // 人材ID存在チェック
		return hrRepository.findById(input) != null;
	}
});

Groovyではクロージャーという仕組みが使えるため、わざわざインターフェースを定義する必要もなく、以下のように簡単に記述することができます。

// 人材ID入力
long hrId = console.acceptLong('人材IDを入力してください。', {input ->
	hrRepository.findById(input) != null
})
Groovy JDK(GDK)によるJDKクラスの機能拡張

私がGroovyのもっとも気に入っているところはGDKと呼ばれる部分です。これは、既存のJDKのクラスに対して足りていない便利メソッドを追加するための機能です。つまり、StringやFileといったJDKのおなじみのクラスに対して見かけ上*3新しいメソッドが追加されているように見えます。
http://groovy.codehaus.org/groovy-jdk/
GDKの拡張で特に注目に値するのは、File IO関連の処理ですね。
http://groovy.codehaus.org/groovy-jdk/java/io/File.html
たとえば、Fileクラスに追加されているeachLineというメソッドを使えば、手動でReaderを作成することなく、テキストファイルの各行の処理を実行し、また、自動的にリソースの解放処理まで実行してくれます。

private List<E> doFind(Closure matcher) {
	try {
		List<E> result = []

		// マスタから1行ずつ読込み
		masterFile.eachLine(encoding, { line ->
			
			E entity = toEntity(line)
			if (!entity.logicalDeleted && matcher.call(entity)) {
				result.add(entity)
			}
		})

		return result

	} catch (IOException e) {
		throw new SystemException('検索処理実行時にIO例外が発生しました。', e)
	}
}

まとめ

この記事を書いている段階では、とりあえずJava版から最低限の修正をしてGroovyに移植しました。Groovyの高度なメタプログラミングDSL作成機能などを利用すれば、もっと単純化される余地が残っています。しかし、このようにJavaとほとんど同じような状態でも立派なGroovyのプログラムとして動作し、また、SpringやcommonsなどのおなじみのJavaのライブラリーと問題なく連携できているということは注目に値します。
なお、ここでは練習のため全てのJavaクラスをGroovyに置き換えてみたのですが、本来はJavaとGroovyはお互いに共存できるはずなので適材適所で使い分けるということが現実的なのではと思います。もちろん、IDEAのように高度なツールが利用できるのであれば、思い切って全体をGroovyで作るというのもないわけではないなと感じました。
Groovyをうまく活用するためのパターンについては、id:kskyさんの以下のエントリーも参考になると思います。7つのGroovy利用パターン - Groovyラボ

Intelli J IDEAについての状況報告

ちなみに昨日紹介したIntelli J IDEAですが、細かい使い方は現状よく把握できていませんが、想像以上に快適にGroovyの開発ができることがわかりました。EclipseJava環境と比べてもほとんど遜色ないくらいです。

Gradleとの連携やGitとの連携もスムーズです。(ただし、私の環境ではGitHubへのPushが認証エラーになってしまうため、それだけはコマンドライン上で処理しています。)もう少し要領がわかったら別エントリで紹介させていただこうと思います。

サンプルのビルド手順についての補足

Github上に上げてあるサンプルを取得して、ビルドする方法について簡単に補足します。

1.IntelliJ IDEAのインストール

まずは、IntelliJ IDEAはGroovyの学習や開発に最適なIDEだった - 達人プログラマーを目指しての説明にしたがって、IntelliJとGroovyをインストールし、正しくHelloWorldが実行できることを確認してください。

2.Gradleのインストール

既に説明したようにこのプロジェクトはGradleというGroovyを使ったツールを利用してビルドすることを前提としています。まず、以下からGradleの最新バージョンをダウンロードし、適当なフォルダーに展開してください。
Gradle | Installation
次に、GRADLE_HOMEを展開先のフォルダーに指定し、GRADLE_HOME配下のbinをPATHに追加してください。

3.Gitのインストール(オプション)

Gitを使ってローカルでバージョン管理したい場合はあらかじめGitをインストールしておく必要があります。以前はWindowsでGitの環境を整えるのは結構困難だったようですが、最近はかなりツールが充実してきているようです。Windows上でのGit環境の構築は以下が参考になります。
http://www.symfony.gr.jp/git/setup-git-windows

4.ソースコードの取得

Git環境が使える場合は、取得先のフォルダーに移動し、以下のコマンドを実行してレポジトリーのクローンを作成します。

git clone git://github.com/ryoasai/certification-refactoring-groovy.git

Gitを使わない場合は、以下からソースをダウンロードして展開してください。
https://github.com/ryoasai/certification-refactoring-groovy/archives/master

5.IntelliJのプロジェクトの作成

上記のソースコード取得先フォルダー内で以下のコマンドを実行することで、IntelliJのプロジェクトを生成できます。

gradle -d idea

初回の実行時はライブラリーがダウンロードされるため数分程度時間がかかります。(進行状況をモニタリングするため-dフラグつきで実行しています。)

6.IntelliJのプロジェクトの設定

IntelliJ IDEAを起動して、File→Open Projectメニューを選択して先ほどのステップで作成したプロジェクトを開きます。

File→Project Structureを選択し、Project SDKを以下のように正しく設定しなおします。

*1:Groovyではスコープ指定のないフィールドに対しては自動的にアクセサがあるように振舞う。

*2:Rubyと違ってGroovyではパラメーターをとらないメソッドの括弧は省略できないため注意が必要

*3:実際にJDKのクラスファイルに対して直接メソッドを追加しているわけではない。