2005-07-24 21:15:55

いまさらながらEclipseプラグインのお勉強

テーマ:TDD

人工無能の方をちょっと休憩して、今は、以前 紹介した「Eclipseプラグイン開発」を使用して、いまさらながらEclipseのプラグイン開発方法を勉強しています。


とりあえず、本の方はざっと全体を読んでみましたが、実際にやってみないと覚えられないだろうと感じて、チュートリアルに従うように、本の内容のまま実行していています。


なんか社会人1年生で眠気と戦うため(最初のころは放置されていたのです)にMFCのチュートリアルをそのまま実行してWindowsアプリ開発を覚えた頃を思い出しました。

そういえば、Eclipse自体にもプラグインのチュートリアルが付いていたけど、英語だったので断念・・・。


そういう意味で、日本語で丁寧に解説されているという意味で、非常に役に立ちますね。


困った点は、この本はEclipse3.0の日本語版を使用しているのに対して、今使っているのはEclipse3.1の英語版であること。ウィザードやメニューの微妙な違いはなんとかできるけど、まずそうなのは標準プラグイン(org.eclipse.*)のクラス構成が変わっている点。

この本は、Eclipseの標準機能もサンプルとして使用するため、解説されている内容と自分の環境が異なっていると迷ってしまいます。


で、Chapter7までやったところで、


Unhandled event loop exception
Reason:
org/eclipse/debug/core/ILaunch


こんなメッセージに嵌ってます。


AD
いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-23 23:00:33

辞書を片手に~PatternResponderの作成(番外編)

テーマ:TDD

前回 のタイトルが間違っていたのでこっそり修正した。


あと、最後に実装したPatternResponder.response()だが、よく見てみると、もっとシンプルに変えられそうに見えたので直す。

PatternResponder

public String response(String msg) {
for (int index=0;index < patterns.length; index++) {
Matcher m = patterns[index].matcher(msg);
if (m.find()) {
return replacePattern.matcher(responses[index].response(msg))
.replaceFirst(m.group());
}
}
return randomResponder.response(msg);
}

replacePatternは前回、

p = Pattern.compile("%match%");

としていた部分をフィールドに追い出した。毎回同じオブジェクトを生成するのも無駄だと思ったからだ。

変更のポイントは、「%match%」が応答に存在するかどうかのチェックをなくしたこと。

もともとreplaceFirst()は正規表現に一致した場合に置換するメソッドであり、一致しない場合は何もしない。

つまり、無条件にreplaceFirst()を呼び出してしまえばいいのだ。

テスト実行して、もちろんGreen。

AD
いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-22 01:00:48

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

テーマ:TDD

さすがにそろそろラストスパートである。


ToDoリスト

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

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

・Dictionaryでコンストラクタで動作を分けているのは美しくない


さて、リファクタリングもひと段落ついたと見なして、最後の機能追加である。まずは、テスト。


PatternResponderTest

public void testMatch() throws FileNotFoundException {
PatternResponder resp = new PatternResponder("pattern", new Dictionary(
new FileReader("dics/pattern.txt")));

assertEquals("カステラは太るよ", resp.response("カステラたべたい"));
assertEquals("甘いものは太るよ", resp.response("甘いものたべたい"));
}
 


「甘いもの」や「カステラ」に反応させて「~は太るよ」と反応させたいわけだ。

テスト実行。当然、Red。


じゃあ、実装だ。

まずは、pattern.txtを仕様にしたがって変更する。以下の定義を追加する。


カステラ|甘いもの[→] %match%は太るよ


テスト実行。やっぱりRedだが、結果はさっきと異なっている。

「カステラ」の定義が追加されているので「%match%は太るよ」と反応している。

この「%match%」の部分をパターンに一致した部分で置換するわけだが?

とりあえずドキュメントをみて正規表現クラスが使えそうだと分かった。


PatternResponderのresponse()を修正する。修正したのは以下の下線部分である。


