辞書を片手に~辞書はファイルに(2)
さて、前回は眠くなっての途中放棄だったが、再開する。
前回失敗するテストを追加するところまでだったが、早速このテストを通るように実装する。
private String[] loadDictionary(String fileName) {
if (fileName.equals("first.dat")) {
return new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"};
}else{
return new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"};
}
}
自分で書いといてなんだが結構無茶苦茶である。
いったいいつまでこれを繰り返せばいいのだろう。いまだにファイルが出てこない。
TDDを実践して最初に嵌ったのが、これである。いつ仮実装(テストを通すためだけの実装)から、本実装(問題に対して一般化した本当の実装)に移れば良いのだろうか?
ここで、リファクタリングに入ることで本実装に移るという方法もある。実際、ここ段階で、リファクタリングするのに十分な量の重複したコード(テストデータ)は出現している。
しかし、このままリファクタリングしたとすると、ファイル名の文字列の代わりに、データの配列を渡すように修正している自分を想像してしまうのであった。やっぱりファイルが出てこない。
このように仮実装から本実装に移るタイミングが分からなくなるときは、最初のテストが一般的過ぎる場合が多い。多分、ある程度経験のあるプログラマーなら無意識にやってしまうと思うのだが、最初から問題に対する解答である実装を一般化してイメージし過ぎなのである。そのため、テストも一般化した状況をイメージしてしまい、適切に開発を駆動できないのである。
迷ったら戻るのがTDDの作法である。自分が何を作りたかったのか、否、どんな機能が欲しかったのか考え直してみる。
「応答例を追加するのにいちいちソースを直すのはかったるい」ので、これを外部ファイルから読めるようにする。
そう、ファイルから辞書を読みこむ機能を作ろうとして、「ファイルに限らず」辞書を読み込む機能のテストを作ってしまったのである。
ということは、テストにファイルは必須条件である。ファイル名ではなくファイルだ。
テストを書き直す。
public void testLoadDictionary() throws FileNotFoundException {
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"}, loadDictionary(new FileReader("first.dat")));
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
loadDictionary(new FileReader("add.dat")));
}
JavaにおけるFileクラスはファイル名と、ほぼ同義のクラスである。ファイル自体を示すクラスが見当たらなかったのでファイル読み込みを示すFileReaderクラスを使用するように変更した。
FileNotFoundException例外が発生する可能性がある、とEclipseが怒るのでテストメソッドにthrowsを追加する。大丈夫JUnitフレームワークがちゃんと捕捉してくれる。
仮実装の方も変更する。
private String[] loadDictionary(FileReader file) {
if (file.equals("first.dat")) {
return new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"};
}else{
return new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"};
}
}
StringからFileReaderに変更し、引数名もfileNameからfileに変更した。
分岐条件は名前変更以外はそのままである。テストを実行しても当然通らないだろうが、FileReaderからファイル名を得る簡単な方法が思いつかなかったので。
ここでテストを実行する。Red!!。しかし、予想外のポイントである。
java.io.FileNotFoundException: first.dat (指定されたファイルが見つかりません。)
at java.io.FileInputStream.open(Native Method)
うん。ファイルを作るのを忘れていた。先ずは、ファイルを作る。空ファイルである。
Eclipse上からJUnitを実行する場合、カレントディレクトリが現在のProjectの直下になるため、そこにファイルを作成する。もう一度テスト。
今度は予想どおりの場所で失敗する。
junit.framework.AssertionFailedError: expected:<3> but was:<4>
..........................
at proto.FileReaderTest.assertEquals(FileReaderTest.java:40)
at proto.FileReaderTest.testLoadDictionary(FileReaderTest.java:23)
これは一つ目のテストである。FireReaderのインスタンスが文字列と等しくなるわけがないので、常にFalseのルートを通り2つ目のテスト用の応答を返してしまう。
さて、これで本実装に移るのに十分なテストを手に入れたと思う。
ここでちょっとずるをして、ファイルに格納するフォーマットは『恋するプログラム―Rubyでつくる人工無脳 』と同様にする。自分で考えたとしても同じになったかもしれないが、この点はあまり重要でない気がしたので、あまりこだわりもなく決定する。
>辞書ファイルは1行に1つの応答メッセージといつ簡単なフォーマットとし、dicsフォルダにrandom.txtという名前で置くことにします。
おっと、ひとつ仕様を見落としていたようだ。辞書ファイルの置き場をdicsフォルダに変更する。
まずはテストを変更する。
public void testLoadDictionary() throws FileNotFoundException {
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"}, loadDictionary(new FileReader("dics/first.dat")));
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
loadDictionary(new FileReader("dics/add.dat")));
}
テスト実行。Red。dicsフォルダを作成して、ファイルの置き場を変更する。Redのまま。
ここでひとつTDDに違反してしまった。
このファイル格納位置変更は機能追加に当たるので、テストがGreenの状態でなければやってはいけないのだ。
そのため、今回はテストで失敗したときの失敗内容から、変更がうまくいったかを判断しなくてはならず余計なリスクとなるのだ。
では、どうすればいいかというと、作業途中で見つけた、仕様忘れや追加テストはToDoリストに追加して、後で実施する必要があるのだ。次回からは、ちゃんとそうしなければ、と心に誓う。
辞書を片手に~辞書はファイルに(1)
さて、いよいよ5章(CHAPTER5-1)である。
ここでは、ランダムに選択する応答例をファイルに保存して読み込めるようにするということをやっている。
とはいっても『恋するプログラム―Rubyでつくる人工無脳 』を読んでいない方にすれば、「ランダムに選択する応答例ってなんだ?」といった感じだろう。
前々回の記事 ではコードすら書いてない
>2) 予め内部に用意した複数の応答のうちのひとつをランダムに返す
のことである。
ちなみに"今日はさむいね","チョコたべたい","きのう10円ひろった"の3種類の応答をランダムに返す。
実際に、この応答はStringの配列に保持しているだけだが、「応答例を追加するのにいちいちソースを直すのはかったるい」ので、これを外部ファイルに持たせるというのが本章の主旨である。
では、さっそくTDDである。
当然、本章にはRubyのコードも外部ファイルの仕様も書かれているわけだが(しかも、そこは当然読み終わっているのだが)、そういうことを考えてはいけないのがTDDである。
ファイルから配列にデータを読み込めば良いのだから、
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"},
loadDictionary("first.dat"));
こんな感じか?
とうぜんloadDictionary()なんてないのでstubを追加。
private String[] loadDictionary(String fileName) {
return null;
}
ここでテストを実行すると、当然Red!!すぐもっとも簡単な実装を行う。
private String[] loadDictionary(String fileName) {
return new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"};
}
もう一度テストを実行する。予想に反して(嘘です、ホントは予想どおりです)Redのまま。
JUnitのassertEqualsは浅い比較しか行わないので、異なるStringの配列だということで比較失敗になるのだった。
これは、配列用のassertEqualsを書くことで解決するが、毎回毎回書かなきゃならないのは、面倒な話である。intはStringの配列用のassertEqualsが標準になればいいのにと思う。
以下を追加。
public void assertEquals(String[] expected, String[] actual) {
assertEquals(expected.length,actual.length);
for (int i=0;i<actual.length;i++) {
assertEquals(expected[i],actual[i]);
}
}
これでようやくGreen!!
続いて次のテストを追加する。
元々の要求は「応答例を追加するのにソースを直すのはかったるい」なので、ファイルを編集するだけで応答例を追加できるようにする、ということだ。テストでは2種類のファイルを用意して(まだ用意していないが)実施することにする。
assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
loadDictionary("add.dat"));
えーと、すみません。眠くなったので、続きは次回・・・。
ちなみに、現段階でテストを実行すると、またRedになる。
本来、TDDではRed状態を残したまま作業を終えてはいけないのであるが、中断するときは次回の作業開始ポイントを明示するために、わざとRedで終える方法がある。
こうすることで作業再開時に早急に中断前の作業に戻ることが可能になる。
目の前のRedをどうクリアしていくか、から考え始めればよいからだ。
ということで、Redを残したまま、一旦中断。
あこがれのGUI
4章である。
ここでは、素敵なGUI版の人工無能プログラムを作っている。
ここは、ざっくり飛ばすことにする。
前回の記事でも書いたように、人間用のインタフェース、特にGUIはテストが難しい。
やってやれないこともないが、それでは人工無能の作成とは本質的に異なる題材になってしまう。
まあ、ある程度完成して、きれいな見た目が欲しくなったら考えることにする。
そのときTDDでGUIを実装するかどうかもそのときに。
プロトタイプ人工無能「proto」
さて、教科書も3章に入りいよいよ人工無能の作成を開始する。
この章ではユーザからの入力に対して
1) 「○○ってなに?」という応答を返す
2) 予め内部に用意した複数の応答のうちのひとつをランダムに返す
という機能をもつ人工無能を作成している。
前の記事 では「テストを書く前に設計をしてはいけない」とえらそうに書いているが、今回の題材のように別の言語で実装済みのものを対称にする場合はちょっと難しい。
今後の対応付けを簡単にするために、クラス構成は出来るだけ教科書のコードと一致する方針でいく。
で、1)、2)の機能とも、Responderというクラスが担う機能である。
1)の部分のテストはこうなっている。
public void testResponse() {
WhatResponder resp = new WhatResponder("proto");
assertEquals("Aってなに?",resp.response("A"));
assertEquals("proto",resp.name());
}
全然、テストに駆動されていないというか、完全に単なるテストファーストである。
Reponder側のコードは以下になっている。
public class WhatResponder extends Responder {
public WhatResponder(String name) {
super(name);
}
public String response(String msg) {
return msg+"ってなに?";
}
}
すこしはテスト駆動っぽいところと言えば、上のクラスは最初Responderという名前だったのに対して、2)のクラスを追加するにあたりスーパークラス(現Responder)に共通部分を抜き出したところだろう。
Responderクラスのコードは、以下のようになっている。
public class Responder {
String name;
public Responder() {
this("");
}
public Responder(String name) {
this.name = name;
}
public String response(String msg) {
return "";
}
public String name() {
return name;
}
}
2)の機能については、テストすら書いていない。これは、ランダムを簡単にUnitTestで扱う方法を思いつかなかったことと、目の前にRubyの簡潔なコードがあるため、Rubyのコードをちょっと手直しするだけで十分だと判断したためだ。
どちらにせよ、現在のバージョンは通過点に過ぎずあまり力を入れてもしかたないので、現状でOKとする。
今後、ランダム応答機能にもう少し機能を追加する必要が生じたら、ランダム部分と他部との分離等も含めて考えよう。
実際、この章で難しかったのは、U/I部分である。
Rubyバージョンでは、入力処理はこう書いてある。
print('> ')
input = gets
input.chomp!
単に標準入力と標準出力を使うだけだが、Javaで標準入力を使ったことがあまりなかったため、やり方が良く分からなかった。
クラスライブラリから必要なクラスを探し出すのが一番手間取った。
getsに相当するクラスを試すために、標準入力から一行ずつ取得する機能のテストコードが以下。
public void testGetLine() throws IOException {
String target ="aaa\nbbb\n";
BufferedReader br = new BufferedReader(new StringReader(target));
assertEquals("aaa",br.readLine());
}
BufferedReaderの使用方法の確認みたいなコードだが、実際にそのとおりである。
最初は、こう書いていた。
assertEquals("aaa\n",br.readLine());
しかし、テストが通らなかったため、こう変更した。
assertEquals("aaa",br.readLine());
この後、chomp!に相当するクラスorメソッドを探そうとしていたが、readLineが行末の改行を含まないことが分かったため、その必要もなくなった。
つまり、BufferedReaderはgets.chompに相当するということだ。
最後にprotoクラスに分離したU/I部分を示す。
public class Proto {
public static void main(String[] args) {
System.out.println("Unmo System prototype : proto");
Unmo proto = new Unmo("proto");
BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("> ");
try {
String input = r.readLine();
if (input.equals("")) break;
String reponse = proto.dialogue(input);
System.out.println(Unmo.prompt(proto)+reponse);
} catch (IOException e) {
e.printStackTrace();
}
}
}
なんだか適当に書いているが、これからTDDで進めていく上ではあまり使用しないと思われるので、これで十分だろう。人間用のU/Iはテストしづらいし、このクラスの代わりをテストが務めることになるからだ。
TDD(テスト駆動開発)とテストファーストの違い
初トラックバックはフィンローダさんからでした。
>Test first と TDD の違いが分からなかった。
とのことなので。
自分で紹介しておいてなんですが、確かに@ITの記事『「テスト駆動開発」はプログラマのストレスを軽減するか?』 はテストファーストとの違いを正確に表していないとおもいます。TDDの雰囲気は『テスト駆動開発入門 』(著者: ケント ベック, Kent Beck, 長瀬 嘉秀, テクノロジックアート)を読むのが一番正確だとは思いますが、簡単に私の考えるTDDを書いて見ます。
- 基本的にはどちらもテストを先に記述(実行)するというところは共通なのですが、『どの工程よりも先に行うのか』という点に着目すると分かりやすいと思います。
・テストファーストの場合
実装(「製造」といった方が分かりやすい?)より先にテストを記述し実行する。
・TDDの場合
設計より先にテストを記述し実行する。
ここで言う設計は設計書などのように紙におこしたものだけでなく、頭の中で行うものも含みます。
たとえば、『テスト駆動開発入門 』にはフィボナッチ数を計算するメソッドをTDDでどうやって導き出すか?という内容が載っています。
テストファーストの場合は、フィボナッチ数を求めるメソッドの実装の前にテストメソッドを記述します。
まずテストが(まだ実装していないメソッドを使用することで)失敗することを確認してから、フィボナッチ数の定義を調べて計算するメソッドを実装しテストが成功することを確認します。
TDDの場合は、テストメソッドの記述までは同様ですが、メソッドを実装する部分が異なります。
厳密なTDDでは失敗するテストなしには、新たな機能を記述できません。
つまり書いたテストを成功させる最低限なコードしか記述できません。そのため、十分な機能がないと考える場合は、その足りない機能が実装されていないことを証明するテストを追加しなければなりません。
通常テストを成功させるためだけに実装してしまうと、例外コードの嵐になります。
例:フィボナッチ数を求める関数だと、0番目は0、1番目は1、2番目は1、・・・n番目はmを返す。というコード
通常の開発だと、それをなんとかするために前もって設計することが重要になるのですが、TDDの場合は前もって設計を行ったりしてはいけません(※)。
ではいつ設計を行うかというと、TDDのレッド-グリーン-リファクタリングのサイクル自体が設計になっているのです。
テストがレッド(失敗)になることが機能の不十分な部分(新たに開発するべき部分)を示し、テストのグリーン(成功)がその機能が盛り込まれたことを示します。そして、リファクタリングが内部構造の一般化を進めます。
ちょっと長くなりすぎたように感じるので強引にまとめると、テストファーストは『実装される前にテストを行うというテスト手法』であるのに対して、TDDは『テストとリファクタリングを用いて設計するという設計手法』であると言えます。
※テストの前に内部設計を考えると、定規で手を叩かれます。
バックアップ
みなさん、公開しているブログのバックアップってどうしているんでしょうか?
昔、ホームページなんか作っていたときは、ローカルに全データを保存してFTPでアップロードだから、自分のPC上のデータのバックアップを考えればいいだけでした。でも、こういうブログサービスを利用して、せっかく書き溜めた記事を別の場所で公開したくなったり、またはサービス停止等で移動を余儀なくされた場合、どうするんでしょう?
とりあえず、まだ記事の量もたいしたこともないので考えないことにするが、良いツールとかあったりするんでしょうか?
制限時間10分でプログラミング
ためしにやってみました。コメントとして書こうとしたら1000文字制限だとかで書けなかった。
トラックバックの練習も兼ねてこっちで書くことに。
ちなみに、EclipseのローカルヒストリーによるとUT含めてジャスト10分(ファイル生成からAllGreen且つリファクタリング完まで)。ただし、最後に例題に合わせて名前を変更したり、フォーマット掛けたりした時間は除いて。
/*
* 作成日: 2005/06/06
*
* この生成されたコメントの挿入されるテンプレートを変更するため
* ウィンドウ > 設定 > Java > コード生成 > コードとコメント
*/
package tramp;
import junit.framework.TestCase;
/**
* @author mar
*
* この生成されたコメントの挿入されるテンプレートを変更するため ウィンドウ > 設定 > Java > コード生成 > コードとコメント
*/
public class Cards extends TestCase {
public static void assertEquals(String[] expected, String[] actual) {
assertEquals(expected.length, actual.length);
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], actual[i]);
}
}
public void testDeal() {
assertEquals(new String[] { "123" }, deal(1, "123"));
assertEquals(new String[] { "1", "2", "3" }, deal(3, "123"));
assertEquals(
new String[] { "12", "23", "31", "12" },
deal( 4, "123123123"));
assertEquals(
new String[] { "000", "111", "222", "333", "444", "555" },
deal(6, "012345012345012345"));
assertEquals(new String[] { "", "", "", "", "", "" }, deal(6, "01234"));
assertEquals(new String[] { "", "" }, deal(2, ""));
}
/**
* @param numPlayers
* @param deck
* @return
*/
private String[] deal(int numPlayers, String deck) {
String[] result = new String[numPlayers];
for (int i = 0; i < result.length; i++) {
result[i] = "";
}
for (int i = 0; i < deck.length(); i++) {
result[i % numPlayers] += deck.substring(i, i + 1);
}
for (int i = 0; i < result.length; i++) {
result[i] = result[i].substring(0, result[result.length - 1]
.length());
}
return result;
}
}
ID
ブログを開くにあたってIDとか悩んだんだけど、結局昔から使っているハンドルを使うことにした。
でも、3文字のありがちなハンドルのせいで大概のサービスで先約がいる。
ハンドルを変えればいいんだろうけど、前に一度やったときは非常に違和感があってすぐ戻してしまった。
結局はIDなんて所詮IDなんてことで、いつもの回避策にした。
自分のハンドル-サービス名
大抵のサービスで記号が使えるので応用可能である。
不思議と被らない。
ハッシュを使おう
題材を『タイトル: 恋するプログラム―Rubyでつくる人工無脳 』にしたわけだけど、この本の構成として第1章が人工無能の説明とRubyのインストール方法、第2章が最低限のRubyの文法の説明になっている。
これをJavaでやるわけだからその辺りは飛ばしていいんだけど、RubyをJavaで移植するという意味で、Rubyの文法は抑えておかなければならない。
そうしたわけで今回はCHAPTER2-8のハッシュをやってみました。
まずは文中からのコードでRubyではハッシュをこう書く。
sound = {
'ジャブ' => '「バシッ」',
'ストレート' => '「ビシィッ」',
'ローキック' => '「ベチィッッ」',
}
これだけで、sound['ジャブ'] == 'バシッ'になるのだから大したものだなぁ。
Javaでハッシュはどう書くのかというと、Hashとか名前がついたクラスがあるのは知っているが実際に使ったことはあまりない。大抵配列で済ましちゃうからだ。
とりあえず使ってみようということで、こんなテストを書く(まずテストを書くのがTDDの作法)。
public void testHash(){
assertEquals("「バシッ」",sound("ジャブ"));
}
Rubyでは配列っぽかった記述が関数になっちゃっているけど、この関数がRubyのハッシュっぽく振舞えば移植する上では問題ないわけだ。
まずは空関数を定義する。
private String sound(String string) {
// TODO Auto-generated method stub
return null;
}
Eclipseでは「即時修正」ってコマンドで「soundメソッドの追加」って出てくるから非常に楽。
当然、ぬるぽで落ちるに決まっているので、即修正する。
private String sound(String string) {
return "「バシッ」";
}
これでテストは通るようになったけど、こんなものは全然ハッシュでないので、2個目のテストを追加する。
public void testHash(){
assertEquals("「バシッ」",sound("ジャブ"));
assertEquals("「ビシィッ」",sound("ストレート"));
}
これでなんとかしてハッシュを実装しなければならない羽目になったわけだ。
さくさくハッシュを使ってみることにする。
private String sound(String string) {
Hash hash = new Hash();
hash.add("ジャブ","「バシッ」");
hash.add("ストレート","「ビシィッ」");
return "「バシッ」";
}
最初はこんな風に適当にHashってクラスを使おうとしたのだが、コンパイルエラーになりEclipseの補完リストに出てこないので諦める(当たり前)。そこでJavaのAPIリファレンスで調べることにする。
HashMap ってのが今回の目的に合いそうだった。同期化がどうとか書いてあるがとりあえず当面気にしないことにして実装してみる。
private String sound(String string) {
Map hash = new HashMap();
hash.put("ジャブ","「バシッ」");
hash.put("ストレート","「ビシィッ」");
return "「バシッ」";
}
こうしてみると、最初書いたのとあまり変わらないなぁ、と思いつつテストを実行する。
ありゃ?やっぱり2個目のテストで失敗。よく見ると戻り値を変えていない。
private String sound(String string) {
Map hash = new HashMap();
hash.put("ジャブ","「バシッ」");
hash.put("ストレート","「ビシィッ」");
return hash.get(string);
}
もう一度実行する。今度はコンパイルエラーが出ている模様。
無作法な気もするが無理やりcastする。
private String sound(String string) {
Map hash = new HashMap();
hash.put("ジャブ","「バシッ」");
hash.put("ストレート","「ビシィッ」");
return (String)hash.get(string);
}
テストを実行。GreenBar、OK。成功である。
こうしてここに書いてみると結構長いけど、実際にやってみると簡単な印象でした。
Eclipseのローカルヒストリーで見てみると最初のテストを追加した時間が1:19:51。
最後の実装が終わったのが1:27:54ということで8分少々のようでした。
実際に一番時間を食っていたのがHashMapを調べるところ。
RubyをJavaに移植するということで、一番差がありそうだと思っていたのがハッシュと正規表現だったので、とりあえず一方は障害にならないことが分かりました。
BackSpaceを押したら消えた
いきなりやる気を失わせる事態だ。
書きかけの記事を編集中にBackSpaceを押したら、サーバエラーと表示されて消えた。
多分、タッチパッドに触れるかしてフォーカスが移動したところでBackSpaceを押したのでブラウザの戻る機能が発動したのだろう。
こまめに下書きモードで保存しておくべきだということか。