[ Java Project Exampleページへ戻る ]

イテレーション No.3

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


コンテンツ

目標を定める

3回目のイテレーションに入る。最初に、今後の大まかな計画(ロードマップ)を考えてから、イテレーションNo.3の内容を定めることにする。

リリース計画(ロードマップ)

時刻プログラムが今後どう発展していくかを計画する。イテレーションNo.1/No.2とやってきて、1つのイテレーションで何でもやろうとすると難しいことが分かった。そこで、機能開発とリリース作業は別イテレーションで行う方がよいと考え、それぞれ奇数イテレーション・偶数イテレーションに分けることにした。また、イテレーションの期間であるが、No.1/No.2の実績を踏まえて奇数、偶数イテレーションのセットで1ヶ月とする。

時刻プログラムのリリース計画
主要な機能 2002.7 2002.8 2002.9 2002.10
No.3 No.4 No.5 No.6 No.7 No.8 No.9 No.10
デスクトップ上で使う
お知らせ機能
ネットワーク(TCP)
ネットワーク(UDP)
ネットワーク(マルチキャスト)
ネットワーク(RMI)
ネットワーク(CORBA)
ネットワーク(Jini)

このリリース計画は、あくまで予定であり、途中途中で見直していく。計画は立てて目に見える形にすることが大切で、状況に応じて変わっていくのが当たり前である。

目標の決定

今回は奇数イテレーションであるから、設計・実装を主眼においた目標を選定する。

時刻プログラムの機能

機能としては、時刻プログラムとして必要なお知らせ機能と、デスクトップで使用できるビューを開発する。

時刻プログラムの非機能要求や課題

設計・実装に関する課題を取り入れる。

設計

今回は、設計のインフラとして今後使用する設計ツール(UML)を選定し、そのツール上で設計を表していく。

設計のインフラ

設計ツールの選定候補を探す

フリーの設計ツールを探す。前回のイテレーションまでに作成したクラス図、シーケンス図、状態遷移図は使えること、今後の開発期間において使用できることを条件に探した。

設計ツール候補一覧
ツール名 バージョン
リリース日
作者/入手先 ライセンス 特徴 動作環境
IIOSS 1.1.2
2002年
IIOSSコンソーシアム Java
ArgoUML 0.10
2002年5月
Tigers.org BSD Java
DOME 5.3
2000年3月
Honeywell GPL/LGPL GTK+
FUJABA 3.0.1
2002年5月
University of Paderborn GPL/LGPL
Poseidon for UML
Community Edition
1.3
2002年?
Gentleware AG 無償

条件が合わずに選定から外れたツールも一応載せておく。

ツール選定

本当はちゃんと比較評価をするとよいのだろうけど。。。
設計ツールには日本語が入れられた方がいいなってことで国産を選んだ。書籍も2冊出ていることだし。なおIIOSSのUMLエディタはArgoUMLを使っているので、ArgoUML使うならIIOSSでもいいかな。DOMEはGTK+環境をWindows上に入れるのがちょっと面倒かなというところで落とした。PoseidonもArgoUMLを使っている。Poseidonは商用製品のエントリー版が無償となっているので、いつ使えなくなる(有償になる/使用制限が厳しくなる)か分からないので落とした。FUJABAは調べる時間がちょっとなく。。。

IIOSSのインストール

IIOSSの入手先から最新版をダウンロードし、解凍して展開する。環境変数を設定するファイルを修正すればOk。Windowsだったら、iioss-1.2-bin.zipを入手する(2002.6.30時点での最新版)。適当な解凍ツールでインストールしたディレクトリ以下に展開する。<解凍ディレクトリ>\bin\setenv.batに記述されている環境変数を自分の環境に合わせて修正する。

D:\win32app\IIOSSの中に展開した場合の設定例を以下に記す。なお、JDKはD:\java\j2sdk1.4.1にインストールしている場合の記述である。

D:\win32app\IIOSS\bin\setenv.batファイルの記述
変更前 変更後
set JAVA_HOME=C:\jdk1.3.1
set IIOSS_HOME=C:\IIOSS
set JAVA_HOME=D:\java\j2sdk1.4.1
set IIOSS_HOME=D:\win32app\IIOSS