PatternResponder

public String response(String msg) {
for (int index=0;index < patterns.length; index++) {
Matcher m = patterns[index].matcher(msg);
if (m.find()) {
String resp = responses[index].response(msg);
Pattern p = Pattern.compile("%match%");
Matcher m2 = p.matcher(resp);

if (!m2.find()) {
return resp;
} else {
return m2.replaceFirst(msg);
}

}
}
return randomResponder.response(msg);
}

パターンに一致した部分だけってのが良く分からなかったので、まずは、入力された文字列で置換した。

テスト実行。当然Redだが、応答は「カステラたべたいは太るよ」である。一歩前進した。


もう一度ドキュメントを探索。どうやらMatcherクラスの 以下のメソッドで一致した部分が入手できるようだ。


string group () 前回のマッチで一致した入力部分シーケンスを返します。


PatternResponder

public String response(String msg) {
for (int index=0;index < patterns.length; index++) {
Matcher m = patterns[index].matcher(msg);
if (m.find()) {
String resp = responses[index].response(msg);
Pattern p = Pattern.compile("%match%");
Matcher m2 = p.matcher(resp);

if (!m2.find()) {
return resp;
} else {
return m2.replaceFirst(m.group());
}
}
}
return randomResponder.response(msg);
}

テスト実行。今度こそGreenである。


さて、残ったToDoであるが、「Dictionaryでコンストラクタで動作を分けているのは美しくない」であるが、これを解決するためにポリモーフィズムでクラスを分割するのも大げさな気がする。

また、検索の結果、 public Dictionary(Reader reader)はテストからしか呼ばれず、しかも全て public Dictionary(String fileName)で置き換えることが可能である。

後々後悔するかもしれないが、 Dictionary(Reader reader)を削除して呼び出し元をDictionary(String fileName)で置き換えることにする。


テスト実行。OK。Greenだ。


Dictionaryのソースを眺めても、これ以上良い案が思い浮かばないので、Closeする。


ToDoリスト

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

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

・Dictionaryでコンストラクタで動作を分けているのは美しくない



とにかく、これで本章は終了である。


AD
いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-21 00:39:52

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

テーマ:TDD

まだまだリファクタリングを続ける。


ToDoリスト

・PatternReponderのloadDictionaryPattern()とloadDictionaryResponse()がテストからしか呼ばれていない


本当に、これらのメソッドがまだ必要なのかを検討する。

とくにこれらを呼んでいるテストが変われば、さらに不要なコンストラクタが削減される可能性がある。


まずは、loadDictionaryPattern()のテスト。


PatternResponderTest:

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

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

この使用方法からloadDictionaryPattern()を簡単に削除しようとしたら、patternsを公開するぐらいしか思いつかない。

loadDicrionaryResponse()のテストも見てみる。


PatternResponderTest:

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

assertEquals(
new RandomResponder[] {
new RandomResponder("", new Dictionary(new String[] {
"さむくないよ", "そうだね" })),
new RandomResponder("", new Dictionary(
new String[] { "食べれば" })),
new RandomResponder("", new Dictionary(
new String[] { "いいね" })) }, responder
.loadDictionaryResponse());
}

現状では、PatternとResponseをばらばらにテストしている。これをパターン-応答例をセットでテストすることにして、loadDictionary*()は削除することにしても良いが、そうするとこのクラスは適当な入力に対して適切な応答を返すかどうかでしかテストできなくなる。

つまり、現状は内部仕様をテストしているが、それを一切見ないようにするかどうかを判断する必要にせまられているわけだ。


内部仕様を隠蔽することを選んだ場合は、単純に上記テストを削除するだけだが、それで不安はないだろうか?


自問自答した結果、全ての入力に対する応答をカバーするテストがあるならば不安じゃないんじゃないか、と考える。既に存在するテストを眺めた結果、正常系のresponse()のテストが、


assertEquals("さむくないよ", resp.response("今日はさむいね"));

