The Test/Code Cycle in XP, Part 1: Model (Japanese)

XPにおけるテスト/コーディングサイクル: 第一部 モデル

この論文では、Extream Programming のテクニックを用いた開発を、小さな図書館システムを 例に用いて示す。 第一部では、モデルの開発を、第二部ではユーザインターフェイスの開発を行う。 (原論文はThe Test/Code Cycle in XP: Part 1, Model および The Test/Code Cycle in XP: Part 2, GUI 。 この原論文は、“Extreme Programming Explored” by William C. Wake, Addison Wesley 2001 に収録されています。日本語版は「XPエクストリーム・プログラミング アドベンチャー」長瀬 嘉秀 (翻訳), 今野 睦 (翻訳), 飯塚 麻理香 (翻訳), 畑田 成広 (翻訳) 発行 ピアソン・エデュケーション 2002年です)

焦点は、プログラミングの過程で、ユニットテストと単純なデザインがどのように協調して 行われるかという点にある。コーディングプロセスが小さなステップの繰り返しになっている点に 注意してほしい。各ステップでは、そのステップの目標となるテストランに対し それをバスするのに必要なだけの実装を行う。 そこには時計の振子のようなリズムがある。つまり、ちょっとテストやって、 ちょっとコード書いて、ちょっとテストやって、ちょっとコード書いて。 (これを分かりやすく示すため、以下ではテストコードはページの左に寄せ、 アプリケーションコードは右に寄せて表示している)

ここで用いる例では、文献情報が著者、書名、発行年からできているとしよう。 ゴールは、入力した値に対する情報を検索することができるシステムを書くことだ。 頭の中では、こんな感じのインターフェイスを思い浮べている。

さて、仕事を二つに分けることにする。モデルとユーザインターフェイスである。

我々のモデルはこういうものから構成される。 Documentのコレクションがあり、それは自分自身のメタデータを知っている。 Searcherはどうやって文書を発見するかを知っている。 つまり、Searcherは、Queryが与えられると、Result(Documentの集合)を返す。

ここで、ユニットテスト(と、これらのクラス)をボトムアップで(Document, Result, Query, Searcherの順に)作ることにしよう。

Document、ResultとQuery

Document

Documentは、その著者と題名と発行年を知っていないといけない。 そこで”データバッグ”クラスから始めよう。まず、テストを書く:

public void testDocument() {
    Document d = new Document("a", "t", "y");
    assertEquals("a", d.getAuthor());
    assertEquals("t", d.getTitle());
    assertEquals("y", d.getYear());
}

このテストはコンパイルを通らない。(Documentクラスをまだ作っていないから)。 そこで、メソッドをスタブにしたクラスを作ろう。

テストを走らせるが、ここではテストがまたもや失敗することを確認しよう。 これはちょっと面白い。つまり、ここでは、普通とは反対に、テストが パスしないことを期待してテストをやってるわけだ。 で、期待通りに失敗という結果が得られる。 つまり、最初にテストが失敗することを確認することで、テストが妥当だと いうことのある程度の保証を手に入れているわけだ。たまに、意外にもテストが パスしたりすると「どうなってる!」ということになる。

コンストラクターとメソッドをテストが通るようにちゃんと書く。

この小さなプロセスのハイライト:

The Test/Code Cycle in XP

  • テストをひとつ書く。
  • テストをコンパイルする。まだ何もつくっていないんだから、これは失敗しないといけない。
  • コンパイルできる程度にコードを書く。(必要ならその前にリファクターする)
  • テストを走らせ、失敗することを確認する。
  • テストが通る程度の実装を行う。
  • テストを走らせ、通ることを確認する。
  • 整理し「同じことは一個所で」となるようにリファクターする。
  • 最初に戻って繰返す。

このプロセスに従えば必ず、あなたはテストが失敗するのと成功するのの両方を確認することになる。 それによって、テストが何かをテストしているということの保証が得られる。 そのテストしている何かというのは、今まさにあなたが修正変更した部分であり、意味のある機能として 追加した部分でなくてはならない。