設計データ用ディレクトリ追加

プロジェクトディレクトリに、設計データを保存するディレクトリを新たに追加する。designという名称のディレクトリを作成し、ここにIIOSSで作成した設計データを格納する。

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

ユースケース分析

IIOSSが使えるようになったら、まずユースケースを記述してみる。イテレーションNo3では、いよいよユーザが時刻プログラムを使うために、GUI時計表示機能とお知らせ機能を開発する。

ユースケース図

まずはユースケース図を作成した。ユースケースとして切り出したのはかなり直感的。悩んでも正解にたどり着きそうにないので(正解があるとも思えないが)、まずは作って次に移り、不都合があればユースケース図に戻って修正する。

ユースケース図 Ver.3.1

ユースケース記述

個々のユースケースについて、シナリオ記述する。

時刻プログラムを起動する
事前条件
  なし(複数プログラムの起動を許す)
基本系列
a) ユーザはシェルプロンプトから時刻プログラム実行用コマンドを投入する
事後条件
  時刻プログラム・ウィンドウが表れる
代替系列
a-1) ユーザはOSが提供するメニューから時刻プログラムを選択する
  なし

ユースケース記述に、起動方法でコマンドを使うかどうかまで書くかは疑問もある。しかし今回はユーザから見える機能にはプログラムの起動や終了もあるので細かく記述した。システムの起動や終了はユーザではなくシステム管理者をアクターとした方がよいかもしれない(→ユースケース図Ver.3.2に反映)。

時刻とレートを設定して開始する
事前条件
  時刻プログラムが起動している
基本系列
a) ユーザは開始したい時刻(時分秒)を設定する
b) ユーザは時刻が更新されるレートを設定する
c) ユーザは時刻開始を指定する
事後条件
  時刻プログラムが開始し、設定したレートで時刻が更新される
代替系列
  なし

設定方法の詳細(ダイアログとかメニューとか)は、画面設計で決定するのでここでは記述しない。

お知らせ時刻を設定する
事前条件
  時刻プログラムが開始している
基本系列
a) ユーザはお知らせ時刻を設定する
b) 時刻がお知らせ時間になったら、ユーザに通知する
c) お知らせ時刻設定を解除する
事後条件
  お知らせ時刻が解除される
代替系列
  なし
考慮
  お知らせ時刻が現在の時刻より前であっても警告は出さない。

ユーザへの通知方法(サウンドとか画面フラッシュとか)は、画面設計で決定するのでここでは記述しない。

時刻プログラムを終了する
事前条件
  時刻プログラムが起動されている
基本系列
a) ユーザは時刻プログラムを終了させる
事後条件
  時刻プログラムが終了する
代替系列
  なし

終了方法は、画面設計で決定するのでここでは記述していない。

設計の課題

今回開発する機能を実現するための設計課題を整理する。

まず、時刻(Clock)とビューの間で「お知らせ」の仕組みを設ける。現在の時刻、設定された時間、の両者は同じ仕組みにした方が設計がシンプルになる。

次に、アナログ・デジタル表示についてだが、Javaのグラフィックス機能を活用する。Java2Dを使えば、針の回転などが楽に記述できると思われる。

「お知らせ」の仕組みを設計する

イテレーションNo.2までは、実行すると時間を表示して終わる簡単なクライアントしか用意してこなかった。今回は、アナログ/デジタル表示を行うクライアントを開発することになる。ここで設計のキーとなるのが、Clockクラスが時刻を更新したことをどのようにビュー(クライアント)へ反映させるかである。以下に、その方式を3案挙げる。

時刻更新のクライアントへの通知方法
方式 クラス間の関係 お知らせ方法
双方向参照通知 時刻更新のクライアントへの通知方法1 Clockクラスは時刻が更新される毎にクライアントクラスへ通知する。
Clockクラスとクライアントクラスは相互参照を持つため、2つのクラスの間に強固な結合が生じてしまう。
ポーリング 時刻更新のクライアントへの通知方法2 クライアントクラスは時刻が更新されたかどうかを定期的にClockクラスへ問い合わせる。
クラス間の関係は片方向と理想的だが、クライアントはポーリングを行うためのスレッドとその処理を記述する必要がある。またクラス間のメッセージが必要以上に発生する。
インタフェース通知 時刻更新のクライアントへの通知方法3 Clockクラスは時刻が更新される毎にリスナ・インタフェース(オブザーバ)へ通知する。
クラス間の関係は片方向と理想的で、ポーリングも発生しない。

