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:「毎回毎回、計算しなおすんを、やめたらどないですやろ。投球のたんびに、curenntFrameのメンバ変数を変更するようにしてみたら?」 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:「これも動くな。ほな、最初の問題に戻るで。socre 関数をうまいこと動かさなあかんねんけど、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:「TThrowクラスから始めたおかげで、間違うたんやな。最初は、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:「大声で泣きとうなってきたわ。お前は、わしらは、fretting over that しとったと言いたいんやな。結局わしらがやったこと言うたら、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. 翻訳:小林 修 翻訳にあたって,平鍋 健児氏,長瀬 嘉秀氏より助言をいただきました.ありがとうございました. |