Pythonでなんか作ってみる -5ページ目

辞書を片手に~PatternResponderの作成(8)

辞書を片手に~辞書はファイルに(5) 」でRandomResponderにおけるランダムに応答を返す処理のテストを実施しない言い訳が書いてある。

あの時点ではその言い訳が通ったが、そろそろ無理が出てきたということだろう。


なぜか、RandomResponderのテストはFileReaderTestになっている。最初からランダムのテストをしない気で満々であるのが良く分かる。

実際にはFileを扱う部分(loadDictionary)はResponderに移動してしまったし、assertEquals(String[],String[])も2箇所で定義されていたりして煩わしい。

ランダムのテストを追加する前に、テストケースのリファクタリングに取り掛かる。


まずは、FileReaderTestをResponderTestとRandomResponderTestに分割する。


手順は、

1 FileReaderTestをRandomResponderTestにリネーム

2 空状態のResponderTestを作成

3 RandomResponderTestとPatternResponderTestをResponderTestから継承するように変更

4 assertEquals(String[],String[])とtestLoadDictionary()をpull up(この順番で無いとコンパイルエラーが発生する)

5 PatternResponderTestのtestLoadDictionary()がオーバライド扱いになってしまうので、testLoadDictionaryPattern()にリネーム


続いて、RandomResponderTestにテストを追加する。

確実に失敗するとは思うが、random.txtの応答を順番に返すテストをしてみる。


public void testRandomResponse(){
RandomResponder rr = new RandomResponder("random");
assertEquals("今日はさむいね", rr.response("1番目の応答"));
assertEquals("チョコたべたい", rr.response("2番目の応答"));
assertEquals("きのう10円ひろった", rr.response("3番目の応答"));
}

テスト実行。当然失敗である。

RandomResponderがどのように応答をランダムに選んでいるのかというと、java.util.Randomクラスを使用している。

このクラスの代わりに、『Randomクラスのように振舞うが予め決められた値を返すクラス』を使用するようにして、テストと本番に応じて使用するRandomクラスを切り替えればよい。


まずは、Randomクラスを継承した偽Randomクラスを作る。


public class FakeRandomInt extends Random {

}


次にRandomResponderがRandomクラスのインスタンスをコンストラクタで受け取れるようにする。


public class RandomResponder extends Responder {

Random rnd;
public RandomResponder(String name) {
this(name,"random.txt",new Random());
}
public RandomResponder(String name,Random rnd) {
this(name,"random.txt",rnd);
}
public RandomResponder(String name,String fileName) {
this(name,fileName,new Random());
}
public RandomResponder(String name,String fileName,Random rnd) {
super(name);
this.rnd = rnd;
try {
resps = loadDictionary(new FileReader("dics/"+fileName));
} catch (FileNotFoundException e) {
resps = new String[]{""};
}
}
....

}


下線部がRandomに関する修正である。

そして、先ほどのテストでRandomResponderのインスタンス生成時に偽Randomクラスを渡すようにする。


public void testRandomResponse(){
RandomResponder rr = new RandomResponder("random",new FakeRandomInt());
assertEquals("今日はさむいね", rr.response("1番目の応答"));
assertEquals("チョコたべたい", rr.response("2番目の応答"));
assertEquals("きのう10円ひろった", rr.response("3番目の応答"));

}

ここまででテスト実行。Red。やはり状況は変わらない。FakeRandomIntがRandomから何も変更していないためだ。

RandomRespondeで使用しているRandomのI/FはnextInt(int)だけである。当面、これをオーバーライドするだけで十分だろう。

とりあえず、かならず0を返すように実装してみる。


public class FakeRandomInt extends Random {

public int nextInt(int arg0) {
return 0;
}

}

テスト実行。1番目の応答のテストは通るようになった。

このテストに使用するだけなら、nextInt(int)は指定した範囲で0から順番に値を返せば十分に思える。

そのように修正する。


public class FakeRandomInt extends Random {
private int fakeValue = 0;
public int nextInt(int arg0) {
return (fakeValue++) % arg0;
}
}

テスト実行。Green。OK、これでランダムを制御する道具を手に入れた。




辞書を片手に~PatternResponderの作成(7)

