[ Java Project Exampleページへ戻る ]

イテレーション No.2

時刻プログラム開発のイテレーションNo.2である。


コンテンツ


目標を定める

いよいよ2回目のイテレーションに入る。まずは、この2回目のイテレーションで開発する機能や確立するインフラといった目標を定める。

目標の候補を抽出

まずは、構想で挙げた時刻プログラムの機能から未開発のものを列挙する。次に、イテレーションNo.1の過程で発生した課題を列挙する。これらの中から、イテレーションNo.2の開発目標を選定する。

時刻プログラムの機能

イテレーションNo.1の課題

目標の決定

イテレーションNo.1の課題がかなり多い。この課題を避けたまま開発を続けると破綻が待ち受けているであろうから、時刻プログラムの機能は最低限の追加とし、課題の解決を図っていくことにする。

時刻プログラムの機能

最低限の機能としてやはり時を刻む機能は欠かせない。そこで、イテレーションNo.2ではこの機能を開発する。

イテレーションNo.1の課題

非常に多くの課題があり、すべてを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
クラス図 Ver.2.1

intervalは、ミリ秒を単位とした整数値とする。ミリ秒としたのは、大抵のOSが持つ割り込みが1ミリ秒単位だからで、これ以上の分解能は不要と考えたから。

クラスを分割すべきか否か

まずは1つのクラス(Clock)に、時刻更新機能を追加する設計を行った。ここで、intervalごとに時刻を更新する機能を別なクラスに設け、2つのクラスが協調して時刻更新を行うという設計も考えられる。どちらがよいのだろうか?

それぞれのクラスが持つ役割(責務)の点から検討すると、時刻を保持する役割と時刻の更新を行うための間隔を計る役割とに分けてもよいかと思う。また、間隔を計る役割を実装する方式も複数想定される(Thread、Timer、他のClockなど)ため、特定の実装をClock内部に持つのは得策ではないと思う。