一部の人たちは、単純なsetterとかgetterメソッドのテストは煩わしいものだと主張している。 (“間違うわけがないじゃないか”とか”大量のgettter/setterのテストを書くのは苦痛だ”とか) 私は、とりあえずテストを書くようにしている。

  • テストを書くのにそんなに長い時間はかからないし、明らかに難しいわけでもないのに そうすることにはいい面がある。(「それが大丈夫だという確信があると思えるときに、それを証明するための テストがちゃんと存在する」というわけだ)
  • テストというものは、それのテスト対象となるコードよりも長いライフタイムを持っている。 たとえば、キャッシングや、オブジェクトの遅延生成や、ログを採るように変更をしても、 もともとの機能がちゃんと保証されていることを示すために、そのテストは存在し続けるのだから。
  • getterやsetterを全部テストするのが退屈だということには、裏の意味が隠されている。 つまり、クラスが自分の重さを支えきれなくなっているのかもしれないということだ。 クラスがほとんど”struct”のようになっているとき、レスポンシビリティがクラスの間で 正しく分担されていないことを示すサインであることが多い。

Result

Resultは二つのことを知らなくてはならない: itemの総数と結果のDocumentのリストである。 まず、空のResultがitemを含んでいないことをテストしよう。

public void testEmptyResult() {
    Result r = new Result();
    assert ("count=0 for empty result", r.getCount() == 0);
}

Resultクラスを作り、getCount()メソッドをスタプにする。 その実装に”return 0;“を付加えなければ、それが失敗することを確認する。

次に、二つのdocumentを持つ結果をテストする。

public void testResultWithTwoDocuments() {
    Document d1 = new Document("a1", "t1", "y1");
    Document d2 = new Document("a2", "t2", "y2");
    Result r = new Result(new Document[]{d1, d2});
    assert (r.getCount() == 2);
    assert (r.getItem(0) == d1);
    assert (r.getItem(1) == d2);
}

getItem()メソッド(nullを返す)を追加し、テストが失敗するのをチェックする。 (もうこれ以上このチェックについていちいち書かないが、それはやっていると考えてほしい それには数秒しかかからないけど、それに見合う保証がえられる)。 Resultの簡単なバージョンを実装する。

public class Result {
    Document[] collection = new Document[0];

    public Result() {}

    public Result(Document[]collection) {
        this.collection = collection;
    }

    public int getCount() {return collection.length;}

    public Document getItem(int i) {return collection[i];}
}

テストが通れば、このクラスは完成する。

Query

Queryの表現としては、query文字列それ自体を 用いることにする。

public void testSimpleQuery() {
    Query q = new Query("test");
    assertEquals("test", q.getValue());
}

Queryクラスにはコンストラクターを定義し、query文字列を覚え、 getValue()によってその文字列を通知できるようにする。

Searcher

Searcherは、一番面白いクラスである。 簡単なテストケースから始める。空のDocumentのコレクションからは、どんなqueryを しても何も返ってこないはずだ。

public void testEmptyCollection() {
    Searcher searcher = new Searcher();
    Result r = searcher.find(new Query("any"));
    assert(r.getCount() == 0);
}

これはコンパイルが通らないので、Searcherクラスのスタプを作る。

public class Searcher {
    public Searcher() {}
    Result find(Query q) {return null;}
}

テストはコンパイルできるが、実行すると正常に失敗する。 (find() がnullを返しているから)。 それを修正して、”public Result find(Query q) {return new Result();}“と すれば動くようになる。

意味のある検索を考えるのは、もっと面白い。 それを考えはじめると、Searcherがドキュメントを獲得しようとするのはどこかという問題 に突き当る。手始めに、Documentの配列をSearcherのコンストラクタに渡すようにしてみよう。 しかし、まずはテストだ。

public void testOneElementCollection() {
    Document d = new Document("a", "a word here", "y");
    Searcher searcher = new Searcher(new Document[]{d});

    Query q1 = new Query("word");
    Result r1 = searcher.find(q1);
    assert(r1.getCount() == 1);

    Query q2 = new Query("notThere");
    Result r2 = searcher.find(q2);
    assert (r2.getCount() == 0);
}

このテストは、そこに存在するものを見つけられなくてはならないということと、 そこに存在しないものは見つけてはいけない、ということを示している。

これを実装するために、(またもや失敗するようになっているので) 新しいコンストラクタを作り、テストがコンパイルできるようにする。。 その後で、実装について真剣に考えていく。

まず、検索では、繰りかえされるfind()の呼出しの間、コレクションに関する知識を 維持していなくてはならないことが分る。 だから、それを保持するためのメンバー変数を追加し、コンストラクターでその引数を記憶するようにする。