この章も随分長くなってきた。そろそろ何がやりたいのか分からなくなってきたので、スピードを上げよう。


ToDoリスト

・正規表現を使ってパターンに反応するResponder(以下の仕様を満たす)

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される

・マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す

・応答例の中に「%match」という文字列があれば、パターンにマッチした文字列と置き換えられる

・パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める

・PatternResponderがRandomResponderの機能を利用している(メソッドの上位クラスへの移動に関するリファクタリングが出来そうである)

・パターン辞書に応答例が無い場合のイリーガルケース


まずは、「パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める」を行う。


今更ながら、PatternResponderがResponderを継承していないことに気づいたので、継承させる。

その上で、response()メソッドを使えばいいだろう。


public void testResponse() throws FileNotFoundException {
PatternResponder resp = new PatternResponder("pattern", new FileReader("dics/pattern.txt"));
assertEquals("さむくないよ",resp.response("今日はさむいね"));
assertEquals("pattern",resp.name());
}

PatternResponderのコンストラクタに名前を指定するように変更している。

テスト実行する。予定通りRed(テスト失敗)。

response()をオーバーライドしよう。


/**
* パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める
*
* @param string 問い掛けのメッセージ
* @return 対応する応答
*/
public String response(String msg) {
for (int index=0;index < patterns.length; index++) {
Matcher m = patterns[index].matcher(msg);
if (m.find()) {
return responses[index];
}
}
return "";
}

中身の実装は、「辞書を片手に~人工無能のための正規表現(3)」のmatch()メソッドから持ってきた。

テスト実行-Green。この実装だとマッチするパターンが無い場合は空文字列を返す。

これだと「マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す」に反しているわけだ。次はこれに取り掛かるべきだろう。


しかし、ランダムはテストするのが難しい。毎回結果が変わるというのがランダムであると定義すると、適切な出力結果と比較するのが不可能なためだ。

そろそろランダムを(元々擬似ランダムだというのはおいといたとしても)擬似る仕組みを入れた方がいいだろう。


その前に細かいToDoを片付けてしまおう。

まず、「PatternResponderがRandomResponderの機能を利用している」はloadDictionary()をResponderに移動させてしまう。その際。PatternResponder.loadDictionary()が被ってしまっているので、loadDictionaryPattern()にリネームしておく。


次に、「パターン辞書に応答例が無い場合のイリーガルケース」だが、この場合は、応答例として一つの空文字列が登録されていると考えるのがいいだろう。

そのテストを追加する。random.txtの形式を読み込ませることで、タブ無し状態をテストする。


public void testLoadDictionaryResponseIlliegal() throws FileNotFoundException {
PatternResponder responder = new PatternResponder("pattern", new FileReader("dics/random.txt"));

assertEquals(new String[]{"","",""},
responder
.loadDictionaryResponse(new FileReader("dics/pattern.txt")));
}

ここで気づいたが、loadDictionaryPattern()とloadDictionaryResponse()の引数はもう使用していないな。この後削除することにして、テストを実行する。

Red。以下でjava.lang.ArrayIndexOutOfBoundsException: 1が発生した。


responses[index] = patternAndResponses[1];

tabが無いから、patternAndResponsed[1]も当然存在しないわけだ。ここは単純にlengthを見るだけでいいだろう。以下のように修正する。


if (patternAndResponses.length > 1) {
responses[index] = patternAndResponses[1];
}else{
responses[index] = "";
}

tabの後に、文字列が無い場合はどうなるんだろう?不安に思ったのでテストを追加する。


public void testLoadDictionaryResponseIlliegal2() throws FileNotFoundException {
PatternResponder responder = new PatternResponder("pattern", new StringReader("test1\ttest1\ntest2\t\ntest3\ttest3"));

assertEquals(new String[]{"test1","","test3"},
responder
.loadDictionaryResponse(new FileReader("dics/pattern.txt")));
}

いちいちファイルを作るほどのことは無いかと思ったので、StringReaderで代用させる。PatternResponderのコンストラクタが引数にFileReaderを指定しているので、Readerに変更する。これは、Responder.loadDictionary()にそのまま渡している引数であるので、そっちも変更する。

また、ついでにloadDictionaryPattern()とloadDictoinaryResponse()の引数も削除。