一方、XP(eXtreme Programming)におけるYAGNIの教え(You're NOT gonna need it!)によれば、「いずれ必要になる」と言って実装してはいけない。今回の場合、今後実装方式が変わるかもしれないからクラスを2つに分ける、というのは戒めるべきことになる。

そろそろ結論を出さねば先に進めない。では、1つのクラスで行くことにしよう。YAGNIの教えに盲目的に従うのは愚かなことだが、まだ時刻更新の設計に問題があるかどうかは分かっていない(設計に問題があることが判明するのは大抵実装中から試験にかけてである)。この時点で方式を切り替え可能な時刻更新機能を追加することは、設計に問題があった場合に(多分あるだろう)、後で大きなやり直し作業をする羽目になる。

シーケンス図

今回はタイミング要素が入ってきたため、きちんとシーケンスを設計することが望ましい。そこで、シーケンス図を書いてみた。

シーケンス図 Ver.2.1
シーケンス図Ver.2.1

使い方は、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中状態では、時刻更新を行う。このことを、状態遷移図に書き表した。言葉で説明するより図の方が、明確に意図が伝わる(気がする)。

状態遷移図Ver.2.1
状態遷移図Ver.2.1

オブジェクトの状態を問合せるメソッドを追加する。また、状態を保持するフィールドを追加する。

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
クラス図 Ver.2.2

状態を、どのように表現するかは設計の重要なポイントである。すぐに思いつくのが、状態を数値化し、現在の状態を示すフィールドを1つ定義する方法だ。次に考えられるのは、デザインパターンの1つであるステートパターンを適用する方法だ。ステートパターンを使った方が今後の拡張性が増すかもしれない。しかし、クラスを分割するか否かで検討したようにClockクラスに時刻更新機能を持たせているのも暫定的なもので、未来永劫設計を変えない訳ではない。そこで、現在のClockクラスに過度の拡張性を持たせても仕方がないため、状態をフィールドで保持する方法でいくことにした。

状態を持たせるフィールドの型だが、状態設計では2つの状態しかないので、booleanとする案と、3つ以上に増えることを考慮しintとする案がある。今回は、booleanで実装していくことにした。ただし、今後のイテレーションで状態が増えれば変更することになる。


実装

さて、実装だ。今回のイテレーションでは、実装にあたってテストファーストによるプログラミングと、コンパイル方法の改善を行う。

テストファーストの導入

テストファーストとは、先にテスト・コードを実装してから本来のコードを実装するやり方。オブジェクト指向プログラミングを行う場合、クラス単位にコードを実装するので、テスト・コードはクラス毎に作成するユニットテスト(単体試験)に相当する。よい設計とは、テストしやすい設計でもあるので、これは非常に理にかなっている。次のURLにいいことが書かれているので参考にする。

テスト・コードの作成だが、開発するクラス毎に一から作成すると非常に面倒だ。「単体(=クラス)ごとにテストをすると時間がかかるし大変なので、テストはしない」などという言語道断な言い訳がまかり通るのも一理はある(本当は一理も認められないけど・・・)。テスト(特にXPのテスティング)の有効性について、以前まとめたものがあるので次のURLに載せておく。

テスト作業を一から行うと大変であるため、何らかのテストツールを使用することが常套手段となる。市販のテストツールは各メソッドのテストを自動的に生成して実行するような高度なものもある。残念ながらこの時刻プログラム開発プロジェクトには資金がほとんど与えられてないため、市販のテストツール採用は適わない。代わりといっては語弊があるが、ユニットテストの定番とも言えるオープンソースのフレームワークであるJUnitを導入することにした。JUnitは、テストケースと呼ぶテストコード作成を簡易化するほか、テストケース実行と結果判定を自動化するため、テスト作業が楽になる。JUnitを使ってテストコードを具体的に書く方法については次のURLに詳しく書かれているので参考にする。

JUnitのインストール

JUnitのインストール方法と簡単な使い方については次のURLに記載している。

本プロジェクトでは、上記インストール方法について一部カスタマイズを行った。

  1. JUnitクラスライブラリファイルは、プロジェクトディレクトリ内に配置する

JUnitもバージョンアップされるため、数年後にこのプロジェクトを保守することになった場合に開発で使用していたバージョンを入手できない可能性がある。そこで、プロジェクトが依存するクラスライブラリについてはプロジェクトと同様バージョン管理下に置くことにする。そこで、プロジェクトディレクトリにlibディレクトリを追加し、ここにJUnitのクラスライブラリファイルを配置した。
また、プロジェクトディレクトリ内に配置したことにより、開発マシンが異なっても必要な環境設定が同じで済むというメリットも生じる。

プロジェクトディレクトリ構造
<Project Root>
      |
      +---- classes
      |
      +---- doc
      |
      +---- lib
      |      +---- junit.jar
      |
      +---- src
      |
      +---- README.txt

テストファーストによるClockクラスの実装

では、早速テストケースの作成に入る。まず、イテレーションNo.1で作成した時分秒を保持する機能をテストするコードを導入の意味も含めて作成する。次に、イテレーションNo.2で開発する機能である時刻更新機能をテストするコードを記述する。

  1. 時分秒を保持する機能
  2. 属性intervalと、そのアクセッサメソッドの追加
  3. オブジェクトの状態設計(start中、stop中)と状態を遷移させるイベントを発生させるメソッドの追加

これを順にテストするコードをテストケースに追加していく。

テストケース(ClockTest.java)の配置と記述項目

ではまず、ClockクラスのテストケースであるClockTest.javaを記述する。ファイルは、Clockクラスのソースと同じパッケージ・同じディレクトリに配置する。(JUnit実践講座を参考にした)

<Project Root>
    :
    +---src
    :   +---jp
            +---gr
                +---java_conf
                        +---torutk
                               +---jpe
                                   +---clock
                                   |     +---Clock.java
                                   |     +---ClockTest.java
                                   :

JUnitを利用する場合にテストケースへ記述する際の規約を、ソースファイルに記述する項目に従って挙げる。

  1. ファイル先頭コメント
    通常のソースファイルと同様。
  2. パッケージ文
    テストケースがテストする対象のクラスと同一パッケージで宣言する
  3. import文
    JUnitフレームワークの中の利用するクラスを記述する
  4. クラスコメント
    通常のソースファイルと同様。何をテストするかを説明するとよい。
  5. クラス宣言
    命名は、テスト対象クラス名にTestを付加する。TestCaseクラスをextendsする。
  6. フィールドの定義
    テストメソッド間で共通で使うものがあれば定義する
  7. インスタンス生成メソッド(コンストラクタ)の定義
    必ずStringを引数にとるコンストラクタを定義する。中ではスーパークラスのコンストラクタを呼ぶ。ここにはテストの初期化処理を書いてはいけない。
  8. サービスメソッドの定義
    これは、テストを実施するメソッドを定義していく。その際、メソッド名は必ずtestで始める。

時分秒指定コンストラクタのテストケースを記述

まず、イテレーションNo.1で作成したClockクラスのコンストラクタ(時分秒を指定するもの)をテストするコードを記述する。

ClockTest.java
/*
 * 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をテストするものを作るべきだが、今回は省略。本来の機能の開発に関わる部分の作成に早く入りたい。

時分秒指定コンストラクタのテストケースをコンパイル

テストケースのコンパイルを行う時は、JUnitクラスライブラリファイルであるjunit.jarをクラスパスに指定する必要がある。そこで、-classpathオプションを使用して指定する。junit.jarはプロジェクトディレクトリ内に置いているため、相対パスで指定している。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java
E:\ClockProject>
-classpathオプション
コンパイル時に、依存するクラスのバイトコードを検索する場所を指定する。バイトコードが置かれているディレクトリ階層の基点パスか、JAR形式にアーカイブされたクラスライブラリファイルを指定できる。

時分秒指定コンストラクタのテストケースを実行

コンパイルが成功すれば、テストを実行します。

E:\ClockProject>java -classpath .\classes;.\lib\junit.jar
 junit.swingui.TestRunner

JUnitのテストケース実行画面(Swing版)が表示されるので、Test classに先ほど作ったClockTestを選ぶ。最初は、[...]を押して、テストケース選択ダイアログからClockTestを選ぶ。この選択ダイアログには、クラスパス上に存在するテストケースのクラスが候補として表示される。

テストの実行は、[Run]を押す。

JUnit SwingUI TestRunner

属性intervalとそのアクセッサのテストケースを記述

属性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
        }
    }

属性intervalとそのアクセッサのテストケースをコンパイル

ここで、テストケースのコンパイルを実施する。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());

属性intervalとそのアクセッサをClockクラスに仮実装

コンパイルエラーを発生させてから、Clockクラスの追加記述を行う。まずは、コンパイルが通る最低限度の記述、すなわち追加するAPI(インタフェース)を記述する。いきなり全ての実装を書かない理由は、先に作成したテストケースが、間違っているにもかかわらず正しいと判断してしまうバグを避けるためである。つまり、まず本来の機能が実装されていないClockクラスをテストさせて、テスト結果が否(fail)となることを最初に確認する。

Clock.javaにintervalとそのアクセッサメソッドの仮実装を追記
    /**
     * インターバルの値を返却する。
     * @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をコンパイルする時に依存関係からコンパイルし直される)

属性intervalとそのアクセッサのテストケースを実行(1)

再び、JUnitのTestRunnerを実行する。下図のように、テストがエラーになればよい。

テストが無事(!)エラーとなったことを確認した。

属性intervalとそのアクセッサメソッドの実装

Clockクラスの仮実装だったintervalフィールドとそのアクセッサメソッドをちゃんと実装する。

Clock.javaに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とそのアクセッサのテストケースを実行(2)

いよいよ最初の機能である属性intervalと、そのアクセッサメソッドの追加をテストする。JUnitのTestRunnerの[Run]ボタンを押す。(TestRunnerはテスト実行の度にクラスファイルをロードし直すことができるので、TestRunner自体を再起動しなくてもよいのが楽)

テストがパスすれば、属性intervalと、そのアクセッサメソッドの追加が完成だ。いよいよ次の本題である時刻を更新する機能の実装に入る。

状態遷移のテストケースを記述(1)

オブジェクトの状態と状態遷移メソッド(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) );
    }

状態遷移のテストケースをコンパイル(1)

テストケースのコンパイルを実施すると、エラーが出る。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();
             ^

状態遷移をClockクラスに仮実装

まずは、コンパイルが通る仮実装を行う。

Clock.javaに状態遷移関係のメソッドの仮実装を追記
    /**
     * 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がコンパイルされる。

状態遷移のテストケースを実行(1)

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メソッドと時刻更新機能の実装に移る。

状態遷移をClockクラスに実装

まずstart/stopメソッドと、状態遷移の値を実装する。

Clock.javaに状態遷移のメソッドの実装を追記
    /**
     * 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による方法

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.util.Timerによる方法

Java 2 Standard Edition, Ver. 1.3から、java.utilパッケージにTimerクラスが追加された。Ver.1.2から導入されたjavax.swing.Timerとは全く別のクラスである。schedule メソッドとscheduleAtFixedRate メソッドがある。前者は、前の処理と次の処理との間隔を指定した時間に保とうとするスケジューリングで、後者は一定時間の中で実行される回数を一定に保とうとするスケジューリングである。今回の時刻更新に適用するのはscheduleAtFixedRate メソッドの方だ。

java.util.Timerの使い方については、以下のURLに記載している。

javax.swing.Timerによる方法

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を採用とする。

java.util.Timerを使用した実装

Timerを使用する場合、周期的に実行する処理はTimerTaskを継承したクラスを定義してそのrunメソッドに記述することになる。また、runメソッド内の処理では時刻を更新するためClockクラスのフィールドを変更することになる。

これを素直に実装するとしたら、ClockクラスとTimerTaskを継承したClockUpdateTaskクラスが、互いに相手を参照として保持する形になるだろう。

clockパッケージのクラス図 Ver.2.3

このように、2つのクラスが相互に相手を持ち合う設計は避けたい。この場合の定石としては、次の2つの方法があるだろう(他にも考えればいろいろ出てくるとは思うが)。

前者は、インナークラス化するクラスが他で利用されることがない場合に適する。後者は、依存先をinterfaceにすることによって他の局面で再利用されることを可能にする。今回はClockUpdateTaskを他で利用することはないと判断し、インナークラスとして実装することにした。

フィールドに、TimerとTimerTaskを追加。Timerはコンストラクタ中でインスタンス生成する。TimerTaskはstartメソッドが呼ばれる度にインスタンスを生成し、stopメソッドで破棄する。ClockUpdateTaskインナークラスは、タスクがスケジュールされたたびに(runメソッドが呼ばれる)、時分秒を更新する(1秒進める)。

Clock.javaに時刻更新機能の実装を追記
    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;
            }
        }
    }

コンパイルが通ったら、いよいよテストケースの実行だ。

状態遷移のテストケースを実行(2)

テストケースを実行してみると、

なんとエラーが発生した。エラーは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を引数にとるコンストラクタを追加。intervalを指定しないコンストラクタが呼ばれた場合、デフォルト値を適用する。
  2. startUpdateは、0 < interval をアサーションする。

1.は、インスタンスが生成されたときにintervalに有効な値が設定されるようにして、クラス不変条件を満たすため。
2.は、インスタンス生成周りのロジックが今後どんどん修正されていったときにクラス不変条件が破られる場合に備えるもの。startUpdateはprivateメソッドであるため、アサーションを使う。

状態遷移のテストケースを記述(2)

テストファーストなので、ソースを変更するときはまずテストケースから変更する。今回Clockクラスのインタフェースが変更されるのは、1.のコンストラクタの変更によるものだ。そこで、コンストラクタの変更によるテストケースの修正を行う。

ClockTest.javaに修正を追記
    /**
     * <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を書くように変えた。

状態遷移のテストケースをコンパイル(2)

ここで、テストケースをコンパイルする。まだ、引数に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つのコンストラクタを呼ぶだけに置き換えている。

Clock.javaに修正を追記
    /**
     * <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;

再度テストケースをコンパイルする。今度はコンパイルが正常に終了するはずだ。

状態遷移の実装(startUpdate)に、アサーションを追加

アサーションは、Java 2 Standard Edition, Ver.1.4から導入された言語仕様なので、Ver.1.3以下のJavaではコンパイル・実行できない。

Clock.javaのstartUpdateメソッドにアサーションを追記
    /**
     * 時刻更新開始
     */
    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

状態遷移のテストケースを実行(3)

テストケースを実行する。

ようやく、テストファーストによる実装が完了した。

ClockClientの修正

イテレーションNo.1で作成したClockClientに修正を加える。

  1. Clockオブジェクトを生成し、時刻更新をstartさせた後、1秒ごとにClockオブジェクトの内容を表示するメソッドを作成する。
  2. Clockオブジェクトを生成し、インターバル100ミリ秒で時刻更新をstartさせた後、30秒間待ってClockオブジェクトの内容を表示するメソッドを作成する。
ClockClient.javaの追記
    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コマンドには、コンパイル対象ソースファイルを列挙したテキストファイルを指定すると、そこに列挙されたソースファイルを全部コンパイルするオプションがある。

compile.list
-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)では、ペアプログラミングがプラクティスとして取り入れられているから、コードレビューは不要という意見もある。しかし、ペアが必ずしも問題点を指摘できるとは限らない。人は、自分が見えるものしか見えないのである。

