導入
忘年会のシーズンが始まります。忘年会といえばビンゴ。賞品が当たるかどうかは別として、
今回はビンゴマシンを作ることを通して、
展開
ビンゴマシンを作ろう
ビンゴゲームとは
おそらく知らない人は少ないと思いますが、
ビンゴゲームとは
- 5行5列の正方形マス目にランダムな番号の書かれたカードを用います。
- 参加者はこのカードを一人一枚持ち、
司会の読み上げる数字が自分のカードにあれば、 カード上の数字にチェックを入れます。 - カード上のチェックが縦・
横・ 斜めいずれかの方向で5つ連続すればあがりです。その際 「ビンゴ!」 とコールします。 - チェックが4つ並んだ時点で
「リーチ!」 とコールする必要があります。
このビンゴゲームで、
Processingでビンゴマシンを作り終えたました
このビンゴマシンをProcessingで開発しました。シンプルながら、
このsketchを実行すると、
BingoMachine.pde
の実行結果
このコードをリファクタリングしていきましょう。意図的に臭いを発生させていますので、
テストを作成する
このコードは正しい実行結果を残しますから、
- ビンゴ数列は、
1から75までの数がランダムに、 一つずつもれなくだぶりなく並んだ数列であること。 - これを順に取り出せること。
現在のビンゴマシンの機能はこれだけですから、ArrayList
のオブジェクトbingosuuretsu
の要素が適切かをチェックするコードが必要です。
現在のsketchは、
この作業は、
メソッドの抽出
(Extract Method) 長すぎて読みにくいコードから、
処理のかたまりを切り分けてメソッドにする。
Extract Method
本来は上述の定義のように、
このような流れで変更し、
テストモードと実行モードを切り替えるフラグTEST_
を定義し、
ビンゴ数列を発生するメソッドはcreateBingoPermutation
で、ArrayList
のオブジェクトへの参照を受け取り、
ビンゴ数列をチェックするテストメソッドがtestBingoPermutation
です。ArrayList
のオブジェクトへの参照を受け取り、
テストは問題があればコンソールにメッセージを表示します。 本来はその他のメッセージをコンソールに表示しないところですが、
コードを見渡す
ここで、
BingoMachine.pde
の全ソースコード//ビンゴマシン
//テストモード
boolean TEST_MODE = true;
int tugi = 0;
//ビンゴ数列を格納するオブジェクト
ArrayList <Integer> bingosuuretsu = new ArrayList<Integer>();
void setup(){
if (TEST_MODE == true) {
runTest();
} else {
runBingoMachineApp();
}
}
void draw(){
if (TEST_MODE == true) {
//Test code
} else {
println("["+tugi+"] : " + bingosuuretsu.get(tugi));
background(204);
textSize(32);
fill(0, 102, 153);
text(bingosuuretsu.get(tugi), 10, 60);
}
}
void mousePressed() {
if (TEST_MODE == true) {
//Test code
} else {
if(tugi<74) tugi++;
}
}
void runBingoMachineApp(){
println("Bingo Machine booting up...");
noLoop();
println("This is Bingo Machine!");
createBingoPermutation(bingosuuretsu);
println("ループスタート");
loop();
}
void createBingoPermutation(ArrayList v){
println("createBingoPermutation called.");
ArrayList <Integer> temp = new ArrayList<Integer>();
for(int i = 1; i <= 75; i++){
temp.add(i);
}
while( temp.size() > 0 ){
int i = (int) (Math.random() * 100 + 1) % temp.size();
v.add(
temp.get(i)
);
temp.remove(i);
println("ビンゴ数列["+(v.size()-1)+"] = " + v.get( v.size()-1 ) );
}
println("createBingoPermutation finished.");
}
void runTest(){
println("runTest called.");
createBingoPermutation(bingosuuretsu);
assert testBingoPermutation(bingosuuretsu) == true : "ビンゴ数列にもれかだぶりがあります" ;
println("runTest finished.");
}
boolean testBingoPermutation(ArrayList v){
println("testBingoPermutation called.");
//ビンゴ数列のチェック
ArrayList <Boolean> tesuto = new ArrayList<Boolean>();
for( int i = 0; i < 75; i++ ){
tesuto.add(false);
}
for( int i = 0; i < 75; i++ ){
println("["+(i+1)+"] = " +v.get(i));
if ( tesuto.get( (int)v.get(i) -1 ) == true ){
return false;
} else {
tesuto.set((int)v.get(i)-1,true);
}
}
println("testBingoPermutation finished.");
return true;
}
・悪臭源:名称が不適切
一つ目の悪臭源は、
ファウラーのカタログでは、
メソッド名の変更
(Rename Method) メソッドが実行内容を正しく表していないので、
正しい名称に変更する。
Rename Method
- 意味不明な変数
-
まず、
冒頭に現れる整数型変数です。 int tugi = 0;
tugi
とありますが、何の 「つぎ」 か、 これでは意味がわかりません。また、 名称から推察するに、 カウンタかポインタの意味で使用しているのでしょうが、 単純なカウンタならもっと狭いスコープで宣言し使用するでしょう。わざわざグローバルに宣言した意味はあるのか、 確認しないといけません。 - 名称がローマ字
-
次に宣言されているオブジェクトも残念です。
//ビンゴ数列を格納するオブジェクト ArrayList <Integer> bingosuuretsu = new ArrayList<Integer>();
オブジェクト名をローマ字で宣言するのは止めましょう。
- 意味が間違っている
-
英語で宣言してあるから安心とは言えません。次のメソッドはすこし意味がずれています。
createBingoPermutation(bingosuuretsu);
ビンゴ数列を作るメソッドの名称が
「ビンゴ順列を作る」 となっています。全くの間違いではありませんが、 名称を 「ビンゴ数列」 に決めたわけですから、 それに従うべきです。ですから 「順列 (Permutation)」ではなく 「数列 (Progression)」が適当です。
・マジックナンバーを取り除く
マジックナンバーとは、
シンボリック定数によるマジックナンバーの置き換え
(Replace Magic Number with Symbolic Constant) マジックナンバーをシンボリック定数に置き換えることで、
コードの意味理解を助け、 値変更の複雑さを緩和する。
Replace Magic Number with Symbolic Constant
- 画面表示のための座標、
色 -
draw
メソッドにはディスプレイウインドウ上に表示する各種要素の座標が必要になります。これを生の数値、すなわちマジックナンバーで記述すると、 後々変更したい場合、 すべてのマジックナンバーをもれなく変更しなければなりません。考えただけでうんざりします。それだけであればエディタの置換機能があるから大丈夫かもしれませんが、 もし状況に応じて位置を微調整するコードが必要になった場合や、 色の微妙な変化を計算したい場合など、 コード実行中の動的な変更には対応できません。やはり、 マジックナンバーは駆逐しておくに限ります。 void draw(){ if (TEST_
MODE == true) { //Test code } else { println("["+tugi+"] : " + bingosuuretsu.get(tugi)); background(204); textSize(32); fill(0, 102, 153); text(bingosuuretsu.get(tugi), 10, 60); } }例えば、
上記コード中の draw
メソッドで指定される背景色の値は、ディスプレイウインドウを一旦クリアするために使用していますから、 204
とせずDEFAULT_
としましょう。また、GLAY 表示する数字のフォントサイズを指定している 32
は、BINGO_
としましょう。もっと短く適当な名称があればそれで構いません。NUMBER_ FONT_ SIZE fill
メソッドで使用しているフォント色は、RGBそれぞれ独立した整数で設定しています。これをそれぞれ定数化する方法と、 一つの16進数リテラルで指定する方法があります。この16進数リテラルをシンボリック定数にするとシンプルになります。 fill(0,102,153); fill(0xFF006699);
16進数リテラルで指定する場合、
最初の FF
の桁はアルファ値(透明度) を表しています。そこで、 次のように書き直しましょう。 static final int FONT_
COLOR = 0xFF006699;
この他、static final
で定義しましょう。おっと、TEST_
もboolean型の定数なので、
リファクタリング実行の手順
取り上げた悪臭源である、
- 適切な名称を考える。
- 不適切な名称の行をコピーし、
直後にペーストする。同じ行が2つできる。 - 上の行をコメントアウトする。
(いつでも復旧できるように) - 下の行を適切な名称に変更する。
- テストモードで実行する。
- アプリモードで実行する。
- 5.
6.で問題がなければ、 上の行を削除する。 - リポジトリ
(があれば) にコミットする。
これまで述べていませんが、
演習
演習1(難易度:easy, but difficult)
sketch BingoMachine.
をリファクタリングしてください。施すリファクタリングは
まとめ
- 「メソッドの抽出」
「名称の変更」 「シンボリック定数によるマジックナンバーの置き換え」 の3種類のリファクタリングを学習しました。
学習の確認
それぞれの項目で、
- 3種類のリファクタリングの意味が理解できましたか?
- 理解できた。気持ちよく納得した。
- 理解できた。しかし、
今ひとつスッキリしない。 - 理解できない。
- 3種類のリファクタリングの効果を実感できましたか?
- 気持ちよく実感した。
- 実感した。しかし、
なんとなくもやもやする。 - 実感できない。
- 演習を自力で完成できましたか?
- 完成できた。
- 部分的にはできたが、
すべてを完成できなかった。 - 何をして良いのかわからない。
参考文献
- 『新装版 リファクタリング―既存のコードを安全に改善する―
(OBJECT TECHNOLOGY SERIES)』(マーチン・ ファウラー 著、 オーム社) - かつてピアソン・
エデュケーション社から出版されていたものの新装版です。リファクタリングのバイブルですから、 必携です。
- かつてピアソン・
- 『Java言語で学ぶリファクタリング入門』
(結城浩 著、 ソフトバンククリエイティブ) - ファウラーのバイブルと併せて読むことをお勧め。著者の解説は一級です。
vv
演習解答
以下にリファクタリングを施したsketchを示します。読者の皆さんの取り組みと異なるところもあるでしょう。リファクタリング前後で振る舞いに違いがなく、
リファクタリング前よりも、 より読みやすく、 理解しやすいコードになっていれば結構です。