テストを実行する。Green。今の実装で大丈夫。


この時点でのToDoリストは以下である。

・正規表現を使ってパターンに反応するResponder(以下の仕様を満たす)

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される

・マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す

・応答例の中に「%match」という文字列があれば、パターンにマッチした文字列と置き換えられる

・パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める

・PatternResponderがRandomResponderの機能を利用している(メソッドの上位クラスへの移動に関するリファクタリングが出来そうである)

・パターン辞書に応答例が無い場合のイリーガルケース



辞書を片手に~PatternResponderの作成(6)

ToDoリスト
・loadDictionary()とloadDictionaryResponse()で、同じファイル読み込みを繰り返している


これを解消するために、上記2メソッド結果をインスタンス変数に保存する形にする。

そのため、まずはメソッドをインスタンスメソッドに変更する。


public void testLoadDictionary() throws FileNotFoundException {
PatternResponder responder = new PatternResponder();

assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった") },
responder
.loadDictionary(new FileReader("dics/pattern.txt")));
}

public void testLoadDictionaryResponse() throws FileNotFoundException {
PatternResponder responder = new PatternResponder();

assertEquals(new String[]{"さむくないよ","食べれば","いいね"},
responder
.loadDictionaryResponse(new FileReader("dics/pattern.txt")));
}

staticメソッドをインスタンスメソッドの形式で呼び出しても問題ないのでテストはGreenのままである。

本体の方をインスタンスメソッドに変更する。

これは、staticを取るだけだ。テスト実行。Green。


patternsとresponsesをインスタンス変数にとり、コンストラクタで初期化しようと思ったが、FileReaderがこの時点では入手できないことに気づいた。コンストラクタの引数で渡す形に変更して実現することにする。当然テスト側もだ。


public class PatternResponder {

private Pattern[] patterns;
private String[] responses;

public PatternResponder() {
patterns = this.loadDictionary(null);
responses = this.loadDictionaryResponse(null);
}

こっちはテスト。


public void testLoadDictionary() throws FileNotFoundException {
PatternResponder responder = new PatternResponder(new FileReader("dics/pattern.txt"));

assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった") },
responder
.loadDictionary(new FileReader("dics/pattern.txt")));
}

public void testLoadDictionaryResponse() throws FileNotFoundException {
PatternResponder responder = new PatternResponder(new FileReader("dics/pattern.txt"));

assertEquals(new String[]{"さむくないよ","食べれば","いいね"},
responder
.loadDictionaryResponse(new FileReader("dics/pattern.txt")));
}

どんどん進む。loadDictionaryとloadDictionaryResponseの中身をコンストラクタに移して、loadDictionaryとloadDictionaryResponseはそれぞれインスタンス変数を返すことにする。


public PatternResponder(FileReader reader) {
String[] patternStrings = splitPatternAndResponse(RandomResponder.loadDictionary(reader), 0);

patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
patterns = this.loadDictionary(reader);
responses = splitPatternAndResponse(RandomResponder.loadDictionary(reader), 1);
}

public Pattern[] loadDictionary(FileReader reader) {
return patterns;
}

public String[] loadDictionaryResponse(FileReader reader) {
return responses;
}

テスト実行。Red!!あれ?何か壊したか?下線部が余計だったか。削除して再テスト。やっぱりRedだ。

テスト結果を見ると、testLoadDictionaryResponse()で取得した応答例の数が異なっていることになっている。


junit.framework.AssertionFailedError: expected:<3> but was:<0>
at junit.framework.Assert.fail(Assert.java:47)

あ、そうか。readerを初期化していないから、patternの方を取得した段階で、ファイルの終わりに行ってしまっているんだな?

こう、変える。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] patternStrings = splitPatternAndResponse(loadStrings, 0);

patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
responses = splitPatternAndResponse(loadStrings, 1);
}

これでテスト実行。Green。しかも、。ファイルの読み込みは一度だけだ。

しかし、いまいち納得できない。

splitPatternAndResponseを(Eclipseのリファクタリング機能で)inline展開してみる。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] responses1 = new String[loadStrings.length];