しか存在しないことに気づいた。テストを拡充する。


assertEquals("さむくないよ", resp.response("今日はさむいね"));
assertEquals("そうだね", resp.response("今日はさむいね"));
assertEquals("食べれば", resp.response("チョコたべたい"));
assertEquals("いいね", resp.response("きのう10円ひろった"));

これでテスト実行。Green。不安が減った気がするので、loadDictionary*()のテストを削除する。

その結果、呼び出されることがなくなったメソッドを削除する。



いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-20 01:15:13

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

テーマ:TDD

機能を追加する前にリファクタリングを片付ける。


・RandomResponderのコンストラクタを整理する


他にも気になったところ(↓)もついかしておく。


・PatternReponderのloadDictionaryPattern()とloadDictionaryResponse()がテストからしか呼ばれていない

・ResonderがloadDictionary()を保持しているのは、しっくりこない

まずは、コンストラクタの方。これは簡単だ。

どこからも呼ばれていないコンストラクタを一旦削除してみればいい。


と思ったが、全部使われている。簡単じゃなかったみたい。


全部、いっぺんに解決しよう。Dictionaryというクラスを導入して、reader,filename,responsesを渡しているのを一掃し、Responder間のコンストラクタの共通化を図る。


まずは、ResponderがDictionaryを保持するように作る。


public class Dictionary {

private Reader reader;

public Dictionary(Reader reader) {
this.reader = reader;
}

public String[] load() {
BufferedReader r = new BufferedReader(reader);
try {
ArrayList response = new ArrayList();
while (true) {
String line = r.readLine();
if (line == null) break;
response.add(line);
}
return (String[])response.toArray(new String[]{});
} catch (IOException e) {
return new String[] {};
}
}
}
 

このクラスをResponder.loadDictionary()で使用するようにする。


public class Responder {
public static String[] loadDictionary(Reader reader) {
Dictionary dictionary = new Dictionary(reader);
return dictionary.load();
}
}


テストを実行する。All Green。大丈夫壊していない。


次にRandomResponderとPatternResponderのコンストラクタにDictionaryを渡せるようにする。


RandomReponderのほうはコンストラクタが一杯あるせいで、どれを修正したらいいのか迷うな。

とりあえず、すべてにつけてみる。

だが、これだと全ての呼び出し元の方にも、Dictionaryを付けなければならない。

まずは、readerのみを置き換えてみる。

既存でReaderを受け取っているコンストラクタをDictionaryを受け取るように変更して、既存でReaderを渡している方はDictionaryを生成して渡すようにする。

最後に、loadDictionary(reader)をdictionary.load()に変更する。


テスト実行。Green。

次はString[]の置き換えをおこなう。DictionaryのコンストラクタでString[]を受け取れるようにする。


