C# テスト駆動開発の実践方法:読込・書込機能を抽象化する #14

当連載ではテスト駆動開発を実践するために必要な基礎知識や実践方法を解説しています。現在は実践編として簡単なアプリケーションを作成しながらテスト駆動開発のやり方を解説しています。

過去の記事については「C# テスト駆動開発の実践方法」から確認できますので、テスト駆動開発について詳しく知りたい人は参考にしてみてください。前回までに読込と書込の機能を作成してきました。今回はそれらの流れを汲みながら抽象化の作業をしていきます。

具象と抽象

具象と抽象については「C# オブジェクト指向の基礎」の「C# オブジェクト指向の基礎:インターフェースの基本を解説 #9」にて解説しています。ざっくりと言ってしまうと「コンポジションや継承を用いてより汎用的に使えるようにすること」と言い換えられます。要するに「すっぽりとほかの機能に置き換えられるようにする」ということでもあります。

今回はBankRepositoryクラスの中身を、将来を見据えて変更可能にしたいと考えています。現在はテキスト形式ですが、DBを使用したり、バイナリ形式にしたりする場合に、モジュール毎切り替えられるようにすることです。今回はインターフェースを用いて、上記のやりたいことを実現していきます。

インターフェースを定義する

では早速、インターフェースを定義していきたいと思います。前々回に作成した「Repositoryフォルダ」に対して、IRepository.csを作成していきます。IRepositoryには2つのメソッドを持たせたいと思います。

  • Read
  • Write

の2つのメソッドのみで十分です。これは読込と書込機能のうちでpublicに公開しているものだけを定義する形式にしています。インターフェースでpublicに公開するメソッドだけを定義しておけば、privateの細かい処理は具象クラスで必要に応じて組み立てることができるからです。IRepositoryインターフェースは以下のようにコーディングします。

using App03.ValueObjects;

namespace App03.Repository
{
    public interface IRepository
    {
        Money Read();

        void Write(Money money);
    }
}

Readメソッドは戻り値としてMoneyオブジェクトを返却し、WriteメソッドはMoneyオブジェクトを受けて外部に出力する仕様とします。このインターフェースを経由することで具象クラスを交換可能なモジュールへと昇華させます。

BankRepositoryクラスを変更する

インターフェースを定義したのでBankRepositoryクラスを修正していきます。定義したインターフェースを実装していきましょう。BankRepositoryクラスのクラス定義の部分を以下のように変更します。

public class BankRepository : IRepository

こうするだけでIRepositoryインターフェースを実装していることになります。すでにBankRepositoryクラスではReadとWriteメソッドを定義通りに実装しているので変更する必要はありません。

とはいえテストクラスは変更しておきたいと思います。テストクラスでの記述はBankRepositoryで処理するようになっているので、これを抽象クラスで実行させるように変更しておきます。テストプロジェクト内のRepositoryTestクラスを以下のように変更します。

[TestMethod]
public void ReadTest_OK()
{
    string path = @"C:\temp\balance.txt";
    string text = ((int)10000).ToString();
    File.WriteAllText(path, text);

    IRepository repos = new BankRepository(path);
    var money = repos.Read();
    Assert.AreEqual(money.Value, 10000);
}

[TestMethod]
public void ReadTest_NG()
{
    string path = @"C:\temp\balance.txt";
    string text = "テスト";
    File.WriteAllText(path, text);

    IRepository repos = new BankRepository(path);
    var money = repos.Read();
    Assert.AreEqual(money.Value, 0);
}

[TestMethod]
public void WriteTest_OK()
{
    string path = @"C:\temp\balance.txt";
    IRepository repos = new BankRepository(path);
    var money = new Money(1000);
    repos.Write(money);

    var balance = repos.Read();
    Assert.AreEqual(money.Value, balance.Value);
}

変更点は「var repos = new BankRepository(path);」と記述していた箇所を「IRepository repos = new BankRepository(path);」に変更したことになります。左側をIRepositoryとしておくことで、右側をIRepositoryを実装しているクラスであることが条件であることが明記できます。

この流れがテスト駆動開発でいうところの「リファクタリング」になるかなと思います。特に拡張性を設計に盛り込む場合には、インターフェースや抽象クラス等を用いて抽象の実装を想定しておかないと、修正時の工数が非常に大きくなってしまうので、将来的に代替される可能性のあるモジュールを作成する場合は抽象的な構想も考えておく必要があります。

上記、リファクタリングが完了したら最後にテストを実行しておき、すべてのテストで「グリーン」になることを確認しましょう。こういったリファクタリングを挟むときにテスト駆動開発は強みを発揮してくれます。「機能が確約されている」という安心感ですね。

ここまでの数回でアプリケーションに必要な周辺機能やオブジェクトの定義が完了しました。次回以降はアプリケーションの動作を形作る部分を作成していきたいと思います。テスト駆動開発の連載もいよいよ最終段階に入ってきましたので、最後まで頑張っていけたらいいなと思います。