for (int index1=0;index1 < loadStrings.length; index1++) {
responses1[index1] = loadStrings[index1].split("\t")[0];
}
String[] patternStrings = responses1;

patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
String[] responses2 = new String[loadStrings.length];

for (int index=0;index < loadStrings.length; index++) {
responses2[index] = loadStrings[index].split("\t")[1];
}
responses = responses2;
}

responses1とresponses2を無くして、patternStringとresponsesを直接使うようにする。

inline展開がうまくいかなかったので、手作業。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] patternStrings = new String[loadStrings.length];

for (int index1=0;index1 < loadStrings.length; index1++) {
patternStrings[index1] = loadStrings[index1].split("\t")[0];
}

patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
String[] responses = new String[loadStrings.length];

for (int index=0;index < loadStrings.length; index++) {
responses[index] = loadStrings[index].split("\t")[1];
}

}

テスト実行。またまた、Red。勢い余ったか。Undoして一つずつ変更する。


まずは、responses2の部分だけ。Green。そうか。responsesに置き換えたときに新たに宣言してしまっている。

この感じでpatternStringsへも置き換える。Green。

配列の大きさは結局loadString.lengthなので、newを最初に移動して、ループをまとめる。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] patternStrings = new String[loadStrings.length];
patterns = new Pattern[patternStrings.length];
responses = new String[loadStrings.length];

for (int index=0;index < loadStrings.length; index++) {
patternStrings[index] = loadStrings[index].split("\t")[0];
patterns[index] = Pattern.compile(patternStrings[index]);
responses[index] = loadStrings[index].split("\t")[1];
}
}

patternStringsは不要に見えるので削除する。

まら、Eclipseのinlineがうまく効かない。手作業である。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);

patterns = new Pattern[loadStrings.length];
responses = new String[loadStrings.length];

for (int index=0;index < loadStrings.length; index++) {
patterns[index] = Pattern.compile(loadStrings[index].split("\t")[0]);
responses[index] = loadStrings[index].split("\t")[1];
}
}

テスト実行。よしよし。Green。

splitを2回実施しているのが無駄に見えるので、一時変数を用いることにする。


public PatternResponder(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);

patterns = new Pattern[loadStrings.length];
responses = new String[loadStrings.length];

for (int index=0;index < loadStrings.length; index++) {
String[] patternAndResponses=loadStrings[index].split("\t");
patterns[index] = Pattern.compile(patternAndResponses[0]);
responses[index] = patternAndResponses[1];
}
}

これで、見た感じ重複はなさそうだ。また、機能の追加に移るか。

辞書を片手に~PatternResponderの作成(5)

それでは、
・loadDictionary()とloadDictionaryResponse()の重複処理の解消
をやってみる。

まずは、

public static Pattern[] loadDictionary(FileReader reader) {
String[] patternStrings = RandomResponder.loadDictionary(reader);
Pattern[] patterns = new Pattern[patternStrings.length];
 
for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index].split("\t")[0]);
}
return patterns;
}

下線部分を最初に取得してしまうように変更する。

public static Pattern[] loadDictionary(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] patternStrings = new String[loadStrings.length];
 
for (int index=0;index < loadStrings.length; index++) {
patternStrings[index] = loadStrings[index].split("\t")[0];
}

Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
return patterns;
}

テスト実行。OK、壊していない。
loadDictionaryResponse()の方も、使用しているローカル変数をloadDictionary()に合わせる。

public static String[] loadDictionaryResponse(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] responses = new String[loadStrings.length];
 
for (int index=0;index < loadStrings.length; index++) {
responses[index] = loadStrings[index].split("\t")[1];
}
return responses;
}

テスト実行してOK。ほとんど一緒にはなったが共通メソッドに抜き出すにはまだ微妙である。
試しに、Eclipseのメソッド抽出をかけてみる。

public static String[] loadDictionaryResponse(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] responses = splitPatternAndResponse(loadStrings);
return responses;
}

/**
* @param loadStrings
* @return
*/
private static String[] splitPatternAndResponse(String[] loadStrings) {
String[] responses = new String[loadStrings.length];
 
for (int index=0;index < loadStrings.length; index++) {
responses[index] = loadStrings[index].split("\t")[1];
}
return responses;
}

split("\t")[1]の1を引数にするとうまく共通化できそうだ。