レビュー指摘事項

  1. アクセッサ以外の場所でフィールドを直接変更している
    1. Clock(int, int, int, int)の中で、hour/minute/second/timerの各フィールドを直接更新している
    2. start()の中で、isStartフィールドを直接参照および更新している
    3. stop()の中で、isStartフィールドを直接参照および更新している
    4. startUpdate()の中で、taskフィールドを直接更新している
    5. stopUpdate()の中で、taskフィールドを直接更新している
    6. インナークラスClockUpdateTaskのrun()の中で、hour/minute/secondの各フィールドを直接更新している
  2. 即値を使用している
    1. Clock()の中で、時分秒の指定に即値 0, 0, 0 を使用している
    2. Clock(int, int, int, int)の中で、時の有効範囲判定に即値 0, 23を使用している
    3. Clock(int, int, int, int)の中で、分の有効範囲判定に即値 0, 59を使用している
    4. Clock(int, int, int, int)の中で、秒の有効範囲判定に即値 0, 59を使用している
    5. Clock(int, int, int, int)の中で、インターバルの有効範囲判定に即値 0を使用している
    6. setHour(int)の中で、時の有効範囲判定に即値 0, 23を使用している
    7. setMinute(int)の中で、分の有効範囲判定に即値 0, 59を使用している
    8. setSecond(int)の中で、秒の有効範囲判定に即値 0, 59を使用している
    9. setInterval(int)の中で、インターバルの有効範囲判定に即値 1を使用している
    10. インナークラスClockUpdateTaskのrun()の中で、時分秒のオーバーフロー判定に即値 60, 0, 24を使用している
  3. 同じコードが繰り返し現れている
    1. Clock(int, int, int, int)の中と、setHour(int)の中で、時の有効範囲判定を行うコードが繰り返し現れている
    2. Clock(int, int, int, int)の中と、setMinute(int)の中で、分の有効範囲判定を行うコードが繰り返し現れている
    3. Clock(int, int, int, int)の中と、setSecond(int)の中で、秒の有効範囲判定を行うコードが繰り返し現れている
    4. Clock(int, int, int, int)の中と、setInterval(int)の中で、インターバルの有効範囲判定を行うコードが繰り返し現れている
      (判定が、<=0 と <1 と表現が異なっているが同じ判定となっている)
  4. マルチスレッド対応が行われていない
    Timerを導入したことにより、マルチスレッド・プログラミングの必要性が生じる。Timerにスケジュールされた処理と、それ以外の処理は、別なスレッドで実行されるからだ。
    1. TimerのスレッドにおいてインナークラスClockUpdateTaskのrun()の中で時分秒の更新を行うが、別なスレッドからgetHour()/getMinute()/getSecond()が呼ばれたとき、整合が取れた時分秒が返されない可能性がある。
  5. ローカル変数がフィールドの名前を隠蔽している
    1. stratUpdate()の中で、ローカル変数intervalは、フィールドintervalの名前隠蔽となっている
  6. ClockクラスはtoStringメソッドをオーバーライドしていない
    オブジェクトの状態を文字列化するtoStringをオーバーライドすべきである。
    1. ObjectクラスのtoStringメソッドが呼ばれるため、オブジェクトのハッシュ値が文字列化される。Clockオブジェクトにとってハッシュ値は状態を表現するに不十分である。

