.NET Tools
NUnit入門 Test Firstのススメ [NUnit 2.0対応版]

5.唐突な仕様変更に対応する

(株)ピーデー 川俣 晶
2003/04/26

唐突な仕様変更

 プログラム開発において、最初に「これこれこういう機能を作ってくれ、頼む」と言われてその通りに作って終わりになるとは限らない。非常に高い確率で、「そこはこう変更してくれ」と言われる。しかも、どんな変更を要求されるか予測できない場合も多い。最初の要求を実現するために書かれたソース・コードは、簡単に変更を余儀なくされてしまうのである。

 この最大データ・サイズ記録クラスに対しても、このような機能変更の要求が出てきた。「最大データ・サイズを要求した顧客のみを記録するだけでは不十分だ。『要求された大きなデータのベスト10』を記録してほしい」と、そんなことを言われたとしよう。そんな仕様変更をいまごろいわれても……、と思いながらも、それを実装することになったとしよう。つまり、最大データ・サイズ記録クラスはこれまでのように1個の整数値ではなく、10個の整数値を保存しなければならない。しかし、すでに最大データ・サイズ記録クラスは、コードのあちこちから参照されているため、中身を書き換えて、もし挙動が変わってしまったりすると、面倒なことが起きる。「動いているものには触らない」というポリシーを持っているプログラマーも多いと思う。だが、それを書き換えねばならなくなったとしたら……。

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

 1つの方法が、コードを書き換えた後で、NUnitで確認するという方法だ。テスト・メソッドは、実装の詳細とは関係なく、メソッドの表面的な挙動だけをチェックしている。ならば、どんどん書き換えても、テストさえパスすれば、挙動は変わっていないと見なせる。テストはすべて自動化されているので、実行するのは簡単である。いちいちファイルを指定するのが面倒なら、引数にファイルを指定して起動するバッチを作ることもできる。Nunit-Guiは開くと前回読み込んだDLLを自動的に開くので、それを活用するのもよいだろう。また、VS.NETのプロジェクト設定の中で、プロジェクト実行時にNunit-Guiを実行してしまうように設定することも可能である。

 これにより、安心してプログラムの書き換えに着手できる。つまり、NUnitと“Test First”は、すでに稼働しているコードを書き換えるための勇気を与えてくれるのである。

 以下が書き直したコードである。

using System;
using System.Collections;

namespace ServerApp
{
  public class MaxDataSizeRecorder
  {
    private const int numberOfPoints = 10;
    class NamedPoint : IComparable
    {
      public string Name = "NoName";
      public int Point = 0;
      public int CompareTo( object obj )
      {
        return ((NamedPoint)obj).Point - Point;
      }
      public NamedPoint( string name, int point )
      {
        this.Name = name;
        this.Point = point;
      }
    }
    private ArrayList points;
    public string Name
    {
      get { return ((NamedPoint)points[0]).Name; }
    }
    public int Point
    {
      get { return ((NamedPoint)points[0]).Point; }
    }