public static Pattern[] loadDictionary(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] patternStrings = splitPatternAndResponse(loadStrings, 0);
 
Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
return patterns;
}

public static String[] loadDictionaryResponse(FileReader reader) {
String[] loadStrings = RandomResponder.loadDictionary(reader);
String[] responses = splitPatternAndResponse(loadStrings, 1);
return responses;
}

/**
* @param loadStrings
* @param num TODO
* @return
*/
private static String[] splitPatternAndResponse(String[] loadStrings, int num) {
String[] responses = new String[loadStrings.length];
 
for (int index=0;index < loadStrings.length; index++) {
responses[index] = loadStrings[index].split("\t")[num];
}
return responses;
}

いちいち書いてはいないが、1ステップずつテストでGreenを確認している。
最後に、いくつかのローカル変数をインライン化する。

public static Pattern[] loadDictionary(FileReader reader) {
String[] patternStrings = splitPatternAndResponse(RandomResponder.loadDictionary(reader), 0);
 
Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
return patterns;
}

public static String[] loadDictionaryResponse(FileReader reader) {
return splitPatternAndResponse(RandomResponder.loadDictionary(reader), 1);
}

/**
* @param loadStrings
* @param num TODO
* @return
*/
private static String[] splitPatternAndResponse(String[] loadStrings, int num) {
String[] responses = new String[loadStrings.length];
 
for (int index=0;index < loadStrings.length; index++) {
responses[index] = loadStrings[index].split("\t")[num];
}
return responses;
}

RandomResponder.loadDictionary(reader)を2回呼び出しいるところが気になるが、これはsplitPatternAndResponse()の中に移しても2回呼ぶことには変わり無いので、次のToDoを追加しておいておく。

ToDoリスト
・loadDictionary()とloadDictionaryResponse()の重複処理の解消
・loadDictionary()とloadDictionaryResponse()で、同じファイル読み込みを繰り返している(これはファイル読み込みの結果を保持しないと解決できない?)



FirefoxとThunderbird

会社のPCでブラウザとメーラーをFirfoxとThunderbirdに変えたので、自宅の環境も変える。

まあ、どうせメールなんてspamしか来ないんだけどね。

辞書を片手に~PatternResponderの作成(4)

失敗するテストが無ければ実装を進めれないので変更する。


public void testLoadDictionaryResponse() throws FileNotFoundException {
assertEquals(new String[]{"さむくないよ","食べれば","いいね"},
PatternResponder
.loadDictionaryResponse(new FileReader("dics/pattern.txt")));
}

random.txtからコピーしてpattern.txtも作成した。[→]はタブである。


今日はさむいね[→]さむくないよ
チョコたべたい[→]食べれば
きのう10円ひろった[→]いいね

テストを実行。予定通り失敗。

さて、どう実装しようか?仮実装を進めても意味が無い気がするので、既存処理を用いた安易な実装を試みる。


今のデータをRandomResponder.loadDictionary()で読み込めば、タブと応答例も含めた文字列の配列になるはず。

それを切り取れば良いんじゃないだろうか。


public static String[] loadDictionaryResponse(FileReader reader) {
String[] patternStrings = RandomResponder.loadDictionary(reader);
String[] responses = new String[patternStrings.length];

for (int index=0;index < responses.length; index++) {
responses[index] = patternStrings[index].split("\t")[1];
}
return responses;
}

あえて、タブが無いときなどは考慮にいれていない。それはToDoに追加しておこう。


テスト実行。Green。あっさり動いたな。loadDictionary()の方も、pattern.txtからの読み込みに変えてみる。


public void testLoadDictionary() throws FileNotFoundException {
assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった") },
PatternResponder
.loadDictionary(new FileReader("dics/pattern.txt")));
}

テスト実行。当然失敗である。応答例まで含んでいるからだ。パターンを作るときに、タブより前の部分のみ使うようにする。


public static Pattern[] loadDictionary(FileReader reader) {
String[] patternStrings = RandomResponder.loadDictionary(reader);
Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index].split("\t")[0]);
}
return patterns;
}

テスト実行。Green。一応、これで応答例の配列は入手できるようになった。

