[ Java Project Exampleページへ戻る ]
時刻プログラム開発のイテレーションNo.2である。
いよいよ2回目のイテレーションに入る。まずは、この2回目のイテレーションで開発する機能や確立するインフラといった目標を定める。
まずは、構想で挙げた時刻プログラムの機能から未開発のものを列挙する。次に、イテレーションNo.1の過程で発生した課題を列挙する。これらの中から、イテレーションNo.2の開発目標を選定する。
イテレーションNo.1の課題がかなり多い。この課題を避けたまま開発を続けると破綻が待ち受けているであろうから、時刻プログラムの機能は最低限の追加とし、課題の解決を図っていくことにする。
最低限の機能としてやはり時を刻む機能は欠かせない。そこで、イテレーションNo.2ではこの機能を開発する。
非常に多くの課題があり、すべてを1回のイテレーションで解決することは難しい。そこで、早めの段階で確立しておくことが望ましい課題を選定する。
テストを後回しにすれば、テスト自体が行われないことは多くの開発プロジェクトの事例から明白である。時間がないからテストを後回しにした結果、納期間際でますますテスト時間がなくなり、結果としてテストが不十分なままリリースされるという悪循環は避けねばならない。そこで、「ユニットテスト(テストファースト)の導入」と「試験のやり方を確立」を今回のイテレーションで解決する。次に、「コンパイル方法の改善」を入れたのは、テストをするためには時刻プログラム本体のほか、多くのテストツール・テストケースを開発することになるため、コンパイル手順が重要となるからである。1回のイテレーションであまりたくさんのことをし過ぎると消化不良気味になるため、「コンパイル方法の改善」、「試験のやり方の確立」は次回以降へ繰り下げることにした。
UMLツールはもう少し設計が複雑になってからでも間に合うと踏んで今後に見送った。デバッグ環境、ロギング環境は、プログラムがもっと複雑になってから真価を発揮するので、まだ着手しなくても大丈夫であろう。時刻の指定を外部から与える、実行方法の改善は、時刻プログラムを頻繁に動かすようになってから欲しくなる機能なので、これもまだ先送りできる。レビューの改善も、コードがもっと増えてきた時点で取り入れればよいだろう。APIドキュメント作成の改善もパッケージがもっと増えた時点で検討することになるだろう。配布方法はもっと製品として完成の域に達してから考えることだろう。構成管理インフラの確立についてはそろそろ着手したいのだが、これは少々難しいので次回イテレーションにおいて取り組むこととしたい。
今回のイテレーションで開発するのは、時刻を一定のレートで刻む機能と、そのレートを自由に設定する機能である。そこで、まずはイテレーションNo.1で開発したClockクラスにこの機能を入れてみる。
まず、レートの定義であるが、Clockオブジェクトが保持する時間(時分秒)が1秒進むのに、マシンクロックがどれだけ経過するか、その経過時間(interval)で表現することにした。例えば経過時間が10[秒]であれば、マシンクロックが10秒経過すると、Clockオブジェクトが保持する時間が1秒進む、といった具合だ。
次に、Clockオブジェクトが時刻を刻むのはいつかを検討する。オブジェクトが生成されると直ぐに刻み始めるとしたら、まだ経過時間(interval)を設定していない可能性がある。そこで、刻み開始と停止の要求をメソッドとして提供し、外部から指示することにした。
以上の内容をイテレーションNo.1のClockクラスのクラス図に、追加してみた。
Clock |
---|
-hour: int -minute: int -second: int -interval: int |
+getHour(): int +setHour(hour:int):void +getMinute(): int +setMinute(minute:int):void +getSecond(): int +setSecond(second: int):void] +getInterval(): int +setInterval(interval: int):void +start():void +stop():void |
intervalは、ミリ秒を単位とした整数値とする。ミリ秒としたのは、大抵のOSが持つ割り込みが1ミリ秒単位だからで、これ以上の分解能は不要と考えたから。
まずは1つのクラス(Clock)に、時刻更新機能を追加する設計を行った。ここで、intervalごとに時刻を更新する機能を別なクラスに設け、2つのクラスが協調して時刻更新を行うという設計も考えられる。どちらがよいのだろうか?
それぞれのクラスが持つ役割(責務)の点から検討すると、時刻を保持する役割と時刻の更新を行うための間隔を計る役割とに分けてもよいかと思う。また、間隔を計る役割を実装する方式も複数想定される(Thread、Timer、他のClockなど)ため、特定の実装をClock内部に持つのは得策ではないと思う。
一方、XP(eXtreme Programming)におけるYAGNIの教え(You're NOT gonna need it!)によれば、「いずれ必要になる」と言って実装してはいけない。今回の場合、今後実装方式が変わるかもしれないからクラスを2つに分ける、というのは戒めるべきことになる。
そろそろ結論を出さねば先に進めない。では、1つのクラスで行くことにしよう。YAGNIの教えに盲目的に従うのは愚かなことだが、まだ時刻更新の設計に問題があるかどうかは分かっていない(設計に問題があることが判明するのは大抵実装中から試験にかけてである)。この時点で方式を切り替え可能な時刻更新機能を追加することは、設計に問題があった場合に(多分あるだろう)、後で大きなやり直し作業をする羽目になる。
今回はタイミング要素が入ってきたため、きちんとシーケンスを設計することが望ましい。そこで、シーケンス図を書いてみた。
![]() |
使い方は、Clockオブジェクトを生成後、start()メソッドを呼んでから、時刻を取り出す。stop()メソッドを呼んで時刻の更新を停止する。シーケンス図では、時分秒を引数で指定するコンストラクタを使っているが、勿論引数なしのコンストラクタでClockオブジェクト生成後、Setterメソッドで時分秒を設定してもよい。
「契約による設計(Design by Contract)」によって、今回追加したメソッドを設計した。
メソッド | 事前条件 [例外] |
事後条件 | 不変条件 | |
---|---|---|---|---|
setInterval |
引数が0より大きいこと [ IllegalArgumentException ] |
引数の値でintervalフィールドを更新 | 時分秒は維持 | |
start |
stop状態であること [ IllegalStateException ] |
start状態になる | 時分秒は維持 | |
intervalが0より大きいこと [ IllegalStateException ] |
||||
stop |
start状態であること [ IllegalStateException ] |
stop状態になる | 時分秒は維持 | |
今回は、start、stopメソッドを入れることにより、オブジェクトに状態が発生する。そこで、次に状態設計を行う。
Clockオブジェクトが生成された時点では、stop中状態にある。stop中状態では時刻更新は行わない。startメソッドが呼ばれた以後は、start中状態に遷移する。start中状態では、時刻更新を行う。このことを、状態遷移図に書き表した。言葉で説明するより図の方が、明確に意図が伝わる(気がする)。
![]() |
オブジェクトの状態を問合せるメソッドを追加する。また、状態を保持するフィールドを追加する。
Clock |
---|
-hour: int -minute: int -second: int -interval: int -isStart: boolean |
+getHour(): int +setHour(hour:int):void +getMinute(): int +setMinute(minute:int):void +getSecond(): int +setSecond(second: int):void] +getInterval(): int +setInterval(interval: int):void +start():void +stop():void +isStart(): boolean |
状態を、どのように表現するかは設計の重要なポイントである。すぐに思いつくのが、状態を数値化し、現在の状態を示すフィールドを1つ定義する方法だ。次に考えられるのは、デザインパターンの1つであるステートパターンを適用する方法だ。ステートパターンを使った方が今後の拡張性が増すかもしれない。しかし、クラスを分割するか否かで検討したようにClockクラスに時刻更新機能を持たせているのも暫定的なもので、未来永劫設計を変えない訳ではない。そこで、現在のClockクラスに過度の拡張性を持たせても仕方がないため、状態をフィールドで保持する方法でいくことにした。
状態を持たせるフィールドの型だが、状態設計では2つの状態しかないので、booleanとする案と、3つ以上に増えることを考慮しintとする案がある。今回は、booleanで実装していくことにした。ただし、今後のイテレーションで状態が増えれば変更することになる。
さて、実装だ。今回のイテレーションでは、実装にあたってテストファーストによるプログラミングと、コンパイル方法の改善を行う。
テストファーストとは、先にテスト・コードを実装してから本来のコードを実装するやり方。オブジェクト指向プログラミングを行う場合、クラス単位にコードを実装するので、テスト・コードはクラス毎に作成するユニットテスト(単体試験)に相当する。よい設計とは、テストしやすい設計でもあるので、これは非常に理にかなっている。次のURLにいいことが書かれているので参考にする。
テスト・コードの作成だが、開発するクラス毎に一から作成すると非常に面倒だ。「単体(=クラス)ごとにテストをすると時間がかかるし大変なので、テストはしない」などという言語道断な言い訳がまかり通るのも一理はある(本当は一理も認められないけど・・・)。テスト(特にXPのテスティング)の有効性について、以前まとめたものがあるので次のURLに載せておく。
テスト作業を一から行うと大変であるため、何らかのテストツールを使用することが常套手段となる。市販のテストツールは各メソッドのテストを自動的に生成して実行するような高度なものもある。残念ながらこの時刻プログラム開発プロジェクトには資金がほとんど与えられてないため、市販のテストツール採用は適わない。代わりといっては語弊があるが、ユニットテストの定番とも言えるオープンソースのフレームワークであるJUnitを導入することにした。JUnitは、テストケースと呼ぶテストコード作成を簡易化するほか、テストケース実行と結果判定を自動化するため、テスト作業が楽になる。JUnitを使ってテストコードを具体的に書く方法については次のURLに詳しく書かれているので参考にする。
JUnitのインストール方法と簡単な使い方については次のURLに記載している。
本プロジェクトでは、上記インストール方法について一部カスタマイズを行った。
JUnitもバージョンアップされるため、数年後にこのプロジェクトを保守することになった場合に開発で使用していたバージョンを入手できない可能性がある。そこで、プロジェクトが依存するクラスライブラリについてはプロジェクトと同様バージョン管理下に置くことにする。そこで、プロジェクトディレクトリにlibディレクトリを追加し、ここにJUnitのクラスライブラリファイルを配置した。
また、プロジェクトディレクトリ内に配置したことにより、開発マシンが異なっても必要な環境設定が同じで済むというメリットも生じる。
<Project Root> | +---- classes | +---- doc | +---- lib | +---- junit.jar | +---- src | +---- README.txt |
では、早速テストケースの作成に入る。まず、イテレーションNo.1で作成した時分秒を保持する機能をテストするコードを導入の意味も含めて作成する。次に、イテレーションNo.2で開発する機能である時刻更新機能をテストするコードを記述する。
これを順にテストするコードをテストケースに追加していく。
ではまず、ClockクラスのテストケースであるClockTest.javaを記述する。ファイルは、Clockクラスのソースと同じパッケージ・同じディレクトリに配置する。(JUnit実践講座を参考にした)
<Project Root> : +---src : +---jp +---gr +---java_conf +---torutk +---jpe +---clock | +---Clock.java | +---ClockTest.java : |
JUnitを利用する場合にテストケースへ記述する際の規約を、ソースファイルに記述する項目に従って挙げる。
まず、イテレーションNo.1で作成したClockクラスのコンストラクタ(時分秒を指定するもの)をテストするコードを記述する。
/* * Project Clock * Copyright 2002 Toru TAKAHASHI. All rights reserved. */ package jp.gr.java_conf.torutk.jpe.clock; import junit.framework.TestCase; /** * Unit Test for class Clock * * Created: Sat Jun 08 00:03:31 2002 * * @author <a href="mailto:torutk@alles.or.jp">Toru TAKAHASHI</a> * @version 1.1 */ public class ClockTest extends TestCase { /** * Creates a new <code>ClockTest</code> instance. * * @param name test name */ public ClockTest (String name) { super(name); } /** * <code>Clock</code>クラスのコンストラクタ(時分秒)をテストする。 * 正常系として、12時34分56秒を指定するコンストラクタをテストする。 */ public void testNewWithValidHMS() throws Exception { Clock clock = new Clock(12, 34, 56); int h = clock.getHour(); int m = clock.getMinute(); int s = clock.getSecond(); assertEquals("有効な時分秒を指定するコンストラクタによる時の設定", 12, h); assertEquals("有効な時分秒を指定するコンストラクタによる分の設定", 34, m); assertEquals("有効な時分秒を指定するコンストラクタによる秒の設定", 56, s); } /** * <code>Clock</code>クラスのコンストラクタ(時分秒)をテストする。 * 異常系として、25時、60分、-1秒を指定するコンストラクタをテスト * する。IllegalArgumentExceptionがスローされれば正しく動作してる * ことになる。 * */ public void testNewWithInvalidHMS() throws Exception { try { Clock clock = new Clock(25, 0, 0); fail("コンストラクタは異常値(25時)に対して例外をスローするはず"); } catch (IllegalArgumentException e) { // ok } try { Clock clock = new Clock(23, 60, 59); fail("コンストラクタは異常値(60分)に対して例外をスローするはず"); } catch (IllegalArgumentException e) { // ok } try { Clock clock = new Clock(13, 59, -1); fail("コンストラクタは異常値(-1秒)に対して例外をスローするはず"); } catch (IllegalArgumentException e) { // ok } } }// ClockTest |
テストメソッドの単位はけっこういい加減に決めた。コンストラクタのテストだから1つのtestメソッドに記述してもよかったが(普通そうする)、ここでは2つのtestメソッドに分けている。主観的だが見やすさだけで分けた。
テストで与えている引数だが、ちゃんと試験するには有効範囲の境界値(最大と最小)の内外、条件分岐があるときは全ての条件分岐が実行される組み合わせ(カバレッジ)、を網羅することが望ましい。その点から言えば、上記のテストケースは失格である。
testメソッドとしては、時/分/秒のSetter/Getterをテストするものを作るべきだが、今回は省略。本来の機能の開発に関わる部分の作成に早く入りたい。
E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java E:\ClockProject>
コンパイルが成功すれば、テストを実行します。
E:\ClockProject>java -classpath .\classes;.\lib\junit.jar junit.swingui.TestRunner
JUnitのテストケース実行画面(Swing版)が表示されるので、Test classに先ほど作ったClockTestを選ぶ。最初は、[...]を押して、テストケース選択ダイアログからClockTestを選ぶ。この選択ダイアログには、クラスパス上に存在するテストケースのクラスが候補として表示される。
テストの実行は、[Run]を押す。
![]() |
属性intervalとそのアクセッサメソッドのtestメソッドを記述する。JUnitではテストメソッドの名前はtestで始めるので、その後ろに続いてIntervalAccessorを付けた。
/** * intervalフィールドのアクセッサメソッドをテストする。 * 正常値として、1, Integr.MAX_VALUEを指定する。 * 異常値として、0, -1, Integer.MIN_VALUEを指定する。 */ public void testIntervalAccessor() throws Exception { try { Clock clock = new Clock(1, 2, 3); clock.setInterval(1); assertEquals(1, clock.getInterval()); } finally { } try { Clock clock = new Clock(4, 5, 6); clock.setInterval(Integer.MAX_VALUE); assertEquals(Integer.MAX_VALUE, clock.getInterval()); } finally { } try { Clock clock = new Clock(7, 8, 9); clock.setInterval(0); fail("intervalに0を設定すれば事前条件エラーとなるはず"); } catch (IllegalArgumentException e) { // Ok } try { Clock clock = new Clock(10, 11, 12); clock.setInterval(-1); fail("intervalに-1を設定すれば事前条件エラーとなるはず"); } catch (IllegalArgumentException e) { // Ok } try { Clock clock = new Clock(13, 14, 15); clock.setInterval(Integer.MIN_VALUE); fail("intervalにInteger.MIN_VALUEを設定すれば" + "事前条件エラーとなるはず"); } catch (IllegalArgumentException e) { // Ok } } |
ここで、テストケースのコンパイルを実施する。Clockクラスにはまだintervalフィールドもそのアクセッサメソッドも追加していないので、当然コンパイルエラーが発生するはずだ。
E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java:84: シンボルを解釈処理 できません。 シンボル: メソッド setInterval (int) 位置 : jp.gr.java_conf.torutk.jpe.clock.Clock の クラス clock.setInterval(1); ^ src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java:85: シンボルを解釈処理 できません。 シンボル: メソッド getInterval () 位置 : jp.gr.java_conf.torutk.jpe.clock.Clock の クラス assertEquals(1, clock.getInterval());
コンパイルエラーを発生させてから、Clockクラスの追加記述を行う。まずは、コンパイルが通る最低限度の記述、すなわち追加するAPI(インタフェース)を記述する。いきなり全ての実装を書かない理由は、先に作成したテストケースが、間違っているにもかかわらず正しいと判断してしまうバグを避けるためである。つまり、まず本来の機能が実装されていないClockクラスをテストさせて、テスト結果が否(fail)となることを最初に確認する。
/** * インターバルの値を返却する。 * @return ミリ秒:1以上の整数値 */ public int getInterval() { return 0; } /** * インターバルの値を更新する。 * @param anInterval インターバル[ミリ秒]: 1-Integer.MAX_VALUE * @throws IllegalArgumentException 引数がインターバルの有効範囲に * ないとき */ public void setInterval(int anInterval) { } |
ここで、ふたたびClockTest.javaをコンパイルする。今度は、コンパイルエラーが取れ、コンパイルが完了する。(Clock.javaは、ClockTest.javaをコンパイルする時に依存関係からコンパイルし直される)
再び、JUnitのTestRunnerを実行する。下図のように、テストがエラーになればよい。
![]() |
テストが無事(!)エラーとなったことを確認した。
Clockクラスの仮実装だったintervalフィールドとそのアクセッサメソッドをちゃんと実装する。
/** * インターバルの値を返却する。 * @return ミリ秒:1以上の整数値 */ public int getInterval() { return interval; } /** * インターバルの値を更新する。 * @param anInterval インターバル[ミリ秒]: 1-Integer.MAX_VALUE * @throws IllegalArgumentException 引数がインターバルの有効範囲に * ないとき */ public void setInterval(int anInterval) { if (anInterval < 1) { throw new IllegalArgumentException(); } interval = anInterval; } /** インターバル[ミリ秒]を保持する。有効範囲は 1以上 */ private int interval; |
Clock.javaをコンパイルする(ClockTest.javaをコンパイルしてもよい)。
いよいよ最初の機能である属性intervalと、そのアクセッサメソッドの追加をテストする。JUnitのTestRunnerの[Run]ボタンを押す。(TestRunnerはテスト実行の度にクラスファイルをロードし直すことができるので、TestRunner自体を再起動しなくてもよいのが楽)
![]() |
テストがパスすれば、属性intervalと、そのアクセッサメソッドの追加が完成だ。いよいよ次の本題である時刻を更新する機能の実装に入る。
オブジェクトの状態と状態遷移メソッド(start/stop)のtestメソッドを記述する。
この時点で、start中状態のときにどうやって時刻を更新するのか?という疑問が生じる。そっちに気を取られると、Threadを使ってsleepを入れるか、Timerクラスを使うか、と考えがテストから離れていってしまう。いかんっ、とテストに考えを戻す。ここは、実装方法はさておき、どうやってテストを行うかを考えるのだ。
/** * stop/startの状態をテストする。 * 正常系として、インスタンス生成後、startメソッド呼び出し後、 * stopメソッド呼び出し後の各状態を判定する。 */ public void testValidStartStop() throws Exception { Clock clock = new Clock(16, 17, 18); boolean state = clock.isStart(); assertTrue("isStart should be false right after initialized", !state); clock.start(); state = clock.isStart(); assertTrue("isStart should be true after start invoked", state); clock.stop(); state = clock.isStart(); assertTrue("isStart should be false after stop invokedd", !state); } /** * stop/startの状態をテストする。 * 異常系として、 * 1. stop中状態でstopメソッド呼び出した時 * (インスタンス生成後にいきなりstopメソッドを呼び出す) * 2. start中状態でstartメソッド呼び出した時 * (インスタンス生成後にstartメソッドを呼び出し、続いて * startメソッドを呼び出す) */ public void testInvalidStartStop() throws Exception { try { Clock clock = new Clock(19, 20, 21); clock.stop(); fail("IllegalStateExceptionが発生するはず"); } catch (IllegalStateException e) { // Ok } Clock clock = new Clock(22, 23, 24); clock.start(); try { clock.start(); fail("IllegalStateExceptionが発生するはず"); } catch (IllegalStateException e) { // Ok } } |
時刻更新のテスト記述はちょっと難しい。タイミングが絡むためだ。とりあえず、以下の方法でテストすることにした。
判定の際に±1秒の誤差を許容するようにした。±1秒ではなく、もっとミリミリと誤差を詰め始めると、時刻プログラムに要求される性能、および試験プログラムの精度に関わってくる。性能および精度はかなりヘビーな内容なので、今後の課題とする。
/** * 時刻更新をテストする。 * <ul> * <li>Clockオブジェクトを時分秒指定で初期化(時刻9:54:00) * <li>intervalを1000ミリ秒に設定する * <li>startメソッドを呼んで開始させる * <li>10秒間スリープ * <li>stopメソッドを呼ぶ * <li>Clockオブジェクトの時分秒が時刻9:54:10±1であることを判定す * る */ public void test10secUpdate() throws Exception { Clock clock = new Clock(9, 54, 0); clock.setInterval(1000); clock.start(); Thread.sleep(10000); clock.stop(); int hour = clock.getHour(); int minute = clock.getMinute(); int second = clock.getSecond(); assertEquals(9, hour); assertEquals(54, minute); assertTrue( (9 <= second) && (second <= 11) ); } |
テストケースのコンパイルを実施すると、エラーが出る。Clockクラスにはまだstart/stop/isStartメソッドが追加されていないからだ。
E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java:128: シンボルを解釈処理 できません。 シンボル: メソッド isStart () 位置 : jp.gr.java_conf.torutk.jpe.clock.Clock の クラス boolean state = clock.isStart(); ^ src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java:131: シンボルを解釈処理 できません。 シンボル: メソッド start () 位置 : jp.gr.java_conf.torutk.jpe.clock.Clock の クラス clock.start(); ^
まずは、コンパイルが通る仮実装を行う。
/** * start中状態か否かを返却する。 * @return start中ならtrue、そうでないならfalse */ public boolean isStart() { return false; } /** * 時刻更新を開始する。 * @throws IllegalStateException オブジェクトがstop中状態にないと * きにstartメソッドが呼ばれた。 */ public void start() throws IllegalStateException { } /** * 時刻更新を停止する。 * @throws IllegalStateException オブジェクトがstart中状態にないと * きにstopメソッドが呼ばれた。 */ public void stop() throws IllegalStateException { } |
コンパイルエラーがなくなり、Clock.javaとClockTest.javaがコンパイルされる。
JUnitのTestRunnerを実行する。今回は趣向を変えて、コマンドライン版TestRunnerを実行してみた。下図のように、テストがエラーになればよい。コマンドライン版では、テストにかかった時間も表示される。この実行では10.014秒となっていた。10秒間スリープさせるテストコードがあるため、時間がかかっている。
E:\ClockProject>java -classpath .\classes;.\lib\junit.jar junit.textui.TestRunner jp.gr.java_conf.torutk.jpe.clock.ClockTest ....F.F.F Time: 10.014 There were 3 failures: 1) testValidStartStop(jp.gr.java_conf.torutk.jpe.clock.ClockTest)junit.framework .AssertionFailedError: isStart should be true after start invoked at jp.gr.java_conf.torutk.jpe.clock.ClockTest.testValidStartStop(ClockTe st.java:133) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl. java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces sorImpl.java:25) 2) testInvalidStartStop(jp.gr.java_conf.torutk.jpe.clock.ClockTest)junit.framewo rk.AssertionFailedError: IllegalStateExceptionが発生するはず at jp.gr.java_conf.torutk.jpe.clock.ClockTest.testInvalidStartStop(Clock Test.java:154) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl. java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces sorImpl.java:25) 3) test10secUpdate(jp.gr.java_conf.torutk.jpe.clock.ClockTest)junit.framework.As sertionFailedError at jp.gr.java_conf.torutk.jpe.clock.ClockTest.test10secUpdate(ClockTest. java:191) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl. java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces sorImpl.java:25) FAILURES!!! Tests run: 6, Failures: 3, Errors: 0 E:\ClockProject>
テストが無事(!)エラーとなった。3つのテストメソッドを追加し、そのテストメソッドに対応する実装が仮であるから、テストケースはそれなりに正しく動いていることが分かる(機能しないコードに対してテストコードが間違って成功することがない)。そこで、Clockクラスの仮実装だったstart/stopメソッドと時刻更新機能の実装に移る。
まずstart/stopメソッドと、状態遷移の値を実装する。
/** * start中状態か否かを返却する。 * @return start中ならtrue、そうでないならfalse */ public boolean isStart() { return isStart; } /** * 時刻更新を開始する。 * @throws IllegalStateException オブジェクトがstop中 * 状態にないときにstartメソッドが呼ばれた。 */ public void start() throws IllegalStateException { if (isStart) { throw new IllegalStateException( "start() invoked in start state" ); } isStart = true; // 時刻更新開始 startUpdate(); } /** * 時刻更新を停止する。 * @throws IllegalStateException オブジェクトがstart中 * 状態にないときにstopメソッドが呼ばれた。 */ public void stop() throws IllegalStateException { if (!isStart) { throw new IllegalStateException( "stop() invoked in stop state" ); } isStart = false; // 時刻更新停止 stopUpdate(); } /** * 時刻更新開始 */ private void startUpdate() { } /** * 時刻更新停止 */ private void stopUpdate() { } /** Clockオブジェクトの状態を保持する */ private boolean isStart; |
startメソッド/stopメソッドは、Clockクラスの利用者から呼ばれる公開メソッドだ。このメソッドの中にClockオブジェクトの状態値の変更と、時刻更新を実現するコードを両方書くと、一つのメソッドが複数の役割を担うことになる。そこで、時刻更新の実現を別なメソッドとして設けた。このメソッドはクラス外部からは呼ばれないので、非公開メソッドとした。
ここで、いったんコンパイルしてTestRunnerを実行してみてもよいだろう。
![]() |
では、残りの時刻更新を実現しよう。intervalミリ秒毎に時間を1秒ずつ進めるという処理を実現する方法をいくつか挙げてみる。
この選択は、コーディングの問題として片付けられてしまうことが多いけど、実は設計上の重要な決断である。時刻プログラムの性能に大きく影響を及ぼすためである。
時刻プログラムにおいて重要なのは、一定時間経過後に時刻が正確に更新されていることだ。例えば、12:00:00を初期値とし、intervalを1000ミリ秒で時刻更新を開始した場合、60分後には13:00:00となっていることが要求される。
まずはそれぞれの案について、机上検討を行う。
Threadクラスのsleepメソッドを使い、intervalミリ秒だけ間をとって時刻を更新する。コーディングイメージは下記のようになる。
while (isStart) { Thread.sleep(interval); 時刻を更新 }
のような実装となる。sleepメソッドでは、指定した時間が経過した「後」に次の処理が実行される。「後」がどれだけの時間になるかは不定である。また、時刻を更新する処理が終わり、次のループでsleepが呼び出されるまでもある程度の時間が経過している。つまり、このロジックでは、毎回の時刻更新に、(interval+ΔT)ミリ秒が経過する。仮にintervalを1000ミリ秒、ΔTを10ミリ秒だとすれば、Clockオブジェクトが24時間経過したときに、実時間では24時間14分24秒経過していることになる。これは1日に14分ずれる時計となってしまう。
そこで、Thread.sleepを使用するときは、ΔTを補正しなくてはならない。補正方法にはいろいろあるが、簡単に思いつくものとしては、前回と今回の実行時間がintervalとずれていたら、今回のsleep時間にずれを加える方法がある。コーディングイメージは下記のようになる。
while (isStart) { prev = current; current = System.currentTimeMillis(); adjust = interval + (prev - current); Thread.sleep(interval + adjust); 時刻を更新 }
ぱっと見てもコードが読みにくい。ループ初回でprevとcurrentが正しく設定されていない場合や、intervalの2倍以上間隔があいてしまった場合などを例外として扱うコードを書く必要がある。interval毎にシステムクロックを取りに行くのでオーバーヘッドがかかる。などの課題がある。これらの課題をクリアするコードを書くことは不可能ではないが、開発コストと得られる利益を照らし合わせて考えると現実的ではないだろう。
Java 2 Standard Edition, Ver. 1.3から、java.utilパッケージにTimerクラスが追加された。Ver.1.2から導入されたjavax.swing.Timerとは全く別のクラスである。schedule メソッドとscheduleAtFixedRate メソッドがある。前者は、前の処理と次の処理との間隔を指定した時間に保とうとするスケジューリングで、後者は一定時間の中で実行される回数を一定に保とうとするスケジューリングである。今回の時刻更新に適用するのはscheduleAtFixedRate メソッドの方だ。
java.util.Timerの使い方については、以下のURLに記載している。
Swingのクラスライブラリの1つとして提供されている。一定時間後にイベントを実行することができる。これは、Swingのイベントスレッドで実行されるので、描画に関わる処理を記述していてもSwingスレッドと干渉しないで済むのがメリット。実行はSwingのイベントキューに載せられるため、正確性には欠ける。
3つの方式から一つを選択する。いわば設計のオーディションだ。オーディションらしく、それぞれの候補を並べて点数付けをして、ベストを選ぼう。このときよく使われる手法は、比較表(星取り表)を作る方法だ。
ここで、性能といったプログラムを作ってみないと答えが得られない項目を評価する場合、検証プログラムを作ってみるとよい。今回は、過去に前述のURLにあるようにTimerについて調べていたため、机上検討のみで比較表を作成できたが、未経験のAPIやライブラリを使う場合、一度作ってみないと評価できないことが多いだろう。
設計は様々な候補から1つを選択することでもある。選択の過程を残すことは今後のイテレーションにおいて重要なことだ。なぜなら、今選んだ方式が駄目になったとき、いつでも代替候補を選択することができるからだ。
項目 | Thread.sleep | java.util.Timer | javax.swing.Timer | 備考 |
---|---|---|---|---|
周期実行 | - | ∨ | ∨ | |
処理遅延時の補正 | - | ∨ | - | |
開始・停止処理 | - | - | ∨ | |
ヘッドレス環境での動作 | ∨ | ∨ | - | |
総合評価 | 1 | 3 | 2 |
3案の比較検討の結果、java.util.Timerを採用とする。
Timerを使用する場合、周期的に実行する処理はTimerTaskを継承したクラスを定義してそのrunメソッドに記述することになる。また、runメソッド内の処理では時刻を更新するためClockクラスのフィールドを変更することになる。
これを素直に実装するとしたら、ClockクラスとTimerTaskを継承したClockUpdateTaskクラスが、互いに相手を参照として保持する形になるだろう。
![]() |
このように、2つのクラスが相互に相手を持ち合う設計は避けたい。この場合の定石としては、次の2つの方法があるだろう(他にも考えればいろいろ出てくるとは思うが)。
前者は、インナークラス化するクラスが他で利用されることがない場合に適する。後者は、依存先をinterfaceにすることによって他の局面で再利用されることを可能にする。今回はClockUpdateTaskを他で利用することはないと判断し、インナークラスとして実装することにした。
フィールドに、TimerとTimerTaskを追加。Timerはコンストラクタ中でインスタンス生成する。TimerTaskはstartメソッドが呼ばれる度にインスタンスを生成し、stopメソッドで破棄する。ClockUpdateTaskインナークラスは、タスクがスケジュールされたたびに(runメソッドが呼ばれる)、時分秒を更新する(1秒進める)。
public Clock(int anHour, int aMinute, int aSecond) { : timer = new Timer(); } : /** * 時刻更新開始 */ private void startUpdate() { task = new ClockUpdateTask(); timer.scheduleAtFixedRate(task, 0, getInterval()); } /** * 時刻更新停止 */ private void stopUpdate() { task.cancel(); task = null; } /** Clockオブジェクト更新タイマ */ private Timer timer; /** Clockオブジェクト更新タイマ・タスク */ private TimerTask task; /** * 時刻更新を行うタイマ・タスク・クラス。 */ class ClockUpdateTask extends TimerTask { public void run() { second++; if (second >= 60) { minute++; second = 0; } if (minute >= 60 ) { hour++; minute = 0; } if (hour >= 24) { hour = 0; } } } |
コンパイルが通ったら、いよいよテストケースの実行だ。
テストケースを実行してみると、
![]() |
なんとエラーが発生した。エラーはtestValidStartStopメソッドとtestInvalidStartStopメソッドで起きている。TimerクラスのメソッドscheduleAtFixedRateメソッドを呼んだ時点でIllegalArgumentExceptionが発生し、メッセージには、「非正の周期」とある。
テストケースが予期せぬ失敗をした。このような場合はまずソースを眺めるところから始めよう。あわててデバッガで追ってみると、近視眼的なデバッグをしがちだ。まず、例外が発生した個所は、ClockクラスのstartUpdateメソッドの中である。"period"は3番目の引数であることから、getIntervalメソッドの戻り値が0以下の値、すなわちintervalフィールドの値が0以下になっていることが分かる。
次に、失敗したテストケースのテストメソッドtestValidStartStopを見る。Clockをnewした後、startメソッドを呼んでいる。Clockがnewされたとき、intervalの値は0である。テストケースではintervalを設定しないままstartを呼んでいる。これが原因であった。
ここで、安直にテストケースにsetInterval(xx)と追加してテストを再実行してOkとしてしまうのはまずい。きっと同じようなあやまちが何度も発生する可能性があるからだ。ユニットテストのテストケースはクラスの開発者が記述するものだが、開発者自身がうっかり誤ってしまうようなら、クラスの利用者は必ず間違えると考えられる。であるから、この問題は根本から解決しなければならない。今回の原因を分析すると、
といった問題がある。
そこで、実装の修正を以下のように行うことにした。
1.は、インスタンスが生成されたときにintervalに有効な値が設定されるようにして、クラス不変条件を満たすため。
2.は、インスタンス生成周りのロジックが今後どんどん修正されていったときにクラス不変条件が破られる場合に備えるもの。startUpdateはprivateメソッドであるため、アサーションを使う。
テストファーストなので、ソースを変更するときはまずテストケースから変更する。今回Clockクラスのインタフェースが変更されるのは、1.のコンストラクタの変更によるものだ。そこで、コンストラクタの変更によるテストケースの修正を行う。
/** * <code>Clock</code>クラスのコンストラクタ(時分秒)をテストする。 * 正常系として、12時34分56秒を指定するコンストラクタをテストする。 */ public void testNewWithValidHMS() throws Exception { Clock clock = new Clock(12, 34, 56); assertEquals("コンストラクタで指定した時とgetHour()は一致するはず", 12, clock.getHour()); assertEquals("コンストラクタで指定した分とgetMinute()は一致するはず", 34, clock.getMinute()); assertEquals("コンストラクタで指定した秒とgetSecond()は一致するはず", 56, clock.getSecond()); assertEquals("デフォルトのintervalがgetInterval()と一致するはず", 1000, clock.getInterval()); } /** * <code>Clock</code>クラスのコンストラクタ(時分秒とインターバル) * をテストする。 * 正常系として、18時3分10秒とインターバル2000を指定するコンストラ * クタをテストする。 */ public void testNewWithValidHMSI() throws Exception { Clock clock = new Clock(18, 3, 10, 2000); assertEquals("コンストラクタで指定した時とgetHour()は一致するはず", 18, clock.getHour()); assertEquals("コンストラクタで指定した分とgetMinute()は一致するはず", 3, clock.getMinute()); assertEquals("コンストラクタで指定した秒とgetSecond()は一致するはず", 10, clock.getSecond()); assertEquals("コンストラクタで指定したのintervalとgetInterval()は一致するはず", 2000, clock.getInterval()); } |
追加ついでに、テストケースを修正もした。テストケースでは見やすさが重要なので、Getterで取得した値をいったんローカル変数で受けてからassertEqualsでチェックしていたコードをローカル変数では受けずにassertEqualsの引数に直接Getterを書くように変えた。
ここで、テストケースをコンパイルする。まだ、引数にintervalを追加したコンストラクタが存在しないので、コンパイルエラーとなることを確認する。
E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java:54: シンボルを解釈処理できま せん。 シンボル: コンストラクタ Clock (int,int,int,int) 位置 : jp.gr.java_conf.torutk.jpe.clock.Clock の クラス Clock clock = new Clock(18, 3, 10, 2000); ^ エラー 1 個
コンパイルエラーが出たら、Clockクラスにコンストラクタを追加しよう。引数4つのコンストラクタを追加する。既存の引数3つのコンストラクタは、引数4つのコンストラクタを呼ぶだけに置き換えている。
/** * <code>Clock</code>オブジェクトを生成する。 * <p> * 引数で指定した時分秒に初期化される。 * @param anHour 時:0-23 * @param aMinute 分:0-59 * @param aSecond 秒:0-59 * @throws IllegalArgumentException 引数が有効範囲にないとき */ public Clock(int anHour, int aMinute, int aSecond) { this(anHour, aMinute, aSecond, DEFAULT_INTERVAL); } /** * <code>Clock</code>オブジェクトを生成する。 * <p> * 引数で指定した時分秒およびインターバルに初期化される。 * @param anHour 時:0-23 * @param aMinute 分:0-59 * @param aSecond 秒:0-59 * @param anInterval インターバル[ミリ秒]: 正の整数(1-Integer.MAX_VALUE) * @throws IllegalArgumentException 引数が有効範囲にないとき */ public Clock(int anHour, int aMinute, int aSecond, int anInterval) { if (anHour < 0 || 23 < anHour) { throw new IllegalArgumentException("out of range: hour"); } if (aMinute < 0 || 59 < aMinute ) { throw new IllegalArgumentException("out of range: minute"); } if (aSecond < 0 || 59 < aSecond) { throw new IllegalArgumentException("out of range: second"); } if (anInterval <= 0) { throw new IllegalArgumentException("out of range: interval"); } hour = anHour; minute = aMinute; second = aSecond; interval = anInterval; timer = new Timer(); } : /** インターバル[ミリ秒]が指定されない場合に適用するデフォルト値 */ private static int DEFAULT_INTERVAL = 1000; |
再度テストケースをコンパイルする。今度はコンパイルが正常に終了するはずだ。
アサーションは、Java 2 Standard Edition, Ver.1.4から導入された言語仕様なので、Ver.1.3以下のJavaではコンパイル・実行できない。
/** * 時刻更新開始 */ private void startUpdate() { task = new ClockUpdateTask(); int interval = getInterval(); assert 0 < interval : "non-positive interval = " + interval; timer.scheduleAtFixedRate(task, 0, getInterval()); } |
アサーションを使用するときは、コンパイル時および実行時にオプションを指定しなくてはならない。下記URLの中に、ソースコードで使用しているAPI:アサーションについて書いている。
コンパイルをしてみよう。
E:\ClockProject>javac -source 1.4 -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java
実行時に、アサーションを有効にするオプションを指定しなければ、アサーションの式は評価されない。今回は、JUnitのTestRunnerを起動しているコマンドラインのオプションに指定する。
E:\ClockProject>java -ea -classpath .\classes;.\lib\junit.jar junit.swingui.TestRunner
テストケースを実行する。
![]() |
ようやく、テストファーストによる実装が完了した。
イテレーションNo.1で作成したClockClientに修正を加える。
public static void main(String[] args) { testDefaultConstractor(); testHmsConstractor(); testSetGet(); testClockTick(); testClockFor30sec(); } : : static void testClockTick() { System.out.println("=== testClockTick ==="); Clock clock = new Clock(9, 59, 30); clock.start(); for (int i=0; i<31; i++) { System.out.println(clock.toString()); try { Thread.sleep(1000); } catch (InterruptedException e) { // do nothing } } clock.stop(); } static void testClockFor30sec() { System.out.println("=== testClockFor30sec ==="); Clock clock = new Clock(); clock.setInterval(100); clock.start(); System.out.println(clock.toString()); try { Thread.sleep(30000); } catch (InterruptedException e) { // do nothing } System.out.println(clock.toString()); clock.stop(); } |
ClockClientをコンパイルするときも、-source 1.4 オプションを追加する。
E:\ClockProject>javac -source 1.4 -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java
今までは個別にソースファイルをコンパイルしていたが、まとめて一度にコンパイルできると便利である。javacコマンドには、コンパイル対象ソースファイルを列挙したテキストファイルを指定すると、そこに列挙されたソースファイルを全部コンパイルするオプションがある。
-source 1.4 -d .\classes -classpath .\lib\junit.jar -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\Clock.java src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java src\jp\gr\java_conf\torutk\jpe\client\ClockClient.java |
このように、コンパイル対象ソースファイルと、コンパイルオプションを記述したファイル(compile.list)を用意する。
コンパイルの実行は、javacのコマンドラインオプションに@を付けてファイル名を指定する。
E:\ClockProject>javac @compile.list
<Project Root>直下に置くのはあまりきれいではないので、サブディレクトリmakeを作成し、その中にcompile.listファイルを置く。
<Project Root> | +---- classes | +---- doc | +---- lib | +---- make | +---- compile.list | +---- src | +---- README.txt |
このとき、コンパイルコマンドは次のようになる。
E:\ClockProject>javac @make\compile.list
テストファーストプログラミングを行ったので、開発者自身が実施するユニットテストは実装と同時に完了していることになる。今回はまだクラスが1つしかないので、統合テスト以降は今後とする。
実はイテレーションNo.1におけるコードレビューで指摘された事項の反映をまだ実施していない。そこで、前回指摘事項と今回新たに検出した指摘事項を合わせてリストアップする。また、前回の指摘事項は、指摘個所が曖昧であったので、具体的に記述することにした。
XP(eXtreme Programming)では、ペアプログラミングがプラクティスとして取り入れられているから、コードレビューは不要という意見もある。しかし、ペアが必ずしも問題点を指摘できるとは限らない。人は、自分が見えるものしか見えないのである。
public Clock(int anHour, int aMinute, int aSecond, int anInterval) { : setHour(anHour); setMinute(aMinute); setSecond(aSecond); setInterval(anInterval); initTimer(); }
public void start() throws IllegalStateException { if (isStart()) { throw new IllegalStateException( "start() invoked in start state" ); } setStart(true); // 時刻更新開始 startUpdate(); } : : private void setStart(boolean start) { isStart = start; }
public void stop() throws IllegalStateException { if (!isStart()) { throw new IllegalStateException( "stop() invoked in stop state" ); } setStart(false); // 時刻更新停止 stopUpdate(); }
public Clock() { this(DEFAULT_HOUR, DEFAULT_MINUTE, DEFAULT_SECOND); } : : private static final int DEFAULT_HOUR = 0; private static final int DEFAULT_MINUTE = 0; private static final int DEFAULT_SECOND = 0;
currentSecond++; if (MAX_SECOND < currentSecond) { currentMinute++; currentSecond = 0; if (MAX_MINUTE < currentMinute) { currentHour++; currentMinute = 0; if (MAX_HOUR < currentHour) { currentHour = 0; } } }
private boolean checkHourCondition(int anHour) { return (MIN_HOUR <= anHour && anHour <= MAX_HOUR); } private boolean checkMinuteCondition(int aMinute) { return (MIN_MINUTE <= aMinute && aMinute <= MAX_MINUTE); } private boolean checkSecondCondition(int aSecond) { return (MIN_SECOND <= aSecond && aSecond <= MAX_SECOND); } private boolean checkIntervalCondition(int anInterval) { return (MIN_INTERVAL <= anInterval); } : : private static final int MIN_HOUR = 0; private static final int MAX_HOUR = 23; private static final int MIN_MINUTE = 0; private static final int MAX_MINUTE = 59; private static final int MIN_SECOND = 0; private static final int MAX_SECOND = 59; private static final int MIN_INTERVAL = 1;コンストラクタClock(int, int, int, int)の中で引数の有効範囲判定
public Clock(int anHour, int aMinute, int aSecond, int anInterval) { if (!checkHourCondition(anHour)) { throw new IllegalArgumentException("out of range - hour"); } if (!checkMinuteCondition(aMinute)) { throw new IllegalArgumentException("out of range - minute"); } if (!checkSecondCondition(aSecond)) { throw new IllegalArgumentException("out of range - second"); } if (!checkIntervalCondition(anInterval)) { throw new IllegalArgumentException("out of range - interval"); } : :アクセッサ(Setter)の中で引数の有効範囲判定
public void setHour(final int anHour) { if (!checkHourCondition(anHour)) { throw new IllegalArgumentException("out of range - hour"); } hour = anHour; } public void setMinute(final int aMinute) { if (!checkMinuteCondition(aMinute)) { throw new IllegalArgumentException("out of range - minute"); } minute = aMinute; } public void setSecond(final int aSecond) { if (!checkSecondCondition(aSecond)) { throw new IllegalArgumentException("out of range - second"); } second = aSecond; } public void setInterval(int anInterval) { if (!checkIntervalCondition(anInterval)) { throw new IllegalArgumentException("out of range - interval"); } interval = anInterval; }
private synchronized void setTime(int anHour, int aMinute, int aSecond) { assert MIN_HOUR <= anHour && anHour <= MAX_HOUR; assert MIN_MINUTE <= aMinute && aMinute <= MAX_MINUTE; assert MIN_SECOND <= aSecond && aSecond <= MAX_SECOND; hour = anHour; minute = aMinute; second = aSecond; } : : class ClockUpdateTask extends TimerTask { public void run() { int currentHour = getHour(); int currentMinute = getMinute(); int currentSecond = getSecond(); currentSecond++; if (MAX_SECOND < currentSecond) { currentMinute++; currentSecond = 0; if (MAX_MINUTE < currentMinute) { currentHour++; currentMinute = 0; if (MAX_HOUR < currentHour) { currentHour = 0; } } } setTime(currentHour, currentMinute, currentSecond); } }
* 例<pre> * Clock clock = new Clock(h, m, s, i); * clock.start(); * : * synchronized(clock) { * hour = getHour(); * minute = getMinute(); * second = getSecond(); * } * </pre>
private void startUpdate() { task = new ClockUpdateTask(); int currentInterval = getInterval(); assert 0 < currentInterval : "non-positive interval = " + currentInterval; timer.scheduleAtFixedRate(task, 0, currentInterval); }
public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(getClass().getName()); buffer.append("[hour="); buffer.append(hour); buffer.append(",minute="); buffer.append(minute); buffer.append(",second="); buffer.append(second); buffer.append(",interval="); buffer.append(interval); buffer.append(",isStart="); buffer.append(isStart); buffer.append(']'); return buffer.toString(); }
レビューによって、クラス設計が変わったので、修正する。
Clock |
---|
-hour: int -minute: int -second: int -interval: int -isStart: boolean -timer: Timer -task: TimerTask -MIN_HOUR: int -MAX_HOUR: int -MIN_MINUTE: int -MAX_MINUTE: int -MIN_SECOND: int -MAX_SECOND: int -MIN_INTERVAL: int -DEFAULT_HOUR: int -DEFAULT_MINUTE: int -DEFAULT_SECOND: int -DEFAULT_INTERVAL: int |
+getHour(): int +setHour(hour:int):void +getMinute(): int +setMinute(minute:int):void +getSecond(): int +setSecond(second: int):void] +getInterval(): int +setInterval(interval: int):void +start():void +stop():void +isStart(): boolean +toString(): String -setStart(start:boolean): void -setTime(hour:int,minute:int,second:int): void -initTimer(): void -startUpdate(): void -stopUpdate(): void -checkHourCondition(hour:int): boolean -checkMinuteCondition(minute:int): boolean -checkSecondCondition(second:int): boolean -checkIntervalCondition(interval:int): boolean |
それぞれの修正を行ってはテストケースを実行して確認する、という作業を繰り返してレビュー対処によるコード修正が進んでいく。もし、ユニットテストでClockTest.javaを記述していなかったら、修正結果の確認が難しく、修正によるバグが潜り込んでいたろう。
まずイテレーションNo.1で作成したREADMEファイルをイテレーションNo.2に合わせて修正を行う。
続いてAPIドキュメントをイテレーションNo.2のソースコードから生成し直す。
イテレーションNo.1の修正版。コンパイル・実行オプション、および実行結果をイテレーションNo.2に合わせた。
このイテレーションでは、jp.gr.java_conf.torutk.jpe.clockパッケージのパッケージドキュメントを加える。パッケージドキュメントは、package.htmlというファイル名を付けて、パッケージのソースディレクトリに配置する。今回の場合、以下のディレクトリに配置すればよい。
<Project Root> : +---src : +---jp +---gr +---java_conf +---torutk +---jpe +---clock | +---package.html : |
package.htmlは、HTMLファイルであり、<body>タグの中にあるコンテンツがjavadocによってAPIドキュメントに反映される。ごく簡単な説明を以下のように用意した。
<html> <head> <title>clockパッケージの概要</title> </head> <body> 時分秒からなる時刻を保持し、一定のレートで時刻を更新する時刻プログラム のAPIを提供します。 時刻プログラムのAPIは、Clockクラスが提供しており、時分秒の設定と参照、 更新レートの設定、時刻の更新開始・停止といった操作が行えます。 内部では、java.util.Timerクラスを使って実現しています。 <h2>パッケージ仕様</h2> このパッケージは、Java 2 Standard Edition, v1.4で導入されたアサーショ ン機構を使用しています。 <address> <a href="mailto:torutk@alles.or.jp">Toru TAKAHASHI</a> </address> </body> </html>
javadocコマンドを使ってドキュメントを生成する。APIドキュメントを生成するパッケージを複数指定するときは、下記のように、スペースで区切ってパッケージ名を列挙する。
E:\ClockProject>javadoc -source 1.4 -d doc -classpath lib\junit.jar -sourcepath src -author -version jp.gr.java_conf.torutk.jpe.clock jp.gr.java_conf.torutk.jpe.client パッケージ jp.gr.java_conf.torutk.jpe.clock のソースファイルを読み込んで います... パッケージ jp.gr.java_conf.torutk.jpe.client のソースファイルを読み込んで います... Javadoc 情報を構築しています... 標準 Doclet バージョン 1.4.0 : :
JARファイルを作成し、実行する方法を使用する。JARにはzip圧縮が付いているので、他の圧縮ツールを使わなくてもよい。
時刻プログラムを実行するには、<Project Root>ディレクトリのclassesディレクトリの中にあるクラスファイルが必要となる。そこで、classesディレクトリの中をJARファイルにまとめる。
E:\ClockProject>jar cvf jpe-clock.jar -C classes . マニフェストが追加されました。 jp/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/jpe/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/jpe/client/ を追加中です。(入 = 0) (出 = 0)(0% 格納されま した) jp/gr/java_conf/torutk/jpe/client/ClockClient.class を追加中です。(入 = 1533) ( 出 = 883)(42% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/ を追加中です。(入 = 0) (出 = 0)(0% 格納されま した) jp/gr/java_conf/torutk/jpe/clock/Clock$ClockUpdateTask.class を追加中です。(入 = 774) (出 = 471)(39% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/Clock.class を追加中です。(入 = 4918) (出 = 235 0)(52% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/ClockTest.class を追加中です。(入 = 3415) (出 = 1742)(48% 収縮されました) E:\ClockProject>
今回作成したJARファイルから実行するには、次のようにコマンドを実行する。
E:\ClockProject>java -cp jpe-clock.jar jp.gr.java_conf.torutk.jpe.client.ClockClient :
JARファイルの中のマニフェストファイルに、アプリケーションを実行するmainメソッドを持つクラスを指定する。なお、JARコマンド実行時に、マニフェスト指定をしないとデフォルトのマニフェストファイルが生成される。
Main-Class: jp.gr.java_conf.torutk.jpe.client.ClockClient |
まず、マニフェストファイルに指定する情報を記述したテキストファイル(clockclient.mf)を作成する。ファイル記述上の注意点は、':'の後ろにはスペースを入れること、行の最後にかならず改行を置くことである。
マニフェストファイル(clockclient.mf)を指定してjarコマンドを実行する。
E:\ClockProject>jar cvfm jpe-clock.jar clockclient.mf -C classes . マニフェストが追加されました。 jp/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/jpe/ を追加中です。(入 = 0) (出 = 0)(0% 格納されました) jp/gr/java_conf/torutk/jpe/client/ を追加中です。(入 = 0) (出 = 0)(0% 格納されま した) jp/gr/java_conf/torutk/jpe/client/ClockClient.class を追加中です。(入 = 1533) ( 出 = 883)(42% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/ を追加中です。(入 = 0) (出 = 0)(0% 格納されま した) jp/gr/java_conf/torutk/jpe/clock/Clock$ClockUpdateTask.class を追加中です。(入 = 774) (出 = 471)(39% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/Clock.class を追加中です。(入 = 4918) (出 = 235 0)(52% 収縮されました) jp/gr/java_conf/torutk/jpe/clock/ClockTest.class を追加中です。(入 = 3415) (出 = 1742)(48% 収縮されました) E:\ClockProject>
作成した実行可能JARファイルから実行するには、次のようにコマンドを実行する。
E:\ClockProject>java -jar jpe-clock.jar :
また、Windowsマシンであれば、エクスプローラ上でダブルクリックしても実行されるが、この場合コンソールが伴わない(javawコマンドに結びついてる)ため、実行の様子は分からない。
そろそろソースコードも変更が増えてきたし、Clockクラスも肥大化してきつつあるのでクラスを分割していくことになりそうだ。そうなってくると、バージョン管理が欲しくなってくる。
だが今回は時間も尽きてきたので、前回同様<Project Root>ディレクトリ以下をごっそり取っておくことにする。
E:\ClockProject>cd .. E:\>ren ClockProject ClockProject-20020616 E:\>
イテレーションNo.2で実施したことは以下のとおり。