Document[] collection = new Document[0];

public Searcher(Document[] docs) {
    this.collection = docs;
}

ここで、find()の一番簡単な実装は、document の各要素についてqueyに合うものをResultに追加していくことを繰返すというものだ。

public Result find(Query q) {
    Result result = new Result();
    for (int i = 0; i < collection.count; i++) {
        if (collection[i].matches(q)) {
            result.add(collection[i]);
        }
    }
    return result;
}

これはうまく行きそうだ。だが二つだけ問題がある。 Documentには、matches() メソッドがないということと、 Resultにはadd() メソッドがないということだ。

テストを追加しよう。それぞれのフィールドでのマッチをチェックするのと、 documentがひとつもマッチしなかったとき、queryはマッチしてはいけない。

public void testDocumentMatchingQuery() {
    Document d = new Document("1a", "t2t", "y3");
    assert(d.matches(new Query("1")));
    assert(d.matches(new Query("2")));
    assert(d.matches(new Query("3")));
    assert(!d.matches(new Query("4")));
}

現実に処理する必要のあるqueryには決めておく必要のあることが三つある。 つまり、空のqueryと、部分的なマッチと、大文字小文字の区別である。 とりあえず、文字列は空文字列とは常にマッチし、 部分的にマッチすれば、全体もマッチすると考える。 また、大文字小文字の区別は行う。 将来、この約束は変えるかもしれない。

これで、マッチを実装するのに必要な情報は全部そろった。

public boolean matches(Query q) {
    String query = q.getValue();

    return 
       author.indexOf(query) != -1
    || title.indexOf(query) != -1
    || year.indexOf(query) != -1;
}

これでtestDocumentMatchingQuery()は動くようになる。しかし、 testOneElementCollection()はまだ動かない。というのは、Resultに add()メソッドがまだないからだ。そこで、Result.add(): メソッド に対するテストを追加する。

public void testAddingToResult() {
    Document d1 = new StringDocument("a1", "t1", "y1");
    Document d2 = new StringDocument("a2", "t2", "y2");

    StringResult r = new StringResult();
    r.add(d1);
    r.add(d2);
        
    assert ("2 items in result", r.getCount() == 2);
    assert ("First item",  r.getItem(0) == d1);
    assert ("Second item", r.getItem(1) == d2);
}

このテストは失敗する。Resultは配列を使ってリストを覚えているように実装したが、 それは要素数が変るような構造にとってはベストな選択ではない。そこで、Vectorを使う ように変更しよう。

Vector collection = new Vector();

public Result(Document[] docs) {
    for (int i = 0; i < docs.length; i++) {
        this.collection.addElement(docs[i]);
    }
}

public int getCount() {return collection.size();}

public Document getItem(int i) {
    return (Document)collection.elementAt(i);
}

前に作ったユニットテストtestEmptyResult()testResultWithTwoDocuments() がまだ通ることを確認しよう。 それから、新しいメソッドを追加する。

public void add(Document d) {
    collection.addElement(d);
}

新しいコンストラクターResult(Document[])について考えよう. これは、testResultWithTwoDocuments()テストを動くようにするために 作成したが、その時点では、これがdocumentを含むようなResultを作るたったひとつの方法だった。 しかしその後、Searcherで必要になったのでResult.add()を追加した。 これによって、配列のコンストラクターはもはや必要がなくなってしまったのだ。 そこで、まずテストの帽子にかぶり直し、テストを書き直すことにする。 つまり、Result r = new Result(new Document[]{d1,d2});の部分を次のようにかえる。

Result r = new Result();
r.add(d1);
r.add(d2);

まず、すべてのテストがまだちゃんと通ることを確認し、配列ベースのコンストラクタ 以外のところに問題がないことを確認する。 この段階で、testAddingToResult() は、基本的に testResultWithTwoDocuments()と同じことのテストになったので、後でそれも 消すことにする。

ついに DocumentとResult、Query、Searcherのテストが通るようになった。

Initialization

Documentsのローディング

Searcherはどこからdocumentを取ってくるのだろうか? 現時点では、メインからそれのコンストラクタを呼出して、documentの配列として渡している。 そのかわり、searcherが自分でdocumentをロードするようになってほしい。