レビュー指摘事項の対処方法

  1. すべてアクセッサを利用するようにコードを修正する。
    1. hour/minute/secondのフィールドについては、アクセッサsetHour/setMinute/setSecondを介して更新する。timerについては、timer初期化メソッドinitTimer()を新規に設ける。
          public Clock(int anHour, int aMinute, int aSecond, int anInterval) {
              :
              setHour(anHour);
              setMinute(aMinute);
              setSecond(aSecond);
              setInterval(anInterval);        
      
              initTimer();
          }
      
    2. isStartフィールドの参照は、isStart()を介する。isStartフィールドの更新は、SetterメソッドsetStart(boolean)を新規に設ける。
          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;
          }
    3. 同上
          public void stop() throws IllegalStateException {
              if (!isStart()) {
                  throw new IllegalStateException(
                      "stop() invoked in stop state"
                  );
              }
              setStart(false);
              // 時刻更新停止
              stopUpdate();
          }     
    4. taskフィールドは、startUpdate()/stopUpdate()からだけ参照される。また、値が変わるのではなく、オブジェクトを参照するかnullかのどちらかである。ここにアクセッサ(Setter/Getter)を持ち込むのはどうかと思い、対処はしないこととする。
    5. 同上
    6. アクセッサを介して更新する(4.1と重なるので、4.1参照)
  2. すべて定数を定義してそれを利用するようにコードを修正する。
    1. 定数DEFAULT_HOUR/DEFAULT_MINUTE/DEFAULT_SECONDを定義してこれを利用する
          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;
    2. 定数MIN_HOUR/MAX_HOURを定義してこれを利用する(3.1と重なるので3.1参照)
    3. 定数MIN_MINUTE/MAX_MINUTEを定義してこれを利用する(3.2と重なるので3.2参照)
    4. 定数MIN_SECOND/MAX_SECONDを定義してこれを利用する(3.3と重なるので3.3参照)
    5. 定数MIN_INTERVALを定義してこれを利用する(3.4と重なるので3.4参照)
    6. 2.と同じ
    7. 3.と同じ
    8. 4.と同じ
    9. 5.と同じ
    10. 定数MAX_HOUR/MAX_MINUTE/MAX_SECONDを利用する
              currentSecond++;
              if (MAX_SECOND < currentSecond) {
                  currentMinute++;
                  currentSecond = 0;
                  if (MAX_MINUTE < currentMinute) {
                      currentHour++;
                      currentMinute = 0;
                      if (MAX_HOUR < currentHour) {
                          currentHour = 0;
                      }
                  }
              }
  3. 範囲チェック用メソッドを定義してこれを利用する
    1. checkHourCondition()を定義して利用する
    2. checkMinuteCondition()を定義して利用する
    3. checkSecondCondition()を定義してこれを利用する
    4. checkIntervalCondition()を定義してこれを利用する
      各checkメソッドと範囲の定数の定義
          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;
          }
  4. スレッドセーフな設計を行う
    1. インナークラスClockUpdateTaskのrun()の中でhour/minute/secondを更新している過渡状態において、他のスレッドからgetHour()/getMinute/getSecond()が呼ばれたときは、過渡状態が終了するまでブロックする。方法は、hour/minute/secondを一括して変更する同期メソッドsetTime(int, int, int)を追加する。
          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);
              }
          }

      getHour()/getMinute/getSecond()を個々に同期メソッドとしても、不整合はありえる。時分秒を一回で返すメソッドを同期化する必要があるが、今回はClockクラスを利用する側で同期ブロックで囲み、getHour()/getMinute/getSecond()を呼ぶことにする。
       * 例<pre>
       * Clock clock = new Clock(h, m, s, i);
       * clock.start();
       *   :
       * synchronized(clock) {
       *   hour = getHour();
       *   minute = getMinute();
       *   second = getSecond();
       * }
       * </pre>
  5. ローカル変数名を変更する
    1. フィールドは必ずアクセッサを介するので、ローカル変数と名前がかぶったとしてもあまり気にしなくてもよいとは思う。が、コーディング標準に従って改名する。currentInterval とでもしておこう。
          private void startUpdate() {
              task = new ClockUpdateTask();
              int currentInterval = getInterval();
              assert 0 < currentInterval :
                  "non-positive interval = " + currentInterval;
              timer.scheduleAtFixedRate(task, 0, currentInterval);
          }
  6. toStringメソッドをオーバーライドする
        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