今回のイテレーションで作成する場合、どの方式を選択したとしても、それほど問題なく実装できるだろう。だが、選択した方式によっては今後のイテレーションの中で不都合が出てくるため、そのときに大幅な変更が発生する。XP(eXtreme Programming)では、YAGNIの教えにおいて「いずれ必要になることはまだ実装しない」とあるが、それはアーキテクチャの方式選択ではなく、開発する機能のスコープについて言っているはず。アーキテクチャの方式選択では、今後も踏まえてベストなものを採ることにする。

そこで、この時刻プログラムにおける時刻更新通知は、3番目のインタフェース通知を採用することにした。

参考文献
「UMLによるJavaオブジェクト設計 第2版」、ピーター・コード+マーク・メイフィールド著、ピアソン刊
この本の5章「通知を利用した設計」が非常に参考になる。また、通知の設計例をいくつか挙げているので、デザインカタログとしても有用。

clockパッケージの設計(1)

まず、時刻更新を知らせる機能を設計していく。

イテレーションNo.2では、clockパッケージには1つのクラスClockだけを設けていた(インナークラスとしてClockUpdateTaskクラスは設けていたが)。今回は、client/clock間の設計方針に従ってリスナ・インタフェースを追加することになる。

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

リスナ・インタフェースとしてClockListenerを追加した。リスナは複数存在してもよいので関連に[0..*]を記した。リスナへ通知するメソッドは名前をclockUpdatedとした。引数については検討課題があるので以下に記す。また、リスナを登録・削除するメソッド(そしてリスナ管理機能)をClockクラスに持たせるかどうかも検討課題として以下に記す。

  1. リスナ通知メソッドの引数の設計
  2. Clockクラスにリスナ管理を持たせるか否か

それぞれ検討を行う。

リスナ通知メソッドの引数の設計

インタフェースによる通知を使用する場合、

  1. 通知したい情報(この場合時分秒)を引数に入れて渡す方法
  2. 通知したい事象が発生したことだけを知らせ、通知を受けたオブジェクトが個別に知りたい情報を取り出す方法

の2つが考えられる。後者は「オブザーバ・パターン」として典型である。

通知時に情報も引数に入れて渡す方法
リスナ通知メソッドの引数設計方法1
事象発生だけを通知し、通知を受けたオブジェクトが情報を取り出す方法
リスナ通知メソッドの引数設計方法2

方法1は、一回の事象発生につきオブジェクト間のメッセージが一回だけ発生する。方法2は一回の事象発生につき、メッセージが(1+取りたい情報の数)回発生する。メッセージ数の観点では方法1がよいように見える。

今後、Clockクラスに情報が増えた場合を考える。例えば年月日が追加されたとする。方法1の場合、引数に年月日を追加することになり、既存のClockListenerインタフェースを変更しなくてはならない。別メソッドを用意する場合でも、既存のClockListener実装クラスに変更が波及してしまう(メソッドの実装を記述しなければならない)。方法2では、インタフェースは変えずに、年月日情報を必要とするClockListener実装クラスだけが追加された情報を取り出せばよい。機能の変更に対する頑強性の観点では方法2がよいように見える。
方法1でも引数をクラス化することで変更には強くすることが可能である。例えば、Timeクラスに時分秒のフィールドを持たせ、通知のメソッドの引数にはTimeオブジェクトを渡す。年月日が追加されたとしてもTimeクラスのフィールドに年月日を追加すれば、インタフェースを変更しなくてもよい。JavaのAPIでは、Eventクラス(のサブクラス)がこの引数に使用されている。

決定:方法1を採用する。

Clockクラスのメソッド追加

リスナへの通知を行うために、Clockクラスにメソッドを追加する。追加するメソッドは、ClockListenerの登録・削除メソッドであり、Javaの命名慣習から以下のメソッドとする。

