.NET Tools
NUnit入門 Test Firstのススメ

4.唐突な仕様変更

(株)ピーデー
川俣 晶
2002/02/23
Page1 Page2 Page3 Page4 Page5

 さて、ここで偉い人が「よし、ハイスコアはトップの10個を保持した方が面白いぞ!」と思い付いたとしよう。そんな仕様変更をいまごろいわれても……、と思いながらも、それを実装することになったとしよう。つまり、ハイスコア・クラスはこれまでのように1個のポイントではなく、10個のポイントを保存しなければならない。しかし、すでにハイスコア・クラスは、ソースのあちこちから参照されていて、中身を書き換えて、もし挙動が変わってしまったりすると、面倒なことが起きる。「動いているものには触らない」というポリシーを持っているプログラマーも多いと思う。だが、それを書き換えねばならなくなったとしたら……。

 機能追加によって新規に増えるメソッドはともかく、既存のメソッドの挙動が変わることは絶対に避けたい。しかし、どうすればそれが実現できるだろうか。

 1つの方法が、ソースを書き換えた後で、NUnitで確認するという方法だ。テスト・メソッドは、実装の詳細とは関係なく、メソッドの表面的な挙動をチェックしている。ならば、どんどん書き換えても、テストさえ通れば、挙動は変わっていないと見なせる。テストはすべて自動化されているので、実行するのは簡単である。いちいちファイルを指定するのが面倒なら、引数にファイルを指定して起動するバッチを作ることもできる。それにより、安心してプログラムの書き換えに着手できる。つまり、NUnitと“Test First”は、すでに稼働しているソースを書き換えるための勇気を与えてくれるのである。

 以下が書き直したソースである。

 1: using System;
 2: using System.Collections;
 3:
 4: namespace MyGame
 5: {
 6:   public class HighScore
 7:   {
 8:     private const int numberOfScores = 10;
 9:     class NamedScore : IComparable
10:     {
11:       public string name = "NoName";
12:       public int point = 0;
13:       public int CompareTo( object obj )
14:       {
15:         return ((NamedScore)obj).point - point;
16:       }
17:       public NamedScore( string name, int point )
18:       {
19:         this.name = name;
20:         this.point = point;
21:       }
22:     }
23:     private ArrayList scores;
24:     public string name
25:     {
26:       get { return ((NamedScore)scores[0]).name; }
27:     }
28:     public int point
29:     {
30:       get { return ((NamedScore)scores[0]).point; }
31:     }
32:     public void setScore( string name, int point )
33:     {
34:       if( ((NamedScore)scores[numberOfScores-1]).point >= point ) return;
35:       scores.Add( new NamedScore( name, point) );
36:       scores.Sort();
37:       scores.RemoveAt(numberOfScores-1);
38:     }
39:     public bool isHighest( int point )
40:     {
41:       return this.point < point;
42:     }
43:     public HighScore()
44:     {
45:       scores = new ArrayList();
46:       for( int i=0; i<numberOfScores; i++ )
47:       {
48:         scores.Add( new NamedScore("NoName", 0 ) );
49:       }
50:       scores.Sort();
51:     }
52:   }
53: }
仕様変更に伴い修正したHighScoreクラス
上位10人のポイントを保持するように修正している。外部仕様は変更していないため、すでに作成したテスト・クラスでテストすることができる。

 ハイスコア機能にあまり手間を掛けたくなかったので、安易な解決方法を採ってみた。System.Collections.ArrayListクラスのSortメソッドを使って、上位10人のポイントを並べ替えるような仕組みを作ってみたのである。Sortメソッドは配列にも用意されているが、ソート計算時には新しいスコアを入れて11人になり、固定長の配列には合わないような気がして、あえてSystem.Collections.ArrayListクラスを使ってみたのである。

 さっそくNUnitで確認して、テストをパスするまでソースを手直しした。すでにハイスコア・クラスを使用しているプログラム・コードにおいても、致命的な問題は起こしていないはずだ。

 なお、新規に作成するクラスやpublicなメソッドに関しても、本当ならテスト・メソッドを用意すべきだが、今回は説明の都合上割愛した。