loadDictionary()とloadDictionaryResponse()の類似性も気になるところで、リファクタリング対象に持って来いだろう。


ToDoリスト

・正規表現を使ってパターンに反応するResponder(以下の仕様を満たす)

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される

・マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す

・応答例の中に「%match」という文字列があれば、パターンにマッチした文字列と置き換えられる

・パターン辞書から応答例のグループを作る

・パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める

・PatternResponderがRandomResponderの機能を利用している(メソッドの上位クラスへの移動に関するリファクタリングが出来そうである)

・パターン辞書に応答例が無い場合のイリーガルケース

・loadDictionary()とloadDictionaryResponse()の重複処理の解消


辞書を片手に~PatternResponderの作成(3)

ToDoリスト

・パターン辞書から応答例のグループを作る


つぎはこれだ。

今のloadDictionary()では、パターンの方しか取得することが出来ない。

応答例の方もまとめて取れるようにしたい。

loadDictionary()がTreeMapを返せば良いのでは?


まずは、テストを書く。


public void testLoadDictionary() throws FileNotFoundException {

TreeMap result1 = PatternResponder.loadDictionary(new FileReader("dics/first.dat"));
Pattern[] pat1 = null;
assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった") },
pat1);

TreeMap result2 = PatternResponder.loadDictionary(new FileReader("dics/add.dat"));
Pattern[] pat2 = null;
assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった"),
Pattern.compile("それからそれから?") },
pat2);
}

どうも長くなったので、こんな感じで一時変数を使った。まだ、コンパイルは通らない。

loadDictionary()をTreeMapを返すように変更する。


public static TreeMap loadDictionary(FileReader reader) {
String[] patternStrings = RandomResponder.loadDictionary(reader);
Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
return new TreeMap((Object[])patterns);
}

適当に書いたらEclipseに怒られたので、素直にドキュメントを探す。

ドキュメントを読むとTreeMapがキーの昇順にソートするということに今更気づく。

HashMapに変更するか?しかし、それだと順序保証がなくなるし。


ArrayListは?これは、パターンから応答例を引っ張ってくるのが大変になるし。

いや、それは今のタスクでは無い。先ずは応答例を取得しなければ・・・。


ぐだぐだ悩みすぎて、リズムが崩れたので、一旦全てを捨ててシャワーに入ることにする。


シャワーから出た。

さっきは、悩みすぎた。TDDなんだから、今はテストしやすいように考えなくては。

一番テストがしやすい方法は新規メソッドの追加だろう。既存メソッドには影響を与えない。

応答例を取得するloadDictionaryResponse()をテストしよう。

まずはStringの配列を返せばいいだろう。

public void testLoadDictionaryResponse() throws FileNotFoundException {
assertEquals(new String[0],
PatternResponder
.loadDictionaryResponse(new FileReader("dics/first.dat")));
}

こんな感じで。first.datには応答例の方が入っていないので、空のString配列が返って来るということでいいか?

いや、パターンと同数の空文字列の配列の方がいいだろう。

public void testLoadDictionaryResponse() throws FileNotFoundException {
assertEquals(new String[]{"","",""},
PatternResponder
.loadDictionaryResponse(new FileReader("dics/first.dat")));
}

QuickFixでスタブ実装。

public static String[] loadDictionaryResponse(FileReader reader) {
// TODO Auto-generated method stub
return null;
}

試しにテストを実行してみると、当然Red。よしよし。

このテストクラスには、String配列用のAssertEqualsが無いので、別のテストクラスからコピペする。

テスト実行。まだ、Red。で、仮実装。

public static String[] loadDictionaryResponse(FileReader reader) {
return new String[]{"","",""};
}


Green。ようやく進める・・・。

辞書を片手に~PatternResponderの作成(2)

ToDoリスト

・パターン辞書から正規表現のパターンオブジェクトのグループを作る


まずは、パターンオブジェクトのグループを作ってみる。

グループとは何だろう?配列か?それともTreeMapがいいのか?

TreeMapが必要だと感じるのは、この先に控えている、「パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める」を意識しているからだと思われる。


そんな先のことを意識しないとテストが考えられないというのは、着手順番を間違えた証拠かもしれない。