メソッド 事前条件
[例外]
事後条件 不変条件
addClockListener
引数がnullでないこと
[ NullPointerException ]
引数で指定されたオブジェクトは通知対象に存在する 引数で指定したオブジェクト以外の通知対象は維持
removeClockListener
引数がnullでないこと
[ NullPointerException ]
引数で指定されたオブジェクトは通知対象に存在しない 引数で指定したオブジェクト以外の通知対象は維持

Clockクラスにリスナ管理を持たせるか否か

リスナへの通知を行うには、リスナのオブジェクトを保持しておき、時刻が更新されるたびにリスナのメソッドを呼び出す。リスナは複数存在し得るので、何らかのリスト管理が必要だ。また、リスナの登録・削除を行うメソッドも用意する必要がある。この機能をClockクラスに実装してもよいが、Clockクラスの責務がかなり大きなものになる。それに、リスナ管理と通知は、「時刻」とは関係が薄い。関係の薄い機能を1つのクラスに持たせるのは設計上望ましくない。

そこで、リスナの管理と通知は独立したクラスを設け、Clockクラスから委譲する。Clockクラスの利用者から見れば、委譲先のオブジェクトは見えないのであたかもClockクラスが責務を果たしていると思える。

clockパッケージのクラス図

ClockCasterクラスがClockLisnterのリストを管理し、通知を行う。Clockクラスは、クライアントからリスナの登録や解除を依頼されると、ClockCasterにその依頼を転送する(委譲)。

クラス図 Ver.3.2
ClockCasterを加えたclockパッケージのクラス図

シーケンス図

この構造をもとに、シーケンス図を作成した。

シーケンス図 Ver.3.1
ClockCasterを加えた時刻プログラムのシーケンス図

clockパッケージの実装(1)

テストファーストに基づいて、ClockCaster、Clockクラスを実装していく。

ClockCasterクラスの実装

まず、ClockCasterから着手する。ClockCasterは新規クラスなので、まずテストケースから作成する。

<Project Root>
    :
    +---src
    :   +---jp
            +---gr
                +---java_conf
                        +---torutk
                               +---jpe
                                   +---clock
                                   |     +---ClockListener.java
                                   |     +---ClockCaster.java
                                   |     +---ClockCasterTest.java
                                   :

Emacs+JDEEでソースを記述しているなら、M-x jde-gen-junit-test-class-buffer でJUnitのテストケースの雛型が生成できる。JDEE2.2.9β10で生成したテストケースの雛型は下記のとおり。

ClockCasterTest.java

/*
 * Project Clock
 * Copyright 2002 Toru TAKAHASHI. All rights reserved.
 */
package jp.gr.java_conf.torutk.jpe.clock;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 *  Unit Test for classClockCaster
 *
 * Created: Wed Jul 10 15:04:18 2002
 *
 * @author toru
 * @version 1.1
 */
public class ClockCasterTest extends TestCase {

    /** 
     * Creates a new <code>ClockCasterTest</code> instance.
     *
     * @param name test name
     */
    public ClockCasterTest (String name){
        super(name);
    }

    /**
     * @return a <code>TestSuite</code>
     */
    public static TestSuite suite(){
        TestSuite suite = new TestSuite ();
        
        return suite;
    }

    /** 
     * Entry point 
     */ 
    public static void main(String[] args) {
        junit.textui.TestRunner.run(suite());
    }
}// ClockCasterTest

まずは、テストケースClockCasterTestがテストする対象であるClockCasterをフィールドに追加する。ついで、テスト対象であるClockCasterを初期化するsetUp()メソッドを記述する。
雛型で作成されたsuite()メソッドとmainメソッドは今回は使わないので削除する。

ClockCasterTest.java(追加)

    ClockCaster caster;

    public void setUp() {
        caster = new ClockCaster();
    }

次に、リスナをaddして、ClockCasterの中のリスナ管理リストに加わったかどうかをテストするメソッドを記述する。ところで、addClockListener()で登録したリスナがClockCasterの中に保持されているリストに入っていることをどうやって確認すればよいだろうか?

