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

いきなりタイトルを変えるってどうなんだろう?

新しいブログを開設するのも面倒なので、冬眠状態のここを別のテーマで復活させたわけだが、読者もいるわけじゃないしいいかね。
リファクタリングってことで。

忙しいのと暑いのと

仕事が忙しくて、今日は久しぶりに午前様回避でした。


とはいえ、暑くてTDDをする気になれません。

TDDの最大の敵は暑さだといっても過言では無いかもしれません。


ところで前回書いたように、一旦人工無能の方は休憩してEclipseプラグインを勉強しているわけですが、前回紹介した本の方は難しかったので一旦(こっちもか)置いといて、次の本で勉強していたりします。


清水 美樹
Eclipseプラグイン入門―〈Java IDE〉便利な機能を簡単に追加!
Eclipseプラグイン開発 の方に比べると、話の進め方は素直です。
入門から始めるには「Eclipseプラグイン入門 」の方が易しいと思います。

まあ、ちょびちょびやっているので、こっちも全然進んでいないわけですが。


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

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


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


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

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


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


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

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


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


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


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


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

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


あと、最後に実装した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。

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

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


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でコンストラクタで動作を分けているのは美しくない



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


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

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


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*()のテストを削除する。

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



Eclipseプラグイン開発

Erich Gamma, Kent Beck, 小林 健一郎
Eclipseプラグイン開発

上記、書籍を買いました。

プラグインというかRCPのアプリを書こうと思って、Webでの情報では痒いところに手が届かないというか、入門には難しそうだったので、基本的なところから始めようと購入しました。

実は、副題が「デザインパターン×テスト駆動開発」となっていて、テスト駆動開発の方に引かれたってのもあります。


人工無能のGUI版の方をEclipseプラグインorRCPで実装する方向にいくかもしれません。

プラグインのほうが面白いよねきっと。


Webの方で、よくまとまっていると思ったのは、

Eclipse/プラグイン開発のTIPS集

でした。

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

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


・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でコンストラクタで動作を分けているのは美しくない


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

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

気力も尽きたが、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のコンストラクタを整理する


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

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

当然テストを作る。


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)を受け取るようにする