テストを書くことから始める。 Readerを使おう。そのためには例外についても準備しよう。 また、テスト専用のgetCount()メソッドを追加し、なにかがロードされたかどうかを 確認する。 テストをテスト対象のクラスと同じパッケージに置いておくことの利点は、テストでそのオブジェクト の内部状態が見えるようにするために、protectedなメソッドを追加していくことができるところにある。

public void testLoadingSearcher() {
    try {
        String docs = "a1tt1ty1na2tt2ty2"; // t=field, n=row
        StringReader reader = new StringReader(docs);
	    Searcher searcher = new Searcher();
	    searcher.load(reader);
	    assert("Loaded", searcher.getCount() == 2);
    } catch (IOException e) {
    	fail ("Loading exception: " + e);
    }
}

Searcherは、まだ配列を使っている(この時点での最も単純なやり方)ことに注意しよう。 Resultでやったのと同じことをここでもやって、配列からVectorにリファクターする。

package search;

import java.util.*;

public class Searcher {
    Vector collection = new Vector();

    public Searcher() {}

    public Searcher(Document[] docs) {
	for (int i = 0; i < docs.length; i++) {
	    collection.addElement(docs[i]);
	}
    }

    public Query makeQuery(String s) {
	return new Query(s);
    }

    public Result find(Query q) {
    	Result result = new Result();
    	for (int i = 0; i < collection.size(); i++) {
            Document doc = (Document)collection.elementAt(i);
	    if (doc.matches(q)) {
                result.add(doc);
            }
        }
        return result;
    }
}

(前に書いたテストがパスするかどうか確認)さて、ローディングを実行できるようになった。

// Searcher:
public void load(Reader reader) throws IOException {
    BufferedReader in = new BufferedReader(reader);
    try {
    	String line = in.readLine();
    	while (line != null) {
	    collection.addElement(new Document(line));
	    line = in.readLine();
    	}
    } finally {
 	try {in.close();} catch (Exception ignored) {}
    }
}

int getCount() {
    return collection.size();
}
// Document:
public Document(String line) {
    StringTokenizer tokens = new StringTokenizer(line, "t");
    author = tokens.nextToken();
    title = tokens.nextToken();
    year = tokens.nextToken();
}

Searcherの配列ベースのコンストラクタはもう必要がない。 testを修正し、コンストラクタを削除する。

public void testOneElementCollection() {
    Searcher searcher = new Searcher();
    try {
	StringReader reader = new StringReader("ata word herety");
	searcher.load(reader);
    } catch (Exception ex) {
	fail ("Couldn't load Searcher: " + ex);
    }

    Query q1 = searcher.makeQuery("word");
    Result r1 = searcher.find(q1);
    assert(r1.getCount() == 1);

    Query q2 = searcher.makeQuery("notThere");
    Result r2 = searcher.find(q2);
    assert (r2.getCount() == 0);
}

SearcherFactory

Searcherはどこからやってくるのか? これまでは、それはコンストラクタを呼ぶ誰かだということで棚上げにしてきた。 クライアントがコンストラクタに依存してしまわないように、Searcherを指定できるような ファクトリーメソッドを導入することにしよう。 (テストのためには、”test.dat”ファイルをテスト用のディレクトリに置くことにする。 もし、もうすこし丁寧にやるのだったら、テストプログラムで、ファイルの作成と削除を やるようにすればよい)

public void testSearcherFactory() {
    try {
        Searcher s = SearcherFactory.get("test.dat");
        assert (s != null);
    } catch (Exception ex) {
        fail ("SearcherFactory can't load: " + ex);
    }
}

次のように実装できる。

public class SearcherFactory {
    public static Searcher get(String filename) throws IOException {
        FileReader in = new FileReader(filename);
        Searcher s = new Searcher();
        s.load(in);
	    return s;
    }
}

これで、クライアントはSearcherFacotryに要求をだして、Searcherを獲得できるようになった。

ふりかえって

ここで、デザインの帽子にかぶり直し、これまで作ってきたメソッドを二つの視点からまとめておく。

二つの視点というのは、SearchクライアントとSearcherクラスである。 publicメソッドについて、誰がそれを使っているのかをまとめておく。

Search クライアント Searcher class
Document.getAuthor()
Document.getTitle()
Document.getYear()
new Query()
Result.getCount()
Result.getItem()
Searcher.find()
new Document()
Document.matches()
Query.getValue()
new Result()
Result.add()