思いつく方法を3つ挙げる。

  1. ClockCasterの内のリスト(仮にArrayList型のlistenersフィールドとして)をクラス外部から見えるように、listenersフィールドのアクセス制御をprivateではなくパッケージローカルに変更する。CloclCasterTestは、直接フィールドを参照すればよい。
  2. ClockCasterに、リストの内容を取り出すメソッドを追加する。このような場合、java.util.Iterator型でリスト内容を取り出すとよさそうだが、取り出した側でリストが変更できないよう細工をしておく必要がある。(Iteratorにはremoveメソッドがあるため)
  3. リフレクションAPIを使うと、クラスのprivateなメンバへアクセスできるようになる。

1.は、テストのために設計を劣化させることになるので却下。2.はよさそうと思う。取り出しメソッドはパッケージローカルなアクセスとすればよい。また、今後このメソッドが役に立つこともあろう。3.もいい案である。

今回は、2.の案を採用する。これに従ってリスナを登録するaddClockListenerメソッドをテストするtestAddClockListenerを記述する。

ClockCasterTest.java(追加)

    /**
     * ClockListenerを登録し、管理リストに追加されたかどうかを確認する。
     */
    public void testAddClockListener() {
        ClockListener theListener = new ClockListener() {
                public void clockUpdated(int h, int m, int s) {
                }
            };
        caster.addClockListener(theListener);
        for (Iterator it=caster.getIterator(); it.hasNext();) {
            ClockListener listener = (ClockListener)it.next();
            if (listener == theListener) {
                return;
            }
        }
        fail("The listener add to the ClockCaster is not exist");
    }

テストケースの記述では、それぞれのメソッドが割と独立している。そこで、メソッド内にインナークラスを定義する方法が有効である。また、インスタンス1つしか生成しない場合、匿名クラスにすることも有用である。テスト技法におけるスタブの作成は、このようにインナークラスや匿名クラスを使うと簡単に行える。

ここで、まずコンパイルを実行する。ClockCaster.javaもClockListener.javaも影も形も存在していないので、コンパイルエラーになるはずである。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java
src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java:22: シンボルを解決でき
ません。
シンボル: クラス ClockCaster
場所    : jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest の クラス
    ClockCaster caster;
    ^
  :

テストファーストに染まる前は、コンパイルエラーを出すことは「いけないこと」みたいな感覚があった。上記のようにClockCasterTest.javaを記述しているときに、まだ存在していないクラスClockCasterを使う場面に出くわしたとする。このままではコンパイルが通らくなるのでClockCasterTest.javaは途中のままでClockCaster.javaを書き始めてしまう。きっとClockCaster.javaを記述している最中に同様なことが起きて、また別のソースを書き始めるのだろう。その結果、書きかけのソースファイルが多量にまき散らされてしまったと思われる。テストファーストを導入すると、コンパイルエラーがでるはず、テストが失敗するはず、テストが1つ通るが残りはまだ失敗するはず、・・・と作業が進んでいくので、コンパイルエラーを出すことは「実装の最初のステップ」となる。つまり、エラーやテスト不合格が、必ず「やらねばならないこと」になる。

次に、ClockCasterTest.javaがコンパイルを通るのに必要最低限なClockCaster.javaとClockListener.javaを記述する。目的はあくまでコンパイルが通ることである。

ClockCaster.java

/*
 * Project Clock
 * Copyright 2002 Toru TAKAHASHI. All rights reserved.
 */
package jp.gr.java_conf.torutk.jpe.clock;

import java.util.Iterator;

/**
 * ClockCaster.java
 *
 * Created: Sun Jul 14 08:27:09 2002
 *
 * @author toru
 * @version 1.1
 */

public class ClockCaster {
    public ClockCaster() {
    }

    /**
     * 引数で指定したリスナを通知対象に加える。
     * すでにリスナが通知対象として存在していた場合でも、リスナを新た
     * に登録する。
     * @param aListener 通知を受けたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    public void addClockListener(ClockListener aListener) {
    }

    /**
     * 引数で指定したリスナを通知対象から削除する。
     * 1つのリスナが複数通知対象として存在していた場合でも、リスナは1
     * つしか取り除かれない。
     * QQQ. 指定したリスナを全て除く方がよいかも。今後検討。
     * @param aListener 通知対象から除きたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    public void removeClockListener(ClockListener aListener) {
    }

    void notifyClockUpdated(int hour, int minute, int second) {
    }

    /**
     * 通知対象リスナのイテレータを返却する。
     * @return 通知対象リスナの不変イテレータ
     */
    Iterator getIterator() {
        return null;
    }
}// ClockCaster