前言撤回して、この「パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める」から手をつけようかとも思ったが、それはそれで面倒そうだったので、とりあえず配列で考えることにする。


配列と決まった場合、このToDoをこなすには必要なのは、どうやってパターンオブジェクトの配列が出来たかをテストするのかだ。PatternクラスはAssertEqualsに対してどうやって振舞うのだろうか?


public void testAsserEqualsForPattern() {
assertEquals(Pattern.compile(".*"),Pattern.compile(".*"));
}

Red。どうやらequalsメソッドに対しては同じパターンでも別物として扱われるようだ。


pattern ()
このパターンのコンパイル元の正規表現を返します。


というメソッドがあるらしいので、それを経由してみる。


public void testAsserEqualsForPattern() {
assertEquals(Pattern.compile(".*").pattern(),Pattern.compile(".*").pattern());
}

Green。どうにか、これでうまくいきそうだ。


辞書ファイルに関しては、とりあえずRandom辞書で使ったものと同じものを再利用して、それから文字列の配列ではなく、正規表現のパターンの配列を取得するメソッドをテストする。

上のテスト結果と以前の文字列の配列をテストするassertEquals()を元にパターンオブジェクトの配列をテストするassertEqualsを作る。


public void testAsserEqualsForPattern() {
Pattern[] p1 = {Pattern.compile("1"),Pattern.compile("2")};
Pattern[] p2 = {Pattern.compile("1"),Pattern.compile("2")};
Pattern[] p3 = {Pattern.compile("1"),Pattern.compile("3")};

assertEquals(p1,p2);
assertEquals(p1,p3);
}

public void assertEquals(Pattern[] expected, Pattern[] actual) {
for (int i=0;i<Math.min(actual.length,expected.length);i++) {
assertEquals(expected[i].pattern(),actual[i].pattern());
}
assertEquals(expected.length,actual.length);
}

テストを実行する。p1とp2のテストで失敗する。予想どおりなのでtestAsserEqualsForPattern()は削除して早速テストを追加する。


public void testLoadDictionary() throws FileNotFoundException {
assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった") },
PatternResponder
.loadDictionary(new FileReader("dics/first.dat")));
assertEquals(new Pattern[] { Pattern.compile("今日はさむいね"),
Pattern.compile("チョコたべたい"), Pattern.compile("きのう10円ひろった"),
Pattern.compile("それからそれから?") },
PatternResponder
.loadDictionary(new FileReader("dics/add.dat")));
}

これは、RandomResponderのloadDictionary()のテストをコピペして改造したものだ。

まだ、PatternResponderが存在しないのでテストは実行できない。QuickFixを何回か実行して、PatternResponderを作る。


public class PatternResponder {

public static Pattern[] loadDictionary(FileReader reader) {
// TODO Auto-generated method stub
return null;
}

}

テスト実行。当然Red。

中身を実装する。

しかし、テストをRandomResponderのテストから流用したのだが、ここで思い出してもらいたい。このテストは最初からこの形だったのではなく、TDDを進めてこの形に落ち着いたのだ。

つまり、このテストでは開発を駆動するには最終形過ぎるのだ。

では、どうするのか?

一旦、このテストを削除してやり直すというのもいい手なのかもしれないが、ここでよく考えてみる。

このテストが最終形をちょっと手直ししたということは、このテストに対応した実装をちょっと手直しすると本実装になるのではないか?

つまり、PatternResponderのloadDictionary()のテストはRandomResponderのloadDictionary()のテストに毛を生やしたものを使ったのだから、loadDictionary()の実装も毛を生やせばいいのではないかと。


テストの場合は毛を生やす方法は、コピーして修正だったが、実装の場合は委譲を用いる。


まずは、毛を生やす前ので実装する。


public static Pattern[] loadDictionary(FileReader reader) {
return RandomResponder.loadDictionary(reader);
}

コンパイルエラーである。Stringの配列をPatternの配列に変換できない。

まずは1個ずつ変換するしか無いだろう。


public static Pattern[] loadDictionary(FileReader reader) {
String[] patternStrings = RandomResponder.loadDictionary(reader);
Pattern[] patterns = new Pattern[patternStrings.length];

for (int index=0;index < patterns.length; index++) {
patterns[index] = Pattern.compile(patternStrings[index]);
}
return patterns;
}