public class Dictionary {

private String[] lines = null;

public Dictionary(String[] lines) {
this.lines = lines;
}

public String[] load() {
if (lines != null) return lines;
...

}


nullと比較している部分は気になるが、とりあえずToDoに追加しておいて、今度は、RandomResponderのコンストラクタでString[]を渡しているものをnew Dictionary(String[])を渡すように変更する。

テスト実行。OK。


RandomResponderのコンストラクタからString[]を引数に持つものを削除する。

テスト実行。OK。

最後はfilenameを引数にとるコンストラクタだ。同じようにDictionaryの方にコンストラクタを追加して。

public class Dictionary {

...

public Dictionary(String fileName) {
try {
reader = new FileReader("dics/"+fileName);
} catch (FileNotFoundException e) {
reader = new StringReader("\n");;
}
}
}


同様に呼び出し元を置き換える。

テスト実行。OK。


PatternResponderの方も同様に、Reader→Dictionaryへ置き換える。


最後にResponder.loadDictionary()を削除する。


ResponderTestでコンパイルエラーになるが、このテストケースは既にloadDictionary()のテストしかしていないため、テストケース名をDictionaryTestに変更して、試験内容を変える。


テスト実行。当然、Green。


ToDoリスト

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

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

・RandomResponderのコンストラクタを整理する

・PatternReponderのloadDictionaryPattern()とloadDictionaryResponse()がテストからしか呼ばれていない

・ResonderがloadDictionary()を保持しているのは、しっくりこない

・Dictionaryでコンストラクタで動作を分けているのは美しくない


いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-18 01:47:21

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

テーマ:TDD

操作ミスで、書きかけが消えてしまいました。

気力も尽きたが、TDDの過程は二度と復旧できないので、概要だけ記述します。


「1つのパターンに対して応答例は「|」で区切って複数設定でき、いずれかがランダムに選択される」を実施しようとしたが、現状では応答例は一つしか保持できない。

そこで、応答例を複数保持するオブジェクトとしてRandomResponderを使うことを思いつく。

まずは、RandomResponderを無理やり利用するために、RandomReponderのnameフィールドを応答例の保持フィールドとして使用した。

次に、RandomRespondeを利用しやすいように「RandomResponderのコンストラクタでファイル名(String)でなくファイル(Reader)を受け取るようにする」を実施する。

しかし、それよりも「応答例の配列(String[])をコンストラクタで受け取れる」方が利用しやすいことに気づき、そのコンストラクタを追加する。

RandomResponderのコンストラクタがごちゃごちゃになっているが、あとでなおすことにして実装完了。


最終的に次のようになった。


public class RandomResponder extends Responder {

String [] resps;
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);
try {
initialize(new FileReader("dics/"+fileName),rnd);
} catch (FileNotFoundException e) {
initialize(new StringReader("\n"),rnd);
}
}
public RandomResponder(String name,Reader reader,Random rnd) {
super(name);
initialize(reader,rnd);
}
public RandomResponder(String name,String[] responses) {
this(name,responses,new Random());
}
public RandomResponder(String name,String[] responses,Random rnd) {
super(name);
this.rnd = rnd;
resps = responses;
}

private void initialize(Reader reader,Random rnd) {
this.rnd = rnd;
resps = loadDictionary(reader);
}
/* (non-Javadoc)
* @see proto.Responder#response(java.lang.String)
*/
public String response(String msg) {
return resps[rnd.nextInt(resps.length)];
}
}

public class PatternResponder extends Responder {

private Pattern[] patterns;
private RandomResponder[] responses;
private Responder randomResponder;

public PatternResponder(String name, Reader reader) {
this(name,reader,new Random());
}
public PatternResponder(String name, Reader reader,Random rnd) {
super(name);
randomResponder = new RandomResponder(name,rnd);

String[] loadStrings = loadDictionary(reader);

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

for (int index=0;index < loadStrings.length; index++) {
String[] patternAndResponses=loadStrings[index].split("\t");
patterns[index] = Pattern.compile(patternAndResponses[0]);
if (patternAndResponses.length > 1) {
responses[index] = new RandomResponder(name,patternAndResponses[1].split("
\\|"),rnd );
}else{
responses[index] = new RandomResponder(name,new String[]{""},rnd);
}
}
}
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].response(msg);
}
}
return randomResponder.response(msg);
}

public Pattern[] loadDictionaryPattern() {
return patterns;
}

public RandomResponder[] loadDictionaryResponse() {
return responses;
}

}

public class PatternResponderTest extends ResponderTest {

.....

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

assertEquals(new RandomResponder[] {
new RandomResponder("", new String[] { "さむくないよ" ,"そうだね"}),
new RandomResponder("", new String[] { "食べれば" }),
new RandomResponder("", new String[] { "いいね" }) }, responder
.loadDictionaryResponse());
}

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

assertEquals(new RandomResponder[] {
new RandomResponder("", new String[] { "" }),
new RandomResponder("", new String[] { "" }),
new RandomResponder("", new String[] { "" }) }, responder
.loadDictionaryResponse());
}

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