voidメソッドについては、中身を空で定義すればコンパイルが通るようになる。voidでないメソッドは、何かしら値を返す必要がある。今回はgetIteratorメソッドがIterator型を返すが、文法上参照型にはnullを入れてもよいので、nullを返却する。

ClockListener.java

/*
 * Project Clock
 * Copyright 2002 Toru TAKAHASHI. All rights reserved.
 */
package jp.gr.java_conf.torutk.jpe.clock;

/**
 * ClockListener.java
 *
 * Created: Sun Jul 14 08:37:01 2002
 *
 * @author toru
 * @version 1.1
 */

public interface ClockListener {
    void clockUpdated(int hour, int minute, int second);
}// ClockListener

これでコンパイルは通るはずである。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java
E:\ClockProject>

ではJUnitのTestRunnerを実行しよう。まだ現時点ではClockCasterTest.javaのコンパイルが通る最低限のソースを記述しただけなので、テストは失敗するはず

E:\ClockProject>java -classpath .\lib\junit.jar;.\classes junit.textui.TestRunner
 jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest
.E
Time: 0.021
There was 1 error:
1) testAddClockListener(jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest)
java.lang.NullPointerException
        at jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest.testAddClockListener
(ClockCasterTest.java:46)
        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: 1,  Failures: 0,  Errors: 1

E:\ClockProject>

テストが失敗することを確認したので、これからはテストを通すための実装および新たなテストの追加を、ClockCasterクラスの機能がすべて実装されるまで行っていく。

まず、ClockCasterTestクラスのtestAddClockListenerメソッドに記述したテストが成功するための実装を行う。それには、ClockListenerを保持する何らかのリスト、リストのIteratorを取り出すメソッド、リストへClockListenerインスタンスを格納するメソッドの実装を行う。

一般にリスナを保持するリストに要求されるのは、リスナをリストへ追加する/リストから削除する/イベントを通知するためにリストを走査する、といった機能である。そこで、Javaコレクションフレームワークの中から、Listインタフェースを実装するものを選択する。ここではArrayListを使用する。

ClockCaster.java(追加)

    /** ClockListenerを保持する */
    private List listeners = new ArrayList();

    /**
     * 引数で指定したリスナを通知対象に加える。
     * すでにリスナが通知対象として存在していた場合でも、リスナを新た
     * に登録する。
     * @param aListener 通知を受けたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    public void addClockListener(ClockListener aListener) {
        if (aListener == null) {
            throw new NullPointerException("aListener is null");
        }
        listeners.add(aListener);
    }

     /**
     * 通知対象リスナのイテレータを返却する。
     * イテレータを通じて要素を削除されないように、不変コレクションの
     * イテレータを生成して返却する。
     * @return 通知対象リスナの不変イテレータ
     */
    Iterator getIterator() {
        Iterator it = Collections.unmodifiableCollection(listeners).iterator();
        return it;
    }

では、コンパイルしてテストを実行する。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java

E:\ClockProject>java -classpath .\lib\junit.jar;.\classes junit.textui.TestRunner
 jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest
.
Time: 0.01

OK (1 tests)

E:\ClockProject>

OKである。

続いて、ClockCasterTestクラスに、testRemoveClockListenerメソッドを追加し、ClockCasterクラスのremoveClockListenerメソッドの実装を記述し、テストを実行する。

ClockCasterTest.java(追加)

    /**
     * ClockListenerを削除し、管理リストから削除されたかどうかを確認する。
     */
    public void testRemoveClockListener() {
        class SampleClockListener implements ClockListener {
            public void clockUpdated(int h, int m, int s) {
            }
        }

        ClockListener theListener1 = new SampleClockListener();
        ClockListener theListener2 = new SampleClockListener();
        ClockListener theListener3 = new SampleClockListener();
        caster.addClockListener(theListener1);
        caster.addClockListener(theListener2);
        caster.addClockListener(theListener3);
        assertEquals("caster size should be 3.", 3,
                     caster.getNumberOfListener());
        
        caster.removeClockListener(theListener2);
        assertEquals("caster size should be 2.", 2,
                     caster.getNumberOfListener());

        for (Iterator it=caster.getIterator(); it.hasNext();) {
            ClockListener listener = (ClockListener)it.next();
            if (listener == theListener2) {
                fail("The listener removed from the ClockCaster is exist");
            }
        }
    }