もっと手直しをしよう

 上のソースはあまりきれいではない。特にArrayListを使った結果、キャストがあちこちに入り込んでいる。何とかならないものかと思ったが、よく考えると、コレクションの長さを変えないで処理する方法があることに気付いたとしよう。新しいポイントを追加するときに、ベスト10に入るかどうかだけ確認して、入るときには、第10位の人の代わりに新しく追加する人を入れて、それからソートしてしまえばよいわけだ。これなら固定長なので、配列で処理できる。配列で処理できればキャストは不要だ。このほかpublicなメンバ変数などもすべて書き直してみた。書き直した結果がこれだ。

 1: using System;
 2: using System.Collections;
 3:
 4: namespace MyGame
 5: {
 6:   public class HighScore
 7:   {
 8:     private const int numberOfScores = 10;
 9:     class NamedScore : IComparable
10:     {
11:       private string _name = "NoName";
12:       private int _point = 0;
13:       public string name
14:       {
15:         get { return _name; }
16:       }
17:       public int point
18:       {
19:         get { return _point; }
20:       }
21:       public int CompareTo( object obj )
22:       {
23:         return ((NamedScore)obj).point - point;
24:       }
25:       public NamedScore( string name, int point )
26:       {
27:         _name = name;
28:         _point = point;
29:       }
30:     }
31:     private NamedScore [] scores;
32:     public string name
33:     {
34:       get { return scores[0].name; }
35:     }
36:     public int point
37:     {
38:       get { return scores[0].point; }
39:     }
40:     public void setScore( string name, int point )
41:     {
42:       if( scores[numberOfScores-1].point >= point ) return;
43:       scores[numberOfScores-1] = new NamedScore( name, point);
44:       Array.Sort( scores );
45:     }
46:     public bool isHighest( int point )
47:     {
48:       return this.point < point;
49:     }
50:     public HighScore()
51:     {
52:       scores = new NamedScore[numberOfScores];
53:       for( int i=0; i<numberOfScores; i++ )
54:       {
55:         scores[i] = new NamedScore("NoName", 0 );
56:       }
57:       Array.Sort( scores );
58:     }
59:   }
60: }
さらに修正してきれいにしたHighScoreクラス
ArrayListクラスの使用をやめ、配列によりベスト10を管理するように大幅に修正している。外部仕様は変更していない。

 見比べていただけばすぐ分かると思うが、相当大胆な変更だ。しかし、プログラムの働きという意味では、前のソースでも問題はなかったはずだ。従って書き換えの結果は、ソースがきれいになっただけである。「動いているものには触るな」というポリシーを持つ人なら、きっと手を出さないだろう。しかし、自動テストの環境がしっかり作られていれば、実装内容の書き換えは決してリスクの高い行為ではない。それよりも、必要に応じてきめ細かくソースの内容を変更していく勇気が与えられるのは非常に重要だ。

 現実のソフトウェア開発では、仕様変更は必ず発生するものであり、何度も変わり続ければソースはどんどん読みにくくなる。読みにくくなったソースを読みやすいように書き直すことは、開発をスムーズに進めるためには必用な作業なのである。このようなソースの修正はリファクタリングと呼ばれるが、何の準備もなくいきなりソースを書き換えればバグはやすやすと入り込む。簡単に自動テストを何度でも実行できる環境を用意することによって、バグが入り込みにくいソース修正が可能になるのである。

まとめ

 さて、NUnitの効用が何となく見えてきただろうか。NUnitは自動テストを行う方法を提供する。自動テストは、ソースを書き換える勇気を与えてくれる。ソースを書き換える精神的負担や時間が軽減されれば、ソフトウェアの質を高めることが容易になる。例えば、同じ処理をソースの別の個所に見つけたら、それを1カ所にまとめることができる。そうすれば、保守性も高くなるし、ソースも短くシンプルになって分かりやすさにもプラスになる。筆者の過去の経験からいっても、自動テストを作成した方が、いろいろな意味でプログラマーの負担が軽いように感じられる。

 自動テストは、ちょっと見た目には遠回りに思えるかもしれない。だが、自動テストとは、道路をきちんと舗装するようなものだ。いったん舗装してしまえば、何倍ものスピードで、しかも軽い負担で走ることが可能になる。NUnitという手軽なツールもあることだし、ぜひ試してみてはどうだろうか?End of Article

関連記事(Insider.NET内)
ソフト開発を成功させる1つの方法
 
関連リンク
NUnitの入手先

関連書籍
XPエクストリーム・プログラミング入門 ソフトウェア開発の究極の手法
(ISBN:4-89471-275-X)
XPエクストリーム・プログラミング導入編 XP実践の手引き
(ISBN:4-89471-491-4)
XPエクストリーム・プログラミング実践記 開発現場からのレポート
(ISBN:4-89471-469-8)
XPエクストリーム・プログラミング実行計画
(ISBN:4-89471-341-1)
 
 

 INDEX
  [.NET Tools]
  NUnit入門 Test Firstのススメ
     1.まず環境を準備する
     2.何はさておき、最初にテストを書こう
     3.NUnitを実行する
   4.唐突な仕様変更
 
インデックス・ページヘ  「.NET Tools」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間