DocumentとQueryクラスを見ていると、彼らが十分に働いていないという思いで 私の心は痛む。(“データバッグ”以上のものではないから) しかし、どちらも同じ程度にはよくできているし、意味もあるし、”ほぼドメイン” クラスになっているように見えるので、それらを修正しようという衝動は押えられる。 それから、ResultとSeacherクラスは、ちょうどいいバランスになっているようだ。

開発プロセスについてはどうだろうか? なにか行き当たりばったりのようにも見える。 たとえば、配列からVectorへのデータ構造の変換を(二度も!)やらなくてはならなかった。 これは、我々の開発方法が間違っているということなのだろうか? そうではない。 配列は最初の段階では、それが最適なデータ構造だったのだ。そして、必要に 応じて、それは変更されたのである。我々は、行き当たりばったりで進んだとしても、 その先が後戻りのできない行き止りでないかぎり、問題だとは思わない。 我々は全知ではないのだから、判断を変更しなくてはならないときがいずれやってくるものだ。 つまり、重要なことは、悪いまたは複雑すぎる設計を相手にしてお手上げになって しまわないように気をつけるということだ。

前進: インターフェイス

これまで展開してきた実装は、出発点としてはいいが、 これが最終地点ではない。 現実のシステムでは、文献情報はどこか他の場所に保存される。たとえばデータベースの中であったり、 XMLファイルになっていたり、あるいは他のネットワークにあったりとか。 私たちは、このクラスを使うクライアントがそれ以外の部分についてどういう実装を選ぶのか については知りたくない。

上の表の”Search クライアント”の列にあるメソッドは、クライアントから必要とされている インターフェイスを示している。”Query”はクラスとしてはたぶん問題ないだろう。 (クライアントはqueryを 作れないといけないから)しかし、SearcherとResultとDocumentにはインターフェィスを定義して おきたい。 そこで、Refactoring(Flowerの本)の”Extract Interface” を適用しよう。

残念なことに、インターフェイスの名前として使いたい名前は、すでにクラスの名前として 使ってしまっている。我々は、クライアントの観点から見てものごとがすっきりしているほうが よいと考えているので、クラスの名前をかえインターフェイスに簡潔な名前を使うようにする。 今のところみんなstringをベースにしているので、SearcherをStringSearcherに、という具合に 変更することにする。

というわけで、Searcher.javaはStringSearcher.javaにする。 呼出し個所と参照をすべて修正すし、テストを走らせ変名を正しく実行したことを確認する。

interfaceを導入する:

public interface Searcher {
    public Result find(Query q);
}

(テストを実行)StringSercherはinterfaceをimplementするようにする。(テストを実行) ここで、StringSercherを名前で参照している唯一の個所はSearcherFactory interface だけになっていなくてはならない。 (この最後の依存関係を除くこともできるし、おそらくSting*オブジェクトを別の パッケージに移動することも可能だろう。しかし、長くなるのでここではそこまではしない)

同じプロセスをResultについて行い、ResultをStringResultに変名し、インターフェイスを 使うようにする。

public interface Result {
    public Document getItem(int i);
    public int getCount();
}

StringSearcherクラスは、StringResultオブジェクトを返すようになったままだが、 その復帰型はResultとしておかなくてはならない。(String*クラスが他のクラスに 依存するかどうかは問題ではないのだが、クライアントがその事実を知るようには したくない)

さいごに、Documentのインターフェイスを導入する。

public interface Document {
    public String getAuthor();
    public String getTitle();
    public String getYear();
}

クライアントが依存することになる具象クラスは二つになった。SearcherFactoryとQueryだ. クライアントはSearcher, Result, Documentについてはインターフェイスに依存するが、 これをinplementするクラスには直接関らない。

結論

我々は、文献検索システムのモデルを典型的なExtream Programmingのスタイルに従って 開発してきた。特に、単純なデザインと、テストとコーディングの反復プロセスを強調してきた。 ユニットテストはデザイン、コーディング、およびリファクタリングの作業をサポートしてくれる。 最終的なシステムは、単純なコマンドラインまたはグラフィックユーザインターフェイスを それにアタッチする形で完成できるだろう。

資料

[Written 1-25-2000; re-titled and revised 2-3-2000; added search.zip 7-2-00.]