登録されているリスナの数をテストするため、ClockCasterクラスにリスナ数を返却するメソッドgetNumberOfListenerを追加した。テスト用のため、アクセス修飾子はパッケージローカルにしている。

ClockCaster.java(追加)

    /**
     * 引数で指定したリスナを通知対象から削除する。
     * 1つのリスナが複数通知対象として存在していた場合でも、リスナは1
     * つしか取り除かれない。
     * QQQ. 指定したリスナを全て除く方がよいかも。今後検討。
     * @param aListener 通知対象から除きたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    public void removeClockListener(ClockListener aListener) {
        if (aListener == null) {
            throw new NullPointerException("aListener is null");
        }
        listeners.remove(aListener);
    }

    /**
     * 通知対象リスナの数を返却する。
     * @return リスナの数
     */
    int getNumberOfListener() {
        return listeners.size();
    }

では、コンパイルしてテストを実行する。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java

E:\ClockProject>java -classpath .\lib\junit.jar;.\classes junit.textui.TestRunner
 jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest
..
Time: 0.01

OK (2 tests)

E:\ClockProject>

OKである。

続いて、リスナへ通知を行う機能をテストするClockCasterTestクラスのtestNotifyClockUpdatedメソッドと、ClockCasterクラスのnotifyClockUpdatedメソッドの実装を行う。

ClockCasterTest.java(追加)

    /**
     * 登録されているリスナに、時刻通知を行うテスト。
     */
    public void testNotifyClockUpdated() {
        class CheckClockListener implements ClockListener {
            boolean called = false;
            public void clockUpdated(int h, int m, int s) {
                called = true;
                if ( h != 10 || m != 10 || s != 10) {
                    fail("clock should be 10:10:10");
                }
            }
        }

        CheckClockListener theListener = new CheckClockListener();
        caster.addClockListener(theListener);

        caster.notifyClockUpdated(10, 10, 10);
        assertTrue("theListener should be called", theListener.called);
    }

ClockCaster.java(追加)

    /**
     * 登録されているリスナ全部に時刻更新を通知する。
     */
    void notifyClockUpdated(int hour, int minute, int second) {
        for (Iterator it = listeners.iterator(); it.hasNext(); ) {
            ClockListener listener = (ClockListener)it.next();
            listener.clockUpdated(hour, minute, second);
        }
    }

では、コンパイルしてテストを実行する。

E:\ClockProject>javac -d .\classes -classpath .\lib\junit.jar -sourcepath
 .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockCasterTest.java

E:\ClockProject>java -classpath .\lib\junit.jar;.\classes junit.textui.TestRunner
 jp.gr.java_conf.torutk.jpe.clock.ClockCasterTest
...
Time: 0.02

OK (3 tests)

E:\ClockProject>

OKである。

ただし、このClockCasterクラスの実装はマルチスレッドを考慮していない。例えば、リスナへ順次通知中にリスナの登録/削除が行われると、例外java.util.ConcurrentModificationExceptionが発生してしまう。リスナへの通知中にはリスナの登録/削除が行われないように同期化を行う必要があるが、それは以降のイテレーションで対応する。ここでは、文書化(ソースコードへのコメントとして)を行っておく。

ClockCaster.java(クラスコメントを追加)

/**
 * ClockCaster.java
 *
 * 本クラスは、ClockListenerインスタンスのリストを管理し、時刻更新/お
 * 知らせの通知を行う。
 * ClockListenerインスタンスの登録/削除は、以下のメソッドを使用する。
 * <ul>
 * <li>addClockListener
 * <li>removeClockListener
 * </ul>
 * 時刻更新を登録されているClockListenerに通知するには、
 * <pre>
 * notifyClockUpdated
 * </pre>
 * メソッドを使用する。
 *
 * 本クラスの実装は、マルチスレッドに対応していない。通知中に
 * ClockListenerの登録/削除が行われるとConcurrentModificationException
 * 例外が発生する。
 * 
 * Created: Sun Jul 14 08:27:09 2002
 *
 * @author <a href="mailto:toru@WINCHAN"></a>
 * @version
 */