クラス図 Ver.2.3

それぞれの修正を行ってはテストケースを実行して確認する、という作業を繰り返してレビュー対処によるコード修正が進んでいく。もし、ユニットテストでClockTest.javaを記述していなかったら、修正結果の確認が難しく、修正によるバグが潜り込んでいたろう。


ドキュメント

まずイテレーションNo.1で作成したREADMEファイルをイテレーションNo.2に合わせて修正を行う。

続いてAPIドキュメントをイテレーションNo.2のソースコードから生成し直す。

READMEファイル

イテレーションNo.1の修正版。コンパイル・実行オプション、および実行結果をイテレーションNo.2に合わせた。

APIドキュメント

このイテレーションでは、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圧縮が付いているので、他の圧縮ツールを使わなくてもよい。

JARファイルの作成

時刻プログラムを実行するには、<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>
cvf
UNIX系のツールtarを使っている人にはお馴染みのオプション。cはJARファイル生成を指定し、vは実行中に詳細表示を指定し、fは生成するJARファイル名を指定する。
-C classes
JARファイルにクラスファイルを収めるときは、パッケージ名に対応するディレクトリ構造にする必要がある。今回の時刻プログラム開発プロジェクトのように、プロジェクトディレクトリの下にclassesとサブディレクトリが置かれている場合、classesディレクトリを含んだJARファイルが出来てしまう。そのときは、-Cオプションで、JAR生成時のカレントディレクトリを指定する。
JARファイルに収めるファイルを指定。.は、カレントディレクトリ以下のディレクトリ・ファイルを対象とする。