テスト実行。Green!!

なんか簡単に出来てしまったが、PatternResponderがRandomResponderのメソッドを利用するのが依存関係的に気に入らない。ToDoに追加しておく。


ToDoリスト

・正規表現を使ってパターンに反応するResponder(以下の仕様を満たす)

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される

・マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す

・応答例の中に「%match」という文字列があれば、パターンにマッチした文字列と置き換えられる

・パターン辞書から正規表現のパターンオブジェクトのグループを作る

・パターン辞書から応答例のグループを作る

・パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める

・PatternResponderがRandomResponderの機能を利用している(メソッドの上位クラスへの移動に関するリファクタリングが出来そうである)

辞書を片手に~PatternResponderの作成(1)

さて、どうやらアメブロの重さも改善されたようなので、続きです。

しかし、間を開けすぎてやる気を取り戻すのに時間が・・・。

まぁ、嫌々やっても仕方ないのですが、のんびり始めることにしましょう。



では、前回からのToDoで残っているもの。


ToDoリスト

・正規表現を使ってパターンに反応するResponder


タイトルどおり、PatternResponderという名前にする。教科書(『恋するプログラム―Rubyでつくる人工無脳 』)でのクラス名そのままだが。そういえば、最近の記事もこの本の章題をそのまま使っていたりするが、良いのだろうか?引用の要件は満たしていると思うのだが。


教科書の該当章はCHAPTER 5-4になるが、ここではそのPatternResponderの仕様について4つにまとめているので、ToDoに追加する。仕様がそのままUnitTestに出来るとは限らないが、テストの出所としては間違いない。


ToDoリスト

・正規表現を使ってパターンに反応するResponder(以下の仕様を満たす)

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される

・マッチするパターンがなかったときは、ランダム辞書からランダムに選択した応答を返す

・応答例の中に「%match」という文字列があれば、パターンにマッチした文字列と置き換えられる


いきなりToDoが難解になってしまう。しかも、ランダムが2箇所に出てきて、テストが難しそうだ。

この章でランダムを誤魔化すことが必要になりそうに思うが、とりあえず考えないことにする。


この中で、まずは手をつけられそうなのは、パターン辞書からの読み込みだろう。少なくとも、既にランダム辞書を読み込むためのコードは「辞書はファイル」に書いていたはず。


教科書は、Rubyを題材にしているため、当然実装にはRubyを使用するわけだが、Rubyの文字列オブジェクトには、『引数として渡された文字列を正規表現として、自分自身に対してパターンマッチを行う』matchというメソッドがあるらしい。


if m = input.match(re)

処理A(応答メッセージを作る)

end


この式では、inputがユーザからのメッセージ文字列で、mにはreで渡された正規表現とマッチした結果が入るそうだ。

Javaの文字列オブジェクトには、そんなメソッドは無いため、正規表現オブジェクトに対して文字列を渡して、正規表現に文字列がマッチするかどうかをチェックするのが自然だろう。


private boolean match(String targetString, String pattern) {
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(targetString);

return m.matches();
}


このメソッドでは、pが正規表現のパターンを示すオブジェクト、mがマッチ結果を示すオブジェクトのはず。

これを辞書ファイルの各行で繰り返せばいいはずだ。

つまり2番目のToDoは、次の小さなToDoに分割できる。


ToDoリスト

・パターン辞書の先頭行からパターンマッチを行い、マッチした行の応答例をもとに応答メッセージを作る

・パターン辞書から正規表現のパターンオブジェクトのグループを作る

・パターン辞書から応答例のグループを作る

・パターンオブジェクトのグループに対して順番に入力文字列がマッチするか確認し、対応する応答例を求める


ここまで考えて随分設計をしている(実装を考えている)という気がしてきた。


本当にテストに駆動させているのだろうか?


わからないが、「パターン辞書から正規表現のパターンオブジェクトのグループを作る」をやってみる。

メンテナンスが終わったようですが

信じられないぐらい重いんですけど。


Eclipseを3.1にしてみました。

LanguagePack は未だみたいなんですが、自分で使う分には別に問題ないんで。

でも、アメブロが重いんでTDDの記事は未だ書く気がしません。