ClockCaster.java(notifyClockUpdatedメソッドコメントを追加)

    /**
     * 登録されているリスナ全部に時刻更新を通知する。
     * 通知中にリスナの登録/削除が行われると、実行時例外
     * ConcurrentModificationException がスローされる。
     */
    void notifyClockUpdated(int hour, int minute, int second) {

Clockクラスの実装

Clockクラスの実装を行う。Clockクラスに追加するのは、以下である。

  1. Clockがnewされるときに、ClockCasterをnewしてフィールドに保持する
  2. ClockクラスのaddClockListenerメソッドに、ClockCasterに処理を委譲する実装を記述する
  3. ClockクラスのremoveClockListenerメソッドに、ClockCasterに処理を委譲する実装を記述する
  4. Clockクラスが時刻を更新したら、ClockCasterのnitifyClockUpdatedメソッドを呼び出す

テストファーストに従って、この4つを順次実装していく。まず、テストメソッドとして、リスナを登録し、10秒後にリスナが保持している時刻が一致しているかを判定する。このテストがパスするには、上記の4つが実装されている必要がある。

ClockTest.java(テストメソッドを追加)

    /**
     * 登録したリスナに時刻更新が通知されるかをテストする。
     */
    public void test10secUpdateOnListener() throws Exception {
        Clock clock = new Clock(18, 18, 18);
        class SampleClockListener implements ClockListener {
            int hour, minute, second;
            public void clockUpdated(int h, int m, int s) {
                hour = h; minute = m; second = s;
            }
        }

        SampleClockListener listener = new SampleClockListener();
        clock.addClockListener(listener);
        clock.setInterval(1000);
        clock.start();
        Thread.sleep(10000);
        clock.stop();
        assertEquals(18, listener.hour);
        assertEquals(18, listener.minute);
        assertTrue( 27 <= listener.second && listener.second <= 29 );
    }

Clock.java(フィールド/メソッドの追加)

    /** リスナを管理し、リスナへ通知する */
    private ClockCaster caster;

    public Clock(int anHour, int aMinute, int aSecond, int anInterval) {
            :
        caster = new ClockCaster();
    }

    /**
     * 引数で指定したリスナを通知対象に加える。
     * すでにリスナが通知対象として存在していた場合でも、リスナを新た
     * に登録する。
     * @param aListener 通知を受けたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    public void addClockListener(ClockListener aListener) {
        caster.addClockListener(aListener);
    }

    /**
     * 引数で指定したリスナを通知対象から削除する。
     * 1つのリスナが複数通知対象として存在していた場合でも、リスナは1
     * つしか取り除かれない。
     * @param aListener 通知対象から除きたいリスナ
     * @throws NullPointerException 引数がnullのとき
     */
    void removeClockListener(ClockListener aListener) {
        caster.removeClockListener(aListener);
    }

コンパイルとテストの実行を行う。

E:\ClockProject>javac -source 1.4 -d .\classes -classpath .\lib\junit.jar
 -sourcepath .\src src\jp\gr\java_conf\torutk\jpe\clock\ClockTest.java

E:\ClockProject>java -classpath .\lib\junit.jar;.\classes junit.textui.
TestRunner jp.gr.java_conf.torutk.jpe.clock.ClockTest
........
Time: 20.029

OK (8 tests)

E:\ClockProject>

clientパッケージの設計(1)

デジタル時計表示を行うクライアントを設計する。クライアントの設計では、ユーザ・インタフェースが主題となる。ユーザ・インタフェースの実現には、GUIツールキット(ライブラリ)が大きく関わってくる。幸いにもJavaには非常に強力なGUIツールキットであるSwingと、Java2Dが標準搭載されているので、これを使用する。

clientパッケージのクラス図

JPanelは、アプリケーション独自のパネルを作成する際にベースとするに適しているのでこれを継承する。JPanelには、背景を描く機能や縁(ボーダー)を描く機能があるため、これをうまく活用してなるべく記述するコード量を減らす。


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