JARファイルからの実行

今回作成したJARファイルから実行するには、次のようにコマンドを実行する。

E:\ClockProject>java -cp jpe-clock.jar jp.gr.java_conf.torutk.jpe.client.ClockClient
  :

実行可能JARファイルの作成

JARファイルの中のマニフェストファイルに、アプリケーションを実行するmainメソッドを持つクラスを指定する。なお、JARコマンド実行時に、マニフェスト指定をしないとデフォルトのマニフェストファイルが生成される。

実行クラスを指定するclockclient.mf
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>
cvfm
オプションのmはマニフェストファイル指定を行う。fmの順で指定した場合は、続いてJARファイル名、マニフェストファイル名とコマンドライン上に記述する。逆にmfの順で指定した場合は、続いてマニフェストファイル名、JARファイル名をコマンドライン上に記述する。

実行可能JARファイルからの実行

作成した実行可能JARファイルから実行するには、次のようにコマンドを実行する。

E:\ClockProject>java -jar jpe-clock.jar
  :

また、Windowsマシンであれば、エクスプローラ上でダブルクリックしても実行されるが、この場合コンソールが伴わない(javawコマンドに結びついてる)ため、実行の様子は分からない。

今後の課題


構成管理

そろそろソースコードも変更が増えてきたし、Clockクラスも肥大化してきつつあるのでクラスを分割していくことになりそうだ。そうなってくると、バージョン管理が欲しくなってくる。

だが今回は時間も尽きてきたので、前回同様<Project Root>ディレクトリ以下をごっそり取っておくことにする。

E:\ClockProject>cd ..
E:\>ren ClockProject ClockProject-20020616
E:\>

まとめ

イテレーションNo.2で実施したことは以下のとおり。


This page is written by Toru TAKAHASHI.(torutk@02.246.ne.jp)