    public void SetPoint( string name, int point )
    {
      if( ((NamedPoint)points[numberOfPoints-1]).Point >= point ) return;
      points.Add( new NamedPoint( name, point) );
      points.Sort();
      points.RemoveAt(numberOfPoints-1);
    }
    public bool IsHighest( int point )
    {
      return this.Point < point;
    }
    public MaxDataSizeRecorder()
    {
      points = new ArrayList();
      for( int i=0; i<numberOfPoints; i++ )
      {
        points.Add( new NamedPoint("NoName", 0 ) );
      }
    }
  }
}
10個のデータを記録するようにしたMaxDataSizeRecorderクラス(C#版)
 
Imports System.Collections

Public Class MaxDataSizeRecorder
  Private Const numberOfPoints As Integer = 10
  Class NamedPoint
    Implements IComparable
    Public Name As String = "NoName"
    Public Point As Integer = 0
    Public Function CompareTo(ByVal obj As Object) As Integer Implements IComparable.CompareTo
      Return obj.Point - Point
    End Function
    Public Sub New(ByVal name As String, ByVal point As Integer)
      Me.Name = name
      Me.Point = point
    End Sub
  End Class

  Private points As ArrayList

  Public ReadOnly Property Name() As String
    Get
      Return points(0).Name
    End Get
  End Property

  Public ReadOnly Property Point() As Integer
    Get
      Return points(0).Point
    End Get
  End Property

  Public Sub SetPoint(ByVal _name As String, ByVal _point As Integer)
    If points(numberOfPoints - 1).Point >= _point Then Exit Sub
    points.Add(New NamedPoint(_name, _point))
    points.Sort()
    points.RemoveAt(numberOfPoints - 1)
  End Sub

  Public Function IsHighest(ByVal _point As Integer) As Boolean
    Return Me.Point < _point
  End Function

  Public Sub New()
    points = New ArrayList()
    Dim i As Integer
    For i = 0 To numberOfPoints - 1
      points.Add(New NamedPoint("NoName", 0))
    Next
  End Sub
End Class
10個のデータを記録するようにしたMaxDataSizeRecorderクラス(VB.NET版)

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

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

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

もっと手直しをしよう

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

using System;

namespace ServerApp
{
  public class MaxDataSizeRecorder
  {
    private const int numberOfPoints = 10;
    class NamedPoint : IComparable
    {
      private string name = "NoName";
      private int point = 0;
      public string Name
      {
        get { return name; }
      }
      public int Point

      {
        get { return point; }
      }
      public int CompareTo( object obj )
      {
        return ((NamedPoint)obj).point - point;
      }
      public NamedPoint( string name, int point )
      {
        this.name = name;
        this.point = point;
      }
    }
    private NamedPoint [] points;
    public string Name
    {
      get { return points[0].Name; }
    }
    public int Point
    {
      get { return points[0].Point; }
    }
    public void SetPoint( string name, int point )
    {
      if( points[numberOfPoints-1].Point >= point ) return;
      points[numberOfPoints-1] = new NamedPoint( name, point);
      Array.Sort( points );
    }
    public bool IsHighest( int point )
    {
      return this.Point < point;
    }
    public MaxDataSizeRecorder()
    {
      points = new NamedPoint[numberOfPoints];
      for( int i=0; i<numberOfPoints; i++ )
      {
        points[i] = new NamedPoint("NoName", 0 );
      }
    }
  }
}
ArrayListクラスをやめて配列により10個のデータを記録するようにしたMaxDataSizeRecorderクラス(C#版)
 
Public Class MaxDataSizeRecorder
  Private Const numberOfPoints As Integer = 10
  Class NamedPoint
    Implements IComparable
    Private _name As String = "NoName"
    Private _point As Integer = 0

    Public ReadOnly Property Name() As String
      Get
        Return _name
      End Get
    End Property

    Public ReadOnly Property Point() As Integer
      Get
        Return _point
      End Get
    End Property

    Public Function CompareTo(ByVal obj As Object) As Integer Implements IComparable.CompareTo
      Return obj.Point - _point
    End Function

    Public Sub New(ByVal name As String, ByVal point As Integer)
      Me._name = name
      Me._point = point
    End Sub
  End Class

  Private points(numberOfPoints - 1) As NamedPoint

  Public ReadOnly Property Name() As String
    Get
      Return points(0).Name
    End Get
  End Property

  Public ReadOnly Property Point() As Integer
    Get
      Return points(0).Point
    End Get
  End Property

  Public Sub SetPoint(ByVal _name As String, ByVal _point As Integer)
    If points(numberOfPoints - 1).Point >= _point Then Exit Sub
    points(numberOfPoints - 1) = New NamedPoint(_name, _point)
    Array.Sort(points)
  End Sub

  Public Function IsHighest(ByVal _point As Integer) As Boolean
    Return Me.Point < _point
  End Function

  Public Sub New()
    Dim i As Integer
    For i = 0 To numberOfPoints - 1
      points(i) = New NamedPoint("NoName", 0)
    Next
  End Sub
End Class
ArrayListクラスをやめて配列により10個のデータを記録するようにしたMaxDataSizeRecorderクラス(VB.NET版)

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

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

まとめ

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

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

 今回の解説は、あくまでNUnitの使い方の入り口にすぎない。また、Test Firstの先にはTest-Driven Developmentというキーワードもあるらしい。いまこそ、あらためてテストについて注目してみてもよいのではないだろうか?End of Article

 

 INDEX
  [.NET Tools]
  NUnit入門 Test Firstのススメ [NUnit 2.0対応版]
     1.NUnitの環境を準備する
     2.何はさておき、最初にテストを書こう
     3.空のメソッドを書いてNUnitを実行してみる
     4.プログラム本体を実装する
   5.唐突な仕様変更に対応する
 
インデックス・ページヘ  「.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 記事ランキング

本日 月間