Processingで学ぶ 実践的プログラミング専門課程

第28回リファクタリング(2)ビンゴマシンを作ろう

導入

忘年会のシーズンが始まります。忘年会といえばビンゴ。賞品が当たるかどうかは別として、ビンゴは楽しいゲームです。最近は、デジタル表示でBGMが流れるなど、凝ったビンゴマシンがおもちゃ屋さんに置かれています。しかしこれ、なんだか自分で作れそうと思いませんか?

今回はビンゴマシンを作ることを通して、リファクタリングを学びます。

展開

ビンゴマシンを作ろう

ビンゴゲームとは

おそらく知らない人は少ないと思いますが、ビンゴゲームの概略は次のとおりです。詳しくはWikiPediaなどを参照してください。

ビンゴゲームとは

  • 5行5列の正方形マス目にランダムな番号の書かれたカードを用います。
  • 参加者はこのカードを一人一枚持ち、司会の読み上げる数字が自分のカードにあれば、カード上の数字にチェックを入れます。
  • カード上のチェックが縦・横・斜めいずれかの方向で5つ連続すればあがりです。その際「ビンゴ!」とコールします。
  • チェックが4つ並んだ時点で「リーチ!」とコールする必要があります。

このビンゴゲームで、司会が読み上げるのは1から75までの数です。それぞれの数は一度しか読み上げられません。これは1から75までの数をランダムに一つずつ取り出す順列です。この順列を以後「ビンゴ数列(Bingo progression⁠⁠」と呼ぶことにします。ビンゴ数列を作成するために、福引き抽選器が使われることがあります。このような、ビンゴ数列を作成する機械・装置を「ビンゴマシン」というわけです。

Processingでビンゴマシンを作り終えたました

このビンゴマシンをProcessingで開発しました。シンプルながら、最低限の機能を持ったビンゴマシンを「急いで」作成し終わったという状況を想定しています。とりあえず今回の忘年会で使えればいいや程度の意識ですから、かなり「臭い」コードです。

このsketchを実行すると、次のようなディスプレイウインドウが表示されます。ディスプレイウインドウをマウスポインタでクリックするたびに、次の数が表示されます。75番目の数字が表示された後は、クリックしても変更されません。sketchを実行するたびに、異なるビンゴ数列を発生します。

かなり臭いBingoMachine.pdeの実行結果
画像

このコードをリファクタリングしていきましょう。意図的に臭いを発生させていますので、かなり読みづらいと思います。そこを我慢して、一通り目を通しておいてください。

テストを作成する

このコードは正しい実行結果を残しますから、リファクタリングの前後で動作が異なっては困ります。そこで、まずはテストコードを用意します。テストの要件は次のとおりとします。

  • ビンゴ数列は、1から75までの数がランダムに、一つずつもれなくだぶりなく並んだ数列であること。
  • これを順に取り出せること。

現在のビンゴマシンの機能はこれだけですから、これをテストします。現在のsketchで言えば、ArrayListのオブジェクトbingosuuretsuの要素が適切かをチェックするコードが必要です。

現在のsketchは、仕事ごとにモジュール化されておらず、テストコードを適用するには不適切です。そこで、まずはテストが適用できるように最低限のモジュール化を施します。具体的には、ビンゴ数列を発生するコードを関数にまとめます。

この作業は、メソッドの抽出と呼ばれる中規模のリファクタリングです。

メソッドの抽出(Extract Method)

長すぎて読みにくいコードから、処理のかたまりを切り分けてメソッドにする。

Extract MethodMartin Fowler Refactoring Catalog

本来は上述の定義のように、長すぎて読みにくいコードを読みやすくする意図のリファクタリングです。今回のように、テストを適用できるようにするための変更とは、厳密には意味が異なります。しかし、だらだらとして、他の仕事のコードと連続していると可読性が低いです。また変数やオブジェクトの利用で不必要に重複していると、変更修正に弱い状態です。テストのためのコードの変更ですが、結果として以上のような不健全な状態を改善します。

このような流れで変更し、テストコードを追加したのが次のsketchです。

テストモードと実行モードを切り替えるフラグTEST_MODEを定義し、これが真なら各テストを実行します。偽ならビンゴマシンとして動作します。

ビンゴ数列を発生するメソッドは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 MethodMartin Fowler Refactoring Catalog)

意味不明な変数

まず、冒頭に現れる整数型変数です。

int tugi = 0;

tugiとありますが、何の「つぎ」か、これでは意味がわかりません。また、名称から推察するに、カウンタかポインタの意味で使用しているのでしょうが、単純なカウンタならもっと狭いスコープで宣言し使用するでしょう。わざわざグローバルに宣言した意味はあるのか、確認しないといけません。

