Robert C. Martin and Robert S. Koss
Object Mentor Inc.
|
設計とプログラミングをやるのは人間である。それを忘れてはいけない。忘れたら、何もかも無くしてしまう。 Bjarne Stroustrup, 1991 XP (eXtreme Programming)のプラクティスのデモンストレーションということで、ボブ・コス(RSK)とボブ・マーティン(RCM)が、簡単なアプリケーションをペアでプログラムする、その間、読者のあなた達は、壁にとまった蝿にでもなったつもりで見ていて欲しい。私達は、テスト・ファースト・デザインで行い、いろいろリファクタリングも行いながら、アプリケーションを作っていく。以下は、二人のボブが実際に行った、とあるプログラミング・エピソードを、忠実に再現したものである。 RCM:「ボウリングのスコアを計算する、小さなアプリケーションを書くんだけど、ちょっと手伝ってくれないか?」 RSK:(考え込んで... XP のプラクティスであるペア・プログラミングでは、頼まれたら、断れないことになっている。頼んでいるのは、私のボスだしなあ) 「もちろんです、ボブさん。喜んで手伝わせてもらいます。」 RCM:「よしっ。私達が書くのは、ボウリングのリーグ戦の記録を取るアプリケーションだ。全部の試合の記録をつけなければならないし、チームのランク付けもしなければならない。毎週の試合の勝ち負けも決めるし、全部の試合の得点を正確につけるんだ。」 RSK:「それはすごい。私、昔は名ボウラーだったんです。これはおもしろそうだ。いろんな機能があがりましたが、どれから始めますか?」 RCM:「単品の試合のスコア付けから始めよう。」 RSK:「オッケイ。それで、どうします?このストーリの入力と出力は何ですか?」 RCM:「入力は、単純に「投球」の連続だろう。「投球」は、ピンの倒れた本数を表す整数で表したらいいだろう。出力は、おなじみのボーリングのスコアカードに書かれたデータ。並んだフレームの中に、それぞれの投球で倒れたピンの数が入ってて、スペアとかストライクを表すマークが入ってたらいい。」 RSK:「そのスコアカードを、小さく絵に描いておきませんか?何をやるはずだったか、後で目で見て思い出せますよ。」(図1) ![]() 図1 RCM:「こいつはまた、下手くそなボウラーだなぁ。」 RSK:「酔っているのかもしれませんね(笑)。でも、受け入れテストとして、結構いけますよ。」 RCM:「他にもテストは要るだろうが、まぁ、それは後でいいか。どう始めようか?システムの設計からやるか?」 RSK:「怒らないでほしいんですけど、UML図で問題領域の概念モデルなんか書いても仕方ないと思うんですが・・・。スコアカードの絵さえみれば十分です。それだけで、オブジェクトの候補はすぐでてくるから、さっさとコード書いていきませんか?」 RCM:(パワフルな、オブジェクト設計者用の帽子をかぶりながら)「よしっ。まず、『ゲーム』オブジェクトは10個の『フレーム』の列からできてて、それぞれのフレームオブジェクトは、ひとつかふたつか、みっつの『投球』を含んでいるわけだ。」 RSK:「うまいなあ。私もちょうどそれを考えてたんです。ざっと絵に描いてみましょうか?でも、私がこの絵描いたことは、ケントには言わないでくださいね。」(図2) ![]() 図2 Kent:「私は、いつでも見ているぞー。」 RSK:「では、クラスをひとつ選んでくれますか?何でもいいです。依存してつながっている端のクラスから初めて、逆向きに実装していくのはどうです?きっとテストが簡単だと思うんですけど。」 RCM:「そうだな。じゃあ、Throwクラスのテストケースから作ろうか。」 RSK: (タイプし始める)
//TestThrow.java---------------------------------
import junit.framework.*;
public class TestThrow extends TestCase
{
public TestThrow(String name)
{
super(name);
}
// public void test????
}
RSK:「Throwオブジェクトにはどんな振る舞いがあるかな?何か考えがあります?」 RCM:「プレイヤーが倒したピンの数を憶えるんだ。」 RSK:「わかった、それって要するに、『大して何もしない』ってことを遠まわしに言ってるんですね?それなら、そんなデータをしまっているだけのオブジェクトなんかほおっておいて、何か振舞があるクラスを先に考えたほうがいいんじゃないですか」 RCM:「ふうむ、Throwクラスなんか、本当はいらないと言いたいのか?」 RSK:(たらりと汗をかく。この人はオレのボスだった...)「いえあの、これがですね、もし何も振舞を持たないのであれば、つまりその重要性はどの程度かな、と...必要かどうかは僕にもよくわからないんですけど、単なる setter とか getter 以上のメソッドを持ったオブジェクトのことを考えた方が、その、より生産的ではないかと、ちょっと感じたわけでして...自分でおやりになりたいんなら、どうぞ。」(と、キーボードを RCM の方に押しやる) RCM:「じゃあ、『つながっているクラス』を Frame にまで遡ってそこからはじめよう。そして、テストケースを書きながら、Throwクラスが本当に必要になるかどうか、やってみようじゃないか。」(キーボードをRSKに押し戻す) RSK:(これは自分を教育するために、真っ暗な裏道を手を引いてくれているのか。それとも、本当に自分の意見に同意してくれているのか、いぶかりながら) 「はいはい、新しいファイルに、新しいテストケースっと。」
//TestFrame.java------------------------------------
import junit.framework.*;
public class TestFrame extends TestCase
{
public TestFrame( String name )
{
super( name );
}
//public void test???
}
RCM:「おや、このコードを打つのは2回目だ。 Frame クラスの何か意味のあるテストケースは思いつくか?」 RSK:「Frameは、スコアの数値と、投げた球ごとに倒れたピンの数を覚えていて、ストライクやスペアがあるかどうか...」 RCM:「喋りすぎ!コードが足りないぞ。早く打たないか!」 RSK: (types)
//TestFrame.java---------------------------------
import junit.framework.*;
public class TestFrame extends TestCase
{
public TestFrame( String name )
{
super( name );
}
public void testScoreNoThrows()
{
Frame f = new Frame();
assertEquals( 0, f.getScore() );
}
}
//Frame.java---------------------------------------
public class Frame
{
public int getScore()
{
return 0;
}
}
RCM:「よし、テストケースはパスするな。しかし、getScore は、本当にいい加減な実装だな。もう1回投球したら、すぐに失敗する。もう何回か投球してから得点をチェックするようなテストケースをつけましょうか?」
//TestFrame.java---------------------------------
public void testAddOneThrow()
{
Frame f = new Frame();
f.add(5);
assertEquals(5, f.getScore());
}
RCM:「ああ、コンパイルも通らない。Frameクラスには、addというメソッドは無いからなあ。」 RSK:「このメソッドを定義さえすれば,コンパイルはきっと通りますよ。」 RCM:
//Frame.java---------------------------------------
public class Frame
{
public int getScore()
{
return 0;
}
public void add(Throw t)
{
}
}
RCM:(大きな声を出して考えながら)「これはコンパイルが通らないぞ。なんでかっていうと、Throwクラスをまだ書いてないからね。」 RSK:「私に向かって言ってくれませんか、ボブ。テストの方は整数を渡しているのに、メソッドはThrowオブジェクトを期待している。矛盾してますね。また Throwクラスのことをやいやい言出だす前に、このクラスの「振る舞い」を教えてもらえませんか?」 RCM:「うわあ、私は "f.add(5)" なんて書いていたのか、気が付かなかった。"f.add(new Throw(5))" って書かないとだめだったんだな。でも,それもみっともないなぁ。そうだ、本当は、"f.add(5)" と書きたかったんだ。」 RSK:「みっともないかどうか知りませんけど、ちょっとの間、格好のことは置いておきましょう。Throwオブジェクトの「振る舞い」を言えるかどうか、答えは二つに一つの2進数で頼みますよ!ボブ。」 RCM:「101101011010100101(どーだ、2進数だぞ) Throw に何か「振る舞い」があるのなぁ、ないのかなぁ。だんだん int でもいいような気になってきた。だが、まだそれは考えなくてもいいだろ?Frame.add は int を受け取るように書けばいいんだ。」 RSK:「だったら、単純だとういだけで、そうしたらいいですよ。他に理由はいりません。それで苦しくなってきたら、またその時に、もう少し凝ったらいいんですよ。」 RCM:「そうだな。」
//Frame.java---------------------------------------
public class Frame
{
public int getScore()
{
return 0;
}
public void add(int pins)
{
}
}
RCM:「よっし。これでコンパイルは通るけど、テストはパスしない。さあ、テストをパスするようにしないと。」
//Frame.java---------------------------------------
public class Frame
{
public int getScore()
{
return itsScore;
}
public void add(int pins)
{
itsScore += pins;
}
private int itsScore = 0;
}
RCM:「今度は、コンパイルもテストも通るぞ。でも、これでも簡略しすぎだなぁ。次のテストケースはどうだろう?」 RSK:「先に、休憩とりませんか?」 -------------------------------------------------------------------------------- RCM:「よし,気分転換できた. "Frame.add" は、やわなメソッドだ。11 を引数に呼ばれたら、いったいどうなるんだ?」 RSK:「そんなことしたら、例外をスローしてもいいんだろうけど、いったい誰がそんな呼び方をすます?これが、何千という人が使うフレームワークだったら、そういう事態のことも考えなきゃだめですけど、私達が使うだけだったら?もしそうなら、要するに、11 では呼ばないようにしたらいいんですよ。」 RCM:「いいとこついてるね。システムの他の部分のテストで、不正な引数は、キャッチしたらいい。それでもしやばいことになったら、その時にチェックを入れればいいよね。さて、add メソッドはまだ、ストライクもスペアも扱わない。ストライクやスペアを表現するテストケースで書いてみようか。」 RSK:「ふうむ、もし、"add(10)" と呼ぶのが、ストライクのことを表しているとしたら、getScore は何を返したらいいんだろ。アサーションをどう書いたらいいのか、よくわからないなぁ。ということは、問題の立て方が間違っているということですよ。それか、問題はいいけど、考える対象のオブジェクトを間違えているんですかね。」 RCM:「もしも、"add(10)" を呼ぶか、"add(3)" の次に "add(7)" を呼んだとすると、Frame の getScore は呼んでも意味がないな。その時は、得点を計算するには、それより後のフレームを覗かないとできない。もし、それより後のフレームがまだ無いとしたら、-1 を返さなければならないが、そんなみっともないものを返すのは、私はいやだね。」 RSK:「いや、私だって、-1 はいやですよ。今、あるフレームが別のフレームのことを知っていると言われましたよね?このフレームを握っているのは、いったい誰なんですか?」 RCM:「Game オブジェクトだ。」 RSK:「そうすると、Game は Frame に依存していて、さらに,Frame も Game に依存しているってことですか?。私は、そんなのいやですよ。」 RCM:「Frame は、Game に依存しなくてもいい。Frame は、「連結リスト」 でまとめたらいいんだ。おのおのの Frame は、次のフレームと手前のフレームへのポインタを持ってる。Frameから得点を取ろうと思うなら、フレームは、まず後ろを見て、手前のフレームの得点を取って、スペアとかストライクの場合は、必要に応じて、その先を見たらいいんだ。」 RSK:「オーケー、それがうまく想い描けないんで、煙にまかれたような感じですよー。ちょっとコードを書いてもらえませんか、ボス。」 RCM:「そうだなぁ。まずテストケースがいるよな?」 RSK:「Game のですか?それとも、Frame の別のテストですか?」 RCM:「Game のテストがいるんじゃないか?なんでって、Game が Frame を作って、互いにつなぎあわせるんだからな。」 RSK:「Frame に対してやっていたことは置いといて、気持ちを一気に Game に持っていきたいんですか?それとも、Frame をちゃんと動かせるだけの MockGame オブジェクトが欲しいんですか?」 RCM:「いや、もう Frame はここで止めておいて、Game に行こう。Game のテストケースが、Frame の連結リストが要ることを証明してくれるに違いないはずだ。」 RSK:「どういう具合に証明してくれるのか、私にはよくわかりませんけど。コードを書いてもらわないと。」 RCM:
//TestGame.java------------------------------------------
import junit.framework.*;
public class TestGame extends TestCase
{
public TestGame(String name)
{
super(name);
}
public void testOneThrow()
{
Game g = new Game();
g.add(5);
assertEquals(5, g.score());
}
}
RCM:「どーだ、これはまあまあだろ?」 RSK:「そうですね。でも、Frame のリストの証明とやらは、どこにあるんですか?」 RCM:「私もそれを探してるんだ。このテストケースから辿っていったら、どこに辿りつくのか、見ていくぞ。」
//Game.java----------------------------------
public class Game
{
public int score()
{
return 0;
}
public void add(int pins)
{
}
}
RCM:「オーケイ。コンパイルは通るけど、テストはパスしないな。テストをパスさせないと。」
//Game.java----------------------------------
public class Game
{
public int score()
{
return itsScore;
}
public void add(int pins)
{
itsScore += pins;
}
private int itsScore = 0;
}
RCM:「パスしたぞ。」 RSK:「逆らう訳ではないですけど、Frame オブジェクトの連結リストが要るっていう、ご立派な証明は、どこにあるんですか。そもそも、そのために Game オブジェクトを持って来たんでしょう?」 RCM:「おう、私もそれを探しているんだ。いざスペアやストライクについてテストケースを突っ込み出したら、きっとフレームを作って連結リストにつなげないとだめになる。絶対だ!でも、コードが私達をそう仕向けるまでは、そうしたくないんだ。」 RSK:「いいとこ突いてますね。じゃあ、Game について、少しずつやりましょうか。2回投球してスペアにならない場合のテストからやってみたら、どうでしょう?」 RCM:「そうだな、それなら、すぐにパスするはずだな。やるぞ。」
//TestGame.java------------------------------------------
public void testTwoThrowsNoMark()
{
Game g = new Game();
g.add(5);
g.add(4);
assertEquals(9, g.score());
}
RCM:「よーし、パスしたぞ。次は4投してマークなしのケースだ。」 RSK:「あれ、これもパスしますよ。パスするとは思わなかったけど。別に Frame なんか一個も作らなくても、投球を足し続けられますね。でも、まだ、スペアもストライクもやってないし、もしそうなったら、frame も作らないとだめになるのかなぁ。」 RCM:「私も、そうだろうと思ってたんだ。だけど、このテストケースを考えてみろ。」
//TestGame.java------------------------------------------
public void testFourThrowsNoMark()
{
Game g = new Game();
g.add(5);
g.add(4);
g.add(7);
g.add(2);
assertEquals(18, g.score());
assertEquals(9, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
}
RCM:「これはいいと思うか?」 RSK:「そうですね。今気付いたんですが,それぞれのフレーム毎に得点を見せないといけないのを忘れてましたよ。おや、スコアカードのスケッチが、私のダイエットコークの壜敷きになっている。それで忘れてたんですね。」 RCM:(ため息ついて)「オーケー、まずは、Game に scoreForFrame メソッドを増やして、テストケースを失敗させてみよう。」
//Game.java----------------------------------
public int scoreForFrame(int frame)
{
return 0;
}
RCM:「これでいい、コンパイルは通って、テストは失敗だ。どうやってテストをパスさせよう?」 RSK:「いよいよ、『フレーム』オブジェクトを作り初める訳ですね。でも、それがテストをパスさせる、一番シンプルな方法なんでしょうか?」 RCM:「いや、そんなことはない。さしあたりは、Game の中に整数の配列を作ればいい。add を呼ぶたびに、配列に新しい整数を付け加えていく。scoreForFrame は、呼ばれるたびに、配列をはじめから見て、得点を計算するんだ。」
//Game.java----------------------------------
public class Game
{
public int score()
{
return itsScore;
}
public void add(int pins)
{
itsThrows[itsCurrentThrow++]=pins;
itsScore += pins;
}
public int scoreForFrame(int frame)
{
int score = 0;
for ( int ball = 0;
frame > 0 && (ball < itsCurrentThrow);
ball+=2, frame--)
{
score += itsThrows[ball] + itsThrows[ball+1];
}
return score;
}
private int itsScore = 0;
private int[] itsThrows = new int[21];
private int itsCurrentThrow = 0;
}
RCM:(自分自身に、すっかり満足して)「ほら、うまくいったぞ。」 RSK:「その21というマジックナンバーは何ですか?」 RCM:「ひとつのゲームで投げられる球の数の最大だよ、君。」 RSK:「うへっ。当ててみましょうか?あなた、若い時は Unix のハッカーで、プログラム全部を1行の命令で書いては、誰も解読できないのを見て、喜んでいたでしょ?」 「scoreForFrame は、もっとわかりやすいように、リファクタリングしなきゃいけませんよ。でも、リファクタリングを考える前に、別の質問させてもらっていいですか?いったい、Game はこのメソッドを加えるのに一番いい場所なんでしょうか?私の頭では、Game は SRP(単一責任の原則)に違反してると思います。それは、投球を受け付ける上に、フレーム毎の投球の計算方法まで知ってますよね? Scorer(得点計算係)オブジェクト,というのを作るのはどうでしょう?」 RCM:(乱暴に、うろたえたように手を振って)「メソッドが今どこにあるかなんて、どうでもいいんだ。今大事なのは、得点がちゃんと出るようにすることだ。SRP の値打ちがどうこういう議論は、全部うまく行ってからだ。」 だけど、Unixハッカーがどうこういう話も意味が分かるぞ。このループを、もっとすっきりさせようじゃないか。」
public int scoreForFrame(int theFrame)
{
int ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
score += itsThrows[ball++] + itsThrows[ball++];
}
return score;
}
RCM:「これで、ちょっとはマシになっただろうが、score+= の式のところに、副作用がある。まあ、どっちの追加の式が先に評価されるかは、この場合関係ないから、どうでもいいんだけど。」(それとも、問題あるのだろうか?両方の配列操作よりも前に、値の増加が2つとも実行されてしまうことがあるかもしれない) RSK:「副作用があるかどうかは、実験して確かめることもできますけど、それより、その関数はスペアやストライクを、うまく扱えない。もっと読みやすくするのが先ですか,それとも機能を増やしていくのが先ですか?」 RCM:「実験してもいいが,特定のコンパイラに関してしか意味がないだろ? 他のコンパイラは、また別の順序で評価するかもしれない。順序に依存するような書き方はやめて、もっとテストケースを増やそうか。」
public int scoreForFrame(int theFrame)
{
int ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
int firstThrow = itsThrows[ball++];
int secondThrow = itsThrows[ball++];
score += firstThrow + secondThrow;
}
return score;
}
RCM:「よし,次のテストケースだ。スペアを試すぞ!」
public void testSimpleSpare()
{
Game g = new Game();
}
RCM:「もう、こんな風に書くのは飽きた。テストをリファクタリングして、Game の生成を setUp メソッドにぶちこもう。」
//TestGame.java------------------------------------------
import junit.framework.*;
public class TestGame extends TestCase
{
public TestGame(String name)
{
super(name);
}
private Game g;
public void setUp()
{
g = new Game();
}
public void testOneThrow()
{
g.add(5);
assertEquals(5, g.score());
}
public void testTwoThrowsNoMark()
{
g.add(5);
g.add(4);
assertEquals(9, g.score());
}
public void testFourThrowsNoMark()
{
g.add(5);
g.add(4);
g.add(7);
g.add(2);
assertEquals(18, g.score());
assertEquals(9, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
}
public void testSimpleSpare()
{
}
}
RCM:「これでマシになった。次は、スペアのテストケースを書くぞ。」 RSK:「私がやります。」
public int scoreForFrame(int theFrame)
{
int ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
int firstThrow = itsThrows[ball++];
int secondThrow = itsThrows[ball++];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
score += frameScore + itsThrows[ball++];
else
score += frameScore;
}
return score;
}
RCM:(キーボードを取り上げて)「よし、けど、frameScore==10 の場合の ball の値の増加をここでやるのは、場所が悪くないか? こっちのテストケースで試したら、よくわかるはずだ。」
public void testSimpleFrameAfterSpare()
{
g.add(3);
g.add(7);
g.add(3);
g.add(2);
assertEquals(13, g.scoreForFrame(1));
assertEquals(18, g.score());
}
RCM:「見てみろ、パスしない! それなら、もし、そのうるさい余分な値の増加を取りのぞいたら...」
if ( frameScore == 10 )
score += frameScore + itsThrows[ball];
RCM:「うう、まだパスしない。score メソッドが間違っているなんてことがあるんだろうか?テストケースを scoreForFrame(2) を呼ぶように変えたら、はっきりするだろう。」
public void testSimpleFrameAfterSpare()
{
g.add(3);
g.add(7);
g.add(3);
g.add(2);
assertEquals(13, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
}
RCM:「ふうむ、これならパスするな。score メソッドの中身はきっとぐちゃぐちゃだ。見てみようか。」
public int score()
{
return itsScore;
}
public void add(int pins)
{
itsThrows[itsCurrentThrow++]=pins;
itsScore += pins;
}
RCM:「ああ、これはだめだ。2番目のメソッドは、ピンの本数の合計を返しているだけだ。正しい得点じゃない。score メソッドは、scoreForFrame を呼ぶ時には、「今の」フレームを渡してやらなきゃだめだ」 RSK:「『今のフレーム』って、いったいどんなものですか? 今あるテストのそれぞれに、その『今のフレーム』を聞くメソッドの呼び出しをつけていきましょうか?もちろん、いっぺんに1個ずつ。」 RCM:「そうだな。」
//TestGame.java------------------------------------------
public void testOneThrow()
{
g.add(5);
assertEquals(5, g.score());
assertEquals(1, g.getCurrentFrame());
}
//Game.java----------------------------------
public int getCurrentFrame()
{
return 1;
}
RCM:「よし、うまくいった。しかし、これはでたらめなメソッドだ。次のテストケースに行こう。」
public void testTwoThrowsNoMark()
{
g.add(5);
g.add(4);
assertEquals(9, g.score());
assertEquals(1, g.getCurrentFrame());
}
RCM:「これも、おもしろくない。次に行こう。」
public void testFourThrowsNoMark()
{
g.add(5);
g.add(4);
g.add(7);
g.add(2);
assertEquals(18, g.score());
assertEquals(9, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
assertEquals(2, g.getCurrentFrame());
}
RCM:「これは、パスしない。はやくパスさせよう。」 RSK:「アルゴリズムは単純だと思いますよ。フレーム毎に2回投球するから、投げた数を2で割るだけです。まあ、ストライクが無い限りだけど、まだストライクはやってないし、今は無視しておきましょう。」 RCM:(うまく行くまで、1を足したり引いたりを試しながら)
public int getCurrentFrame()
{
return 1 + (itsCurrentThrow-1)/2;
}
RCM:「なんかしっくり来ないなぁ。」 RSK:「毎回毎回、計算しなおすのを、やめたらどうでしょうか。投球のたびに、currenntFrameのメンバ変数を変更するようにしてみたら?」 RCM:「よし、試してみるか。」
//Game.java----------------------------------
public int getCurrentFrame()
{
return itsCurrentFrame;
}
public void add(int pins)
{
itsThrows[itsCurrentThrow++]=pins;
itsScore += pins;
if (firstThrow == true)
{
firstThrow = false;
itsCurrentFrame++;
}
else
{
firstThrow=true;
}
}
private int itsCurrentFrame = 0;
private boolean firstThrow = true;
}
RCM:「よし、うまく行ってるぞ。でも、これは、『今のフレーム』は『最後に投げた球のフレーム』で、『次に投げる球のフレーム』じゃないということだね。まあ、それさえ忘れなかったら、ばっちりだ。」 RSK:「私は物覚えが悪いから、もう少し読み易くしませんか? でも、そのコードをいじるる前に、まず add からその部分を取り出して、 adjustCurrentFrame という、プライベートなメンバ関数にくくり出しましょうよ。」 RCM:「おお、それはいいなぁ。」
public void add(int pins)
{
itsThrows[itsCurrentThrow++]=pins;
itsScore += pins;
adjustCurrentFrame();
}
private void adjustCurrentFrame()
{
if (firstThrow == true)
{
firstThrow = false;
itsCurrentFrame++;
}
else
{
firstThrow=true;;
}
}
RCM:「それから、変数とメソッドの名前を、もっと解りやすくしておこう。 itsCurrentFrame という名前はどうだ?」 RSK:「それでもいい思いますけど、値をインクリメントする場所が悪いんじゃないかなぁ。『今のフレーム』は、私が思うに、これから投げる球のフレームの番号だから、フレームの中で最後に球を投げた後に、値のインクリメントをしなきゃいけないんじゃないでしょうか。 RCM:「その通りだな。テストケースをそんな風に変えよう。それから、adjustCurrentFrame を見直すぞ。」
//TestGame.java------------------------------------------
public void testTwoThrowsNoMark()
{
g.add(5);
g.add(4);
assertEquals(9, g.score());
assertEquals(2, g.getCurrentFrame());
}
public void testFourThrowsNoMark()
{
g.add(5);
g.add(4);
g.add(7);
g.add(2);
assertEquals(18, g.score());
assertEquals(9, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
assertEquals(3, g.getCurrentFrame());
}
//TestGame.java------------------------------------------
private void adjustCurrentFrame()
{
if (firstThrow == true)
{
firstThrow = false;
}
else
{
firstThrow=true;
itsCurrentFrame++;
}
}
private int itsCurrentFrame = 1;
}
RCM:「よし。これで動くぞ。じゃあ、スペアの場合のテストケース2つ作って、getCurrentFrame をテストしようか。」
public void testSimpleSpare()
{
g.add(3);
g.add(7);
g.add(3);
assertEquals(13, g.scoreForFrame(1));
assertEquals(2, g.getCurrentFrame());
}
public void testSimpleFrameAfterSpare()
{
g.add(3);
g.add(7);
g.add(3);
g.add(2);
assertEquals(13, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
assertEquals(3, g.getCurrentFrame());
}
RCM:「これも動くな。最初の問題に戻るぞ。score 関数をうまく動かさなきゃいけないんだが、score関数の中で、"scoreForFrame(getCurrentFrame()-1)" を呼ぶようにしたらいい。」
public void testSimpleFrameAfterSpare()
{
g.add(3);
g.add(7);
g.add(3);
g.add(2);
assertEquals(13, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
assertEquals(18, g.score());
assertEquals(3, g.getCurrentFrame());
}
//Game.java----------------------------------
public int score()
{
return scoreForFrame(getCurrentFrame()-1);
}
RCM:「TestOneThrow のテストケースで落ちるな。見てみようか。」
public void testOneThrow()
{
g.add(5);
assertEquals(5, g.score());
assertEquals(1, g.getCurrentFrame());
}
RCM:「1回投球しただけなら、最初のフレームはまだ不完全だ。scoreメソッドは "scoreForFrame(0)" と呼んでる。これは無駄だ。」 RSK:「どっちとも言えないんじゃないですか?このプログラムは、いったい誰のために書いてるんですか。誰がこの score を呼ぼうとしてるんです?不完全なフレームは、誰も呼ばないと思っててもいいんじゃないですか?」 RCM:「そうだなぁ、けど気になる。testOneThrow のテストケースから、scoreメソッドを放り出して逃げとこうか。それでいいか?」 RSK:「いいと思いますよ。いっそ、testOneThrow そのものを消してもいいんじゃないですか?もともと、やりたいテストケースまで持ってくための、その場しのぎみたいなもんだし。ほんとに役に立ってるんでしょうか?それにテストケースなら、他にもたくさんありますし。」 RCM:「うん、君の言いたいことはわかった.じゃあ、行くぞ。(コードを直し、テストを走らせ、緑のバーが表示される。)ああ、だいぶマシだな。」 「次に、ストライクのテストケースをやろう。結局のところ、フレームオブジェクトが、連結リストにつながるところが見たいんだ。そうだろ?」(くすくす笑う)
public void testSimpleStrike()
{
g.add(10);
g.add(3);
g.add(6);
assertEquals(19, g.scoreForFrame(1));
assertEquals(28, g.score());
assertEquals(3, g.getCurrentFrame());
}
RCM:「よっしゃ、コンパイル通るけど、テストは失敗するな。思った通りだ。さあて、パスするようにするか。」
//Game.java----------------------------------
public class Game
{
public void add(int pins)
{
itsThrows[itsCurrentThrow++]=pins;
itsScore += pins;
adjustCurrentFrame(pins);
}
private void adjustCurrentFrame(int pins)
{
if (firstThrow == true)
{
if( pins == 10 ) // strike
itsCurrentFrame++;
else
firstThrow = false;
}
else
{
firstThrow=true;
itsCurrentFrame++;
}
}
public int scoreForFrame(int theFrame)
{
int ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
int firstThrow = itsThrows[ball++];
if (firstThrow == 10)
{
score += 10 + itsThrows[ball] + itsThrows[ball+1];
}
else
{
int secondThrow = itsThrows[ball++];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
score += frameScore + itsThrows[ball];
else
score += frameScore;
}
}
return score;
}
private int itsScore = 0;
private int[] itsThrows = new int[21];
private int itsCurrentThrow = 0;
private int itsCurrentFrame = 1;
private boolean firstThrow = true;
}
RCM:「よし、そんなに難しくないな。パーフェクトゲームの得点が計算できるか、見てみよう。」
public void testPerfectGame()
{
for (int i=0; i<12; i++)
{
g.add(10);
}
assertEquals(300, g.score());
assertEquals(10, g.getCurrentFrame());
}
RCM:「うわ、330点? なんでそうなるんだ。」 RSK:「なんでって、「今のフレーム」が、12になるまで増えて行ってるからじゃないですか?」 RCM:「おおそうだ、10 までに制限しなきゃだめだった。」
private void adjustCurrentFrame(int pins)
{
if (firstThrow == true)
{
if( pins == 10 ) // strike
itsCurrentFrame++;
else
firstThrow = false;
}
else
{
firstThrow=true;
itsCurrentFrame++;
}
itsCurrentFrame = Math.min(10, itsCurrentFrame);
}
RCM:「くそ!こんどは 270 だって!どうなってるんだ?」 RSK:「ボブさん、score関数は、getCurrentFrame から1を引いてますから、それで、10 番目じゃなくて、9 番目のフレームの点を返してるんですよ。」 RCM:「ん?『今のフレーム』を制限を10じゃなくて11にしろって言うのか? よし。やってみよう。」 itsCurrentFrame = Math.min(11, itsCurrentFrame); RCM:「よっしゃ、得点は正しくなったぞ。でも、『今のフレーム』が 10 じゃなくて 11 になったから、テストがコケる。ちぇ、この『今のフレーム』は、ケツの痛みみたいだ。これから投げる球が属するフレームのことを『今のフレーム』ということにしたいんだが、そうしたら、ゲームの最後は、どういうことになるんだ?」 RSK:「たぶん、『今のフレーム』は最後に球を投げた時のフレーム、というとこに戻らなきゃいけないんじゃないですか?」 RCM:「それか、最後の「完了した」フレームという風に考えなきゃいかんということかもしれんな。結局は、ゲーム中のある時点での得点というのは、最後の「完了した」フレームまでの得点ってことだろ?」 RSK:「完了したフレームというのは、得点を書き込めるフレームっていう意味ですね?」 RCM:「そうそう。スペアの出たフレームが完了するのは、次の1投の後だってことだ。ストライクの出たフレームだったら、次の2投の後。マークの無いフレームだったら、そのフレームの中の第2投の後に完了するってことだ。」 「ちょっと待った...私達は、scoreメソッドを動くようにしようとしてたんだったな。だったら、もしゲームが終わってた場合は、scoreメソッドがscoreForFrame(10) を呼ぶようにするだけでいいんだ。」 RSK:「どうしたら、ゲームの終わりだとわかるんですか?」 RCM:「もし、adjustCurrentFrame メソッドが、itsCurrentFrame の値を増やしたときに、10個めのフレームを超えたら、それでゲームは終わりだ。」 RSK:「待って下さい。要するに、もし getCurrentFrame が11を返したら、ゲーム終了だと言いたいんですか? だったら、もうそうなってますよ!」 RCM:「ふーむ。テストケースに合うようにコードを変更しろと言ってるのか?」
public void testPerfectGame()
{
for (int i=0; i<12; i++)
{
g.add(10);
}
assertEquals(300, g.score());
assertEquals(11, g.getCurrentFrame());
}
RCM:「よし、これはパスするぞ。これは、getMonth が1月のことを0と言うのに比べたら悪くはないけど、やっぱり、しっくり来ないなぁ。」 RSK:「たぶん、後でどうにかなるでしょ。あっ、バグだっ!ちょっと貸してくださいっ!?」(キーボードを握り込む)
public void testEndOfArray()
{
for (int i=0; i<9; i++)
{
g.add(0);
g.add(0);
}
g.add(2);
g.add(8); // 10th frame spare
g.add(10); // Strike in last position of array.
assertEquals(20, g.score());
}
RSK:「ふうん、別にコケないなぁ。配列の21番目の場所がストライクだから、『得点計算係』クラスとしては、22番目と23番目の場所も得点に足すと思ったんだけどなあ。けど、そうはならないみたいだなぁ。」 RCM:「ふうむ、お前、まだ「得点計算係」オブジェクトにこだわってんのか?とにかく、お前の言ってることは解るけど、scoreメソッドは、10以上の数を引数にして、scoreForFrame を呼ぶことは絶対に無いから、最後のストライクは、実際はストライクとして扱ってない訳だ。それは、最後のスペアを完了させるのに、10に数えられるだけなんだぞ。配列の終わりを越えてしまうことは無いはず。」 RSK:「じゃあ、私達が前に作ったスコアカードの点を、プログラムに放り込んでみましょうか?」
public void testSampleGame()
{
g.add(1);
g.add(4);
g.add(4);
g.add(5);
g.add(6);
g.add(4);
g.add(5);
g.add(5);
g.add(10);
g.add(0);
g.add(1);
g.add(7);
g.add(3);
g.add(6);
g.add(4);
g.add(10);
g.add(2);
g.add(8);
g.add(6);
assertEquals(133, g.score());
}
RSK:「やった、ちゃんと動いてる。他にテストケースを思いつきます?」 RCM:「よし、もうちょっと、境界条件を試そうか。ストライクを 11 回放った後に、最後の1投だけ9ピンっていう、哀れなピエロってのはどう?」
public void testHeartBreak()
{
for (int i=0; i<11; i++)
g.add(10);
g.add(9);
assertEquals(299, g.score());
}
RCM:「これも動くぞ。よし、10個めのフレームがスペアだったら?」
public void testTenthFrameSpare()
{
for (int i=0; i<9; i++)
g.add(10);
g.add(9);
g.add(1);
g.add(1);
assertEquals(270, g.score());
}
}
RCM:(緑のバーを幸せそうに見つめながら)「これもちゃんと動くぞ。もうこれ以上は、思いつかないな。そっちはどうだ?」 RSK:「いや、全部出たと思います。ところで、私、このごちゃごちゃをリファクタリングしたくて、しょうが無いんですけど。やっぱり、どこかに、得点計算係オブジェクトがハマりそうな気がするんです。。。」 RCM:「そうだなぁ、scoreForFrame関数は、もうごちゃごちゃだからな。よしっ、そいつを考えようか。」
public int scoreForFrame(int theFrame)
{
int ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
int firstThrow = itsThrows[ball++];
if (firstThrow == 10)
{
score += 10 + itsThrows[ball] + itsThrows[ball+1];
}
else
{
int secondThrow = itsThrows[ball++];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
score += frameScore + itsThrows[ball];
else
score += frameScore;
}
}
return score;
}
RCM:「私は、そのelseのところを、handleSecondThrow みたいな名前が別の関数に分けたくて仕方ないのだが、変数の ball と firstThrow と secondThrowを使ってるから、できないんだよ。」 RSK:「このローカル変数を、メンバ変数に変えますか?」 RCM:「ああ、そうすると、得点計算を、それ自身独立した『得点計算係』オブジェクトに分けれるはずっていう、お前の言い分が、それらしくなってくるな。よーし、そいつをやってみるか。」 RSK:(キーボードを取り上げて)
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if( pins == 10 ) // strike
itsCurrentFrame++;
else
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
itsCurrentFrame++;
}
itsCurrentFrame = Math.min(11, itsCurrentFrame);
}
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
firstThrow = itsThrows[ball++];
if (firstThrow == 10)
{
score += 10 + itsThrows[ball] + itsThrows[ball+1];
}
else
{
secondThrow = itsThrows[ball++];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
score += frameScore + itsThrows[ball];
else
score += frameScore;
}
}
return score;
}
private int ball;
private int firstThrow;
private int secondThrow;
private int itsScore = 0;
private int[] itsThrows = new int[21];
private int itsCurrentThrow = 0;
private int itsCurrentFrame = 1;
private boolean firstThrowInFrame = true;
RSK:「名前がぶつかるとは思いませんでした。firstThrow って変数は、もうあったんだ。けど、firstThrowInFrame って名前のほうが似合いますね。とにかく、それでうまいこといきますよ。そしたら、elseのとこを、独立したメソッドにできるし。」
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
firstThrow = itsThrows[ball++];
if (firstThrow == 10)
{
score += 10 + itsThrows[ball] + itsThrows[ball+1];
}
else
{
score += handleSecondThrow();
}
}
return score;
}
private int handleSecondThrow()
{
int score = 0;
secondThrow = itsThrows[ball++];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
score += frameScore + itsThrows[ball];
else
score += frameScore;
return score;
}
RCM:「socreForFrame の構造を見てみろ。擬似コードで書いたら、こんなもんだろ?」 if strike score += 10 + nextTwoBalls(); else handleSecondThrow. RCM:「で、それを、こんなふうに変えたらどうなる?」 if strike score += 10 + nextTwoBalls(); else if spare score += 10 + nextBall(); else score += twoBallsInFrame() RSK:「げっ、それって、ボウリングの得点ルールそのものじゃないですか。よし、ほんものの関数でも、同じ構造になるか、見てみましょう。最初に、ball 変数が増加される方法を変えて、3つのケースで、それぞれ独立にやるようにします。」
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
firstThrow = itsThrows[ball];
if (firstThrow == 10)
{
ball++;
score += 10 + itsThrows[ball] + itsThrows[ball+1];
}
else
{
score += handleSecondThrow();
}
}
return score;
}
private int handleSecondThrow()
{
int score = 0;
secondThrow = itsThrows[ball+1];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( frameScore == 10 )
{
ball+=2;
score += frameScore + itsThrows[ball];
}
else
{
ball+=2;
score += frameScore;
}
return score;
}
RCM:(キーボードを取り上げて)「よしっ、次は、変数の firstThrow と secondThrow を除いて、適当な関数に置き換えるぞ。」
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
firstThrow = itsThrows[ball];
if (strike())
{
ball++;
score += 10 + nextTwoBalls();
}
else
{
score += handleSecondThrow();
}
}
return score;
}
private boolean strike()
{
return itsThrows[ball] == 10;
}
private int nextTwoBalls()
{
return itsThrows[ball] + itsThrows[ball+1];
}
RCM:「うまくいった。続けるぞ。」
private int handleSecondThrow()
{
int score = 0;
secondThrow = itsThrows[ball+1];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( spare() )
{
ball+=2;
score += 10 + nextBall();
}
else
{
ball+=2;
score += frameScore;
}
return score;
}
private boolean spare()
{
return (itsThrows[ball] + itsThrows[ball+1]) == 10;
}
private int nextBall()
{
return itsThrows[ball];
}
RCM:「よぉし、これもうまくいくぞ。次は、frameScore だ。」
private int handleSecondThrow()
{
int score = 0;
secondThrow = itsThrows[ball+1];
int frameScore = firstThrow + secondThrow;
// spare needs next frames first throw
if ( spare() )
{
ball+=2;
score += 10 + nextBall();
}
else
{
score += twoBallsInFrame();
ball+=2;
}
return score;
}
private int twoBallsInFrame()
{
return itsThrows[ball] + itsThrows[ball+1];
}
RSK:「ボブ、ball の値を増やすやり方が、ばらばらですよ。スペアとストライクの場合は、得点を計算する前に増やしてるけど、twoBallsInFrame の時は、計算の後にやってる。で、コードはこの順番に依存してますよ。どうしたんですか?」 RCM:「すまん、説明しなきゃな。私は、値の増加を、strike, spare, と twoBallsInFrame のメソッドの中に持っていこう思ってるんだ。そうすれば、scoreForFrame からは消えてきれいになるし、関数がちょうど擬似コードそのものみたいになるだろ?」 RSK:「わかりました。もうちょっとの間、信用しときます。でも覚えておいてくださいね、私は見てますよぉ。」 Kent:「私も見てるぞー。」 RCM:「よしっ、もう、誰も firstThrow, secondThrow, と frameScore を使ってないから、これらは、捨てるぞ。」
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
if (strike())
{
ball++;
score += 10 + nextTwoBalls();
}
else
{
score += handleSecondThrow();
}
}
return score;
}
private int handleSecondThrow()
{
int score = 0;
// spare needs next frames first throw
if ( spare() )
{
ball+=2;
score += 10 + nextBall();
}
else
{
score += twoBallsInFrame();
ball+=2;
}
return score;
}
RCM:(眼の中に、緑のバーの反射をひらめかせながら)「もう、3つのケースを結びつける変数はballだけになったし、ball はそれぞれのケースで独立に扱われているから、3つのケースはひとつにまとめられるな。」
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
if (strike())
{
ball++;
score += 10 + nextTwoBalls();
}
else if ( spare() )
{
ball+=2;
score += 10 + nextBall();
}
else
{
score += twoBallsInFrame();
ball+=2;
}
}
return score;
}
RSK:(ピーター・ローリーは息を呑んで)「マスター...マスター...僕にやらせて。お願い、僕にやらせて。」 RCM:「おお、イゴール、値の増加の場所を変えたいのかい?」 RSK:「そうです、マスター。ああ、そうなんです、マスター」(キーボードをつかむ)
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
if (strike())
score += 10 + nextTwoBalls();
else if (spare())
score += 10 + nextBall();
else
score += twoBallsInFrame();
}
return score;
}
private boolean strike()
{
if (itsThrows[ball] == 10)
{
ball++;
return true;
}
return false;
}
private boolean spare()
{
if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
{
ball += 2;
return true;
}
return false;
}
private int nextTwoBalls()
{
return itsThrows[ball] + itsThrows[ball+1];
}
private int nextBall()
{
return itsThrows[ball];
}
private int twoBallsInFrame()
{
return itsThrows[ball++] + itsThrows[ball++];
}
RCM:「よくやったぞ、イゴール。」 RSK:「ありがとう、マスター。」 RCM:「scoreForFrame 関数を見てみろ。ボウリングのルールを、これ以上簡潔には書けないぞ。」 RSK:「でも、ボブさん、Frame オブジェクトの連結リストはどうなったんですか?」 RCM:(ため息)「『設計中毒症』の悪魔にたぶらかされてたのかもしれないな。あぁ、ナプキンの裏に描かれた3つの小さい箱、Game、Frame、Throw だけでも、もう複雑すぎて、全然間違いだったんだ。」 RSK:「Throwクラスから始めたおかげで、間違ったんだな。最初は、Gameクラスから始めるべきだったんだ!」 RCM:「そうだ。この次は、一番高いレベルから順に低いレベルに降りていく方法で試すぞ。」 RSK:(息を呑んで)「トップダウン設計ですか! じゃあ、デマルコはずっと正しかったってことですか?」 RCM:「言い直すと、トップダウン・テストファースト設計。正直言って、それでうまくいくかどうかわからない。今回は、たまたまそれでうまくいったんだ。だから、次もそれでやってみて、どうなるか試すんだ。」 RSK:「わかりました。ところで、もう少し、リファクタリングしなきゃだめですよ。ball変数は、scoreForFrameメソッドとその手下のメソッドのための、プライベートな繰り返し用変数だから、別のオブジェクトに突っ込むべきでしょ。」 RCM:「おお、そうだな。お前の言う Scorer(得点計算係)オブジェクトだな。結局、お前が正しかったんだ。やってみろ。」 RSK:(キーボードをつかみ、テストを交えながら、少しずつ、以下のコードを書いていく)
//Game.java----------------------------------
public class Game
{
public int score()
{
return scoreForFrame(getCurrentFrame()-1);
}
public int getCurrentFrame()
{
return itsCurrentFrame;
}
public void add(int pins)
{
itsScorer.addThrow(pins);
itsScore += pins;
adjustCurrentFrame(pins);
}
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if( pins == 10 ) // strike
itsCurrentFrame++;
else
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
itsCurrentFrame++;
}
itsCurrentFrame = Math.min(11, itsCurrentFrame);
}
public int scoreForFrame(int theFrame)
{
return itsScorer.scoreForFrame(theFrame);
}
private int itsScore = 0;
private int itsCurrentFrame = 1;
private boolean firstThrowInFrame = true;
private Scorer itsScorer = new Scorer();
}
//Scorer.java-----------------------------------
public class Scorer
{
public void addThrow(int pins)
{
itsThrows[itsCurrentThrow++] = pins;
}
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
if (strike())
score += 10 + nextTwoBalls();
else if (spare())
score += 10 + nextBall();
else
score += twoBallsInFrame();
}
return score;
}
private boolean strike()
{
if (itsThrows[ball] == 10)
{
ball++;
return true;
}
return false;
}
private boolean spare()
{
if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
{
ball += 2;
return true;
}
return false;
}
private int nextTwoBalls()
{
return itsThrows[ball] + itsThrows[ball+1];
}
private int nextBall()
{
return itsThrows[ball];
}
private int twoBallsInFrame()
{
return itsThrows[ball++] + itsThrows[ball++];
}
private int ball;
private int[] itsThrows = new int[21];
private int itsCurrentThrow = 0;
}
RSK:「これで、だいぶん良くなりましたよ。Game がフレームの記録を取り、Scorer は単に、得点を計算するだけ。まさに SRP 。」 RCM:「なんでもいいけど、確かにマシだな。itsScore 変数がもう使われてないことに気づいたか?」 RSK:「あ。ほんとだ。消してまえ。」(上機嫌に消し始める)
public void add(int pins)
{
itsScorer.addThrow(pins);
adjustCurrentFrame(pins);
}
RSK:「悪くないね。次は、adjustCurrentFrame を綺麗にしましょうか?」 RCM:「よし、それを見てみようか。」
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if( pins == 10 ) // strike
itsCurrentFrame++;
else
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
itsCurrentFrame++;
}
itsCurrentFrame = Math.min(11, itsCurrentFrame);
}
RCM:「よし、まず、値の増加を、単独の関数に分けて、フレームを11に制限するのも、その中でやろう。」(でも、やっぱり、11はきらいだ) RSK:「ボブさん、11 ってのは、要するにゲームの終わりですよ。」 RCM:「わかってる。」(キーボードをつかんで、いくつかの変更を加え、テストする)
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if( pins == 10 ) // strike
advanceFrame();
else
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
advanceFrame();
}
}
private void advanceFrame()
{
itsCurrentFrame = Math.min(11, itsCurrentFrame + 1);
}
RCM:「よーし、ちょっとマシになったな。じゃあ、ストライクのケースを、別のメソッドに分けようか」
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if (adjustFrameForStrike(pins) == false)
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
advanceFrame();
}
}
private boolean adjustFrameForStrike(int pins)
{
if (pins == 10)
{
advanceFrame();
return true;
}
return false;
}
RCM:「これは素晴らしい。さて、あの11のことだけどな。」 RSK:「ボブさん、ほんとに11が嫌いなんですね。」 RCM:「ああ、score関数を見てみようか。」
public int score()
{
return scoreForFrame(getCurrentFrame()-1);
}
RCM:「この-1 は変だ。getCurrentFrame を使ってるのはここだけなのに、何で、その戻り値を調整しなきゃいけないんだ。」 RSK:「チクショー、ボブさんが正しい!これについて、私達って、いったい何回気を変えたんだろ?」 RCM:「気が変わり過ぎだな。だが、そこにある通りだ。itsCurrentFrame が表してるのは、これから投げようとしている球のフレームじゃなくうて、最後の投球のフレームを表すことを、コードが望んでるんだ。」 RSK:「シェーッ、これでダメんなるテストケースが、たくさんある?」 RCM:「本来なら、全部のテストケースから getCurrentFrame を削って、ついでに getCurrentFrame そのものも削ってしおうと思っているんだ。だあれも使ってないんだからな。」 RSK:「オーケー、言うことはわかりました。そうしましょう。びっこになった馬を、惨めな境遇から救ってやる訳ですね† 。」
//Game.java----------------------------------
public int score()
{
return scoreForFrame(itsCurrentFrame);
}
private void advanceFrame()
{
itsCurrentFrame = Math.min(10, itsCurrentFrame + 1);
}
RCM:「大声で泣きたくなってきた。お前は、私達がやきもきしてたと言いたいんだな。結局私達がやったことと言えば、11 を 10 に変えたのと、-1 を削っただけだもんな。くそっ。」 RSK:「いや、ボブ叔父さん、本当に、私達がやった 苦悩程の値打ちも無かったなあ。」 RCM:「よしっ、これで出来上がりだ。もう一度、プログラム全部読んで、めいっぱいシンプルで解り易いかどうか、確かめておこう!」
//Game.java----------------------------------
public class Game
{
public int score()
{
return scoreForFrame(itsCurrentFrame);
}
public void add(int pins)
{
itsScorer.addThrow(pins);
adjustCurrentFrame(pins);
}
private void adjustCurrentFrame(int pins)
{
if (firstThrowInFrame == true)
{
if (adjustFrameForStrike(pins) == false)
firstThrowInFrame = false;
}
else
{
firstThrowInFrame=true;
advanceFrame();
}
}
private boolean adjustFrameForStrike(int pins)
{
if (pins == 10)
{
advanceFrame();
return true;
}
return false;
}
private void advanceFrame()
{
itsCurrentFrame = Math.min(10, itsCurrentFrame + 1);
}
public int scoreForFrame(int theFrame)
{
return itsScorer.scoreForFrame(theFrame);
}
private int itsCurrentFrame = 0;
private boolean firstThrowInFrame = true;
private Scorer itsScorer = new Scorer();
}
//Scorer.java-----------------------------------
public class Scorer
{
public void addThrow(int pins)
{
itsThrows[itsCurrentThrow++] = pins;
}
public int scoreForFrame(int theFrame)
{
ball = 0;
int score=0;
for (int currentFrame = 0;
currentFrame < theFrame;
currentFrame++)
{
if (strike())
score += 10 + nextTwoBalls();
else if (spare())
score += 10 + nextBall();
else
score += twoBallsInFrame();
}
return score;
}
private boolean strike()
{
if (itsThrows[ball] == 10)
{
ball++;
return true;
}
return false;
}
private boolean spare()
{
if ((itsThrows[ball] + itsThrows[ball+1]) == 10)
{
ball += 2;
return true;
}
return false;
}
private int nextTwoBalls()
{
return itsThrows[ball] + itsThrows[ball+1];
}
private int nextBall()
{
return itsThrows[ball];
}
private int twoBallsInFrame()
{
return itsThrows[ball++] + itsThrows[ball++];
}
private int ball;
private int[] itsThrows = new int[21];
private int itsCurrentThrow = 0;
}
RCM:「よっしゃあ、ばっちりだ。もうこれ以上、何もすることはないぞ。」 RSK:「いや、素晴らしい! それから、テストの方も見ておきましょう、おまけで。」
//TestGame.java------------------------------------------
import junit.framework.*;
public class TestGame extends TestCase
{
public TestGame(String name)
{
super(name);
}
private Game g;
public void setUp()
{
g = new Game();
}
public void testTwoThrowsNoMark()
{
g.add(5);
g.add(4);
assertEquals(9, g.score());
}
public void testFourThrowsNoMark()
{
g.add(5);
g.add(4);
g.add(7);
g.add(2);
assertEquals(18, g.score());
assertEquals(9, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
}
public void testSimpleSpare()
{
g.add(3);
g.add(7);
g.add(3);
assertEquals(13, g.scoreForFrame(1));
}
public void testSimpleFrameAfterSpare()
{
g.add(3);
g.add(7);
g.add(3);
g.add(2);
assertEquals(13, g.scoreForFrame(1));
assertEquals(18, g.scoreForFrame(2));
assertEquals(18, g.score());
}
public void testSimpleStrike()
{
g.add(10);
g.add(3);
g.add(6);
assertEquals(19, g.scoreForFrame(1));
assertEquals(28, g.score());
}
public void testPerfectGame()
{
for (int i=0; i<12; i++)
{
g.add(10);
}
assertEquals(300, g.score());
}
public void testEndOfArray()
{
for (int i=0; i<9; i++)
{
g.add(0);
g.add(0);
}
g.add(2);
g.add(8); // 10th frame spare
g.add(10); // Strike in last position of array.
assertEquals(20, g.score());
}
public void testSampleGame()
{
g.add(1);
g.add(4);
g.add(4);
g.add(5);
g.add(6);
g.add(4);
g.add(5);
g.add(5);
g.add(10);
g.add(0);
g.add(1);
g.add(7);
g.add(3);
g.add(6);
g.add(4);
g.add(10);
g.add(2);
g.add(8);
g.add(6);
assertEquals(133, g.score());
}
public void testHeartBreak()
{
for (int i=0; i<11; i++)
g.add(10);
g.add(9);
assertEquals(299, g.score());
}
public void testTenthFrameSpare()
{
for (int i=0; i<9; i++)
g.add(10);
g.add(9);
g.add(1);
g.add(1);
assertEquals(270, g.score());
}
}
RSK:「よくカバーできてますね。他にいいテストケース、思いつきます?」 RCM:「いや、これが決定版。削っていいようなものも、何も見あたらないし。」 RSK:「じゃあ、完成ですね?」 RCM:「そうだ。よく助けてくれたな、ありがとう。」 RSK:「どういたしまして、おもしろかったです。」 † 脚を折った馬は,すぐに弱って死んでしまうため,安楽死させるという. © Copyright 1994 - 2003 Object Mentor, Inc. 翻訳:小林 修 翻訳にあたって,平鍋 健児氏,長瀬 嘉秀氏より助言をいただきました.ありがとうございました. |