assertEquals(new RandomResponder[] {
new RandomResponder("", new String[] { "test1" }),
new RandomResponder("", new String[] { "" }),
new RandomResponder("", new String[] { "test3" }) }, responder
.loadDictionaryResponse());
}

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

......

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


ToDoリスト

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

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

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

・RandomResponderのコンストラクタでファイル名(String)でなくファイル(Reader)を受け取るようにする

・RandomResponderのコンストラクタを整理する


いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-18 01:18:59

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

テーマ:TDD

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

当然テストを作る。


public class PatternResponderTest extends ResponderTest {

public void testRandomResponse() throws FileNotFoundException {
PatternResponder resp = new PatternResponder("pattern", new FileReader("dics/pattern.txt"));
assertEquals("今日はさむいね",resp.response("今日はさむくないね"));
assertEquals("チョコたべたい",resp.response("今日はさむくないね"));
assertEquals("きのう10円ひろった",resp.response("今日はさむくないね"));
}
}


パターンにないメッセージを送ったら、ランダム辞書の一番目から順に応答を返すテストである。

念のためテストを実行。Red。


PatternResponderのコンストラクタがRandomを受け取るようになっていないので、さっき作ったFakeRandomIntを使用できない。コンストラクタに追加する。


public PatternResponder(String name, Reader reader) {...}
public PatternResponder(String name, Reader reader,Random rnd) {..}

テストの方にもFakeRandomIntを追加するが、当然これでもテストは通らない。PatternResponderは元々Randomを使用していないのだからである。


それでは、早速コンストラクタで手に入れたRandomを使用してランダム辞書から応答を返すわけだが、『ランダム辞書から応答を返す』というのはどういうことだろうか?


それは、RandomResponderのように振舞うということである。


では、RandomResponderのように振舞うのに一番簡単な方法はなんだろうか?


それは、RandomResponderのインスタンスを保持しておいて、必要なときに処理を委譲するというのが簡単なのでは無いだろうか?


そのように処理を追加する。

まずは、コンストラクタでインスタンス変数にRandomResponderのインスタンスを保持する。


public class PatternResponder extends Responder {

private Responder randomResponder;

public PatternResponder(String name, Reader reader,Random rnd) {
super(name);
randomResponder = new RandomResponder(name,rnd);
...


次に、今まで一致するパターンが無いときに空文字列を返していたところの処理をRandomResponderに委譲する。


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 randomResponder.response(msg);
}

テスト実行。Green!!!一発完動である。


ここで気づいたのだが、RandomResponderは辞書のファイル名をコンストラクタで受け取るのに対し、PatternResponderは辞書ファイル自体(Reader)を受け取る。

PatternResponderの異常データ系テストでそれを利用してファイルを作らずにテストケースを作成したように、PatternReponderの形式の方が優れているように思える。

ToDoに追加しておく。


ToDoリスト

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

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

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

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

・RandomResponderのコンストラクタでファイル名(String)でなくファイル(Reader)を受け取るようにする


いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-17 23:56:07

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

テーマ:TDD

辞書を片手に~辞書はファイルに(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、これでランダムを制御する道具を手に入れた。




いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-17 23:05:06

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

テーマ:TDD

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


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の機能を利用している(メソッドの上位クラスへの移動に関するリファクタリングが出来そうである)

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



いいね!した人  |  コメント(0)  |  リブログ(0)
2005-07-17 00:48:27

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

テーマ:TDD

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];
}
}

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

いいね!した人  |  コメント(0)  |  リブログ(0)

AD

ブログをはじめる

たくさんの芸能人・有名人が
書いているAmebaブログを
無料で簡単にはじめることができます。

公式トップブロガーへ応募

多くの方にご紹介したいブログを
執筆する方を「公式トップブロガー」
として認定しております。

芸能人・有名人ブログを開設

Amebaブログでは、芸能人・有名人ブログを
ご希望される著名人の方/事務所様を
随時募集しております。