名称がローマ字

次に宣言されているオブジェクトも残念です。

//ビンゴ数列を格納するオブジェクト
ArrayList <Integer> bingosuuretsu = new ArrayList<Integer>();

オブジェクト名をローマ字で宣言するのは止めましょう。

意味が間違っている

英語で宣言してあるから安心とは言えません。次のメソッドはすこし意味がずれています。

  createBingoPermutation(bingosuuretsu);

ビンゴ数列を作るメソッドの名称が「ビンゴ順列を作る」となっています。全くの間違いではありませんが、名称を「ビンゴ数列」に決めたわけですから、それに従うべきです。ですから「順列(Permutation⁠⁠」ではなく「数列(Progression⁠⁠」が適当です。

マジックナンバーを取り除く

マジックナンバーとは、コードを記述した本人には自明かもしれませんが、そのコードを読む第三者にとっては意味不明な、コードに記載された具体的数値のことです。マジックナンバーがコード中に散在するのは、明日のコード変更であっても、5分後のコード変更であっても害があります。よほど狭いスコープでない限り、極力除去しましょう。

シンボリック定数によるマジックナンバーの置き換え(Replace Magic Number with Symbolic Constant)

マジックナンバーをシンボリック定数に置き換えることで、コードの意味理解を助け、値変更の複雑さを緩和する。

Replace Magic Number with Symbolic ConstantMartin Fowler Refactoring Catalog

画面表示のための座標、

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_MODEもboolean型の定数なので、これも忘れずに設定してください。定数は変更がないから定数と呼ばれるのです。

リファクタリング実行の手順

取り上げた悪臭源である、不適切な名称を変更する場合は、次の手順で取り去ります。

  1. 適切な名称を考える。
  2. 不適切な名称の行をコピーし、直後にペーストする。同じ行が2つできる。
  3. 上の行をコメントアウトする。⁠いつでも復旧できるように)
  4. 下の行を適切な名称に変更する。
  5. テストモードで実行する。
  6. アプリモードで実行する。
  7. 5.6.で問題がなければ、上の行を削除する。
  8. リポジトリ(があれば)にコミットする。

これまで述べていませんが、バージョン管理システムを活用することをお勧めします。コードを過去の状態に戻したいことが必ずあります。変更の履歴や、変更箇所の比較ができるのもバージョン管理システム活用のメリットです。

演習

演習1(難易度:easy, but difficult)

sketch BingoMachine.pdeをリファクタリングしてください。施すリファクタリングは「名称の変更」「シンボリック定数によるマジックナンバーの置き換え」の2つです。「一度に一つ」の原則を守り、一つ名称を変更するたびにテストを実行してください。テストに合格したら次の名称を変更しましょう。こうしてすべての名称が適切に変更されたら、シンボリック定数への置き換えに入りましょう。作業そのものは至って単純で簡単です。しかし、適切な名称を選ぶ、適切な名称のシンボリック定数に置き換える、という作業は広い知識と高度な判断力を必要とします。"easy, but difficult"という難易度表示はそのような意味です。

まとめ

  • 「メソッドの抽出」⁠名称の変更」⁠シンボリック定数によるマジックナンバーの置き換え」の3種類のリファクタリングを学習しました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. 3種類のリファクタリングの意味が理解できましたか?
    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。
  2. 3種類のリファクタリングの効果を実感できましたか?
    1. 気持ちよく実感した。
    2. 実感した。しかし、なんとなくもやもやする。
    3. 実感できない。
  3. 演習を自力で完成できましたか?
    1. 完成できた。
    2. 部分的にはできたが、すべてを完成できなかった。
    3. 何をして良いのかわからない。

参考文献

  • 『新装版 リファクタリング―既存のコードを安全に改善する―(OBJECT TECHNOLOGY SERIES⁠⁠マーチン・ファウラー 著、オーム社
    • かつてピアソン・エデュケーション社から出版されていたものの新装版です。リファクタリングのバイブルですから、必携です。
  • 『Java言語で学ぶリファクタリング入門』⁠結城浩 著、ソフトバンククリエイティブ
    • ファウラーのバイブルと併せて読むことをお勧め。著者の解説は一級です。
  • vv

演習解答

  1. 以下にリファクタリングを施したsketchを示します。読者の皆さんの取り組みと異なるところもあるでしょう。リファクタリング前後で振る舞いに違いがなく、リファクタリング前よりも、より読みやすく、理解しやすいコードになっていれば結構です。

おすすめ記事

記事・ニュース一覧