これから始めるSpring.NET その2

前回 ではクラス間の依存関係の切り離し方とSpring.NETの役割について説明しました。
では、そもそもクラス間の依存関係を切り離す事にどのような利点があるのでしょうか?

インターフェースで分離する事による利点でよく言われるのは多態性ポリモーフィズム)でしょう。インターフェース経由でオブジェクトを参照しているため、以下の例のように実装クラスを自由に入れ替えられるというものです。

例1
class Hoge {
    private IExample obj;
    
    public IExample Example {
        get { return obj; }
        set { obj = value; }
    }
}

interface IExample {
}
class Example1 : IExample {
}
class Example2 : IExample {
}

static class Program {
    static void Main() {
        Hoge hoge = new Hoge();
        // どちらでも入れられる
        hoge.Example = new Example1();
        hoge.Example = new Example2();
    }
}

しかし、こんな事が必要になる状況がそうあるでしょうか?否、滅多にありません。
このようなオブジェクト構造の場合、実装クラスを自由に入れ替えられるという事に、利点はほとんど無いのです。

では、インターフェースを用いて依存関係を切り離す事による利点はどこにあるのでしょうか?
クラスの実装をどのように変更しても、インターフェースを変更しない限り他のクラスに(表面上は)影響を与えない事でしょうか?それもありますが、一番の利点はなんと言っても、単体テストがし易くなるということでしょう。

では、単体テストがし易くなるとは具体的にどういうことでしょうか?以下の例を見てください。

例2

interface IProductService {
    /// <summary>
    /// 指定した商品名の商品コードを取得します。
    /// </summary>
    /// <param name="productName">商品名</param>
    /// <return>商品コード</return>
    string GetProductCode(string productName);
}
class ProductService : IProductService {
    public string GetProductCode(string productName) {
        // データベースにアクセスして、商品名に関連づいたコードを取得する。
        return "商品コード";
    }
}

interface ICustomerService {
    /// <summary>
    /// 指定したコードの商品の顧客の一覧を取得します。
    /// </summary>
    /// <param name="productCode">商品コード</param>
    /// <return>顧客の一覧</return>
    string[] GetCustomersByProductCode(string productCode);
}
class CustomerService : ICustomerService {
    public string[] GetCustomersByProductCode(string productCode) {
        // データベースにアクセスして、商品コードに関連付く顧客の一覧を取得する。
        return new string[] { "顧客1", "顧客2" };
    }
}

class Customer {
    private string productCode;
    private string customerName;
    
    public string ProductCode {
        get { return productCode; }
        set { productCode = value; }
    }
    public string CustomerName {
        get { return customerName; }
        set { customerName = value; }
    }
}
class BizLogicFacade {
    private IProductService productService;
    private ICustomerService customerService;
    
    public IProductService ProductService {
        get { return productService; }
        set { productService = value; }
    }
    public ICustomerService CustomerService {
        get { return customerService; }
        set { customerService = value; }
    }
    
    /// <summary>
    /// 指定した商品の顧客の一覧を取得します。
    /// </summary>
    /// <param name="productName">商品名</param>
    /// <return>顧客の一覧</return>
    public IEnumerable<Customer> GetCustomersByProductName(string productName) {
        var pCode = ProductService.GetProductCode(productName);
        
        foreach(var customer in CustomerService.GetCustomersByProductCode(pCode)) {
            yield return new Customer() {
                ProductCode=pCode, CustomerName=customer
            };
        }
    }
}

まず、商品名から商品コードを取得できるProductServiceクラスと商品コードから顧客の一覧を取得できるCustomerServiceクラスがあります。それぞれIProductServiceインターフェースとICustomerServiceインターフェースを実装しています。

そして、これら二つのクラスを参照するBizLogicFacadeクラスがあります。このクラスは二つのクラスを組み合わせて、商品名から商品コードを取得し、商品コードから顧客の一覧を取得し、それらをCustomerクラスにラップして返します。

では、このBizLogicFacadeクラスを単体テストするにはどうすればいいでしょうか?普通に考えれば以下のようにするでしょう*1

例3
[Test]
public void Test() {
    ProductService pService = new ProductService();
    CustomerService cService = new CustomerService();
    
    BizLogicFacade bizLogic = new BizLogicFacade();
    bizLogic.ProductService = pService;
    bizLogic.CustomerService = cService;
    
    List<Customer> customers = new List<Customer>(
        bizLogic.GetCustomersByProductName("Hoge")
    );
    // 5 件返ってくる
    Assert.That(customers, Has.Property("Count", 5));
    Assert.That(customers[0].ProductCode, Is.EqualTo("001")):
    // 1, 2 件目を調べる
    Assert.That(customers[0].CustomerName, Is.EqualTo("ジョン"));
    Assert.That(customers[1].CustomerName, Is.EqualTo("ボブ"));
}

GetCustomersByProductCodeメソッドの結果として5 件返ってくると想定し、その値をAssertしています。

このテストを正常に動作させるためには、ProductServiceクラスとCustomerServiceクラスがこちらの期待する結果を返すように細工をしなくてはいけません。この場合双方のクラスともデータベースから情報を取得して結果を返すので、データベース上のデータをそのように構成しておく必要があります。

さて、ここではBizLogicFacadeクラスの単体テストを行いたいだけで、他のクラスが期待通りに振舞うようにお膳立てをするのはかなり面倒くさい作業です。それにこれらのクラスの実装が変更され返す結果が変わったりすると、もろに影響を受けてしまいます。BizLogicFacadeクラスが受け取ったデータを正常に加工するかどうかをテストしたいだけなのに。

そんな時、インターフェースで依存関係を分離した事が生きてきます。
ProductSerivceクラス、CustomerServiceクラス共にインターフェース経由で参照している為、自由に実装を取り替える事ができます*2。そして、単体テスト時はこちらが期待する結果のみを返す、以下のようなモック(はりぼて)クラスを定義して、それに入れ替えてしまえばいいのです。

例4
class ProductServiceMock : IProductService {
    public string GetProductCode(string productName) {
        return "001";
    }
}

また、NMockというライブラリを使えばモッククラスをいちいち作らなくても、動的にモックオブジェクトを作る事ができます。

例5

[Test]
public void Test() {
    IProductService pService = new DynamicMock(typeof(IProductService));
    ICustomerService cService = new DynamicMock(typeof(ICustomerService));
    
    BizLogicFacade bizLogic = new BizLogicFacade();
    bizLogic.ProductService = (IProductService)pService.MockInstance;
    bizLogic.CustomerService = (ICustomerService)cService.MockInstance;
    
    // GetProductCode メソッドが呼び出され、引数にHoge、返り値として001が返されるという意味
    pService.ExpectAndReturn("GetProductCode", "001", "Hoge");
    
    var customerNames = new[] {
        "ジョン", "ボブ", "キャサリン", "リッチー", "ほげ"
    };
    // GetCustomersByProductCode メソッドが呼び出され、引数に001、返り値としてcustomerNames が返されるという意味
    cService.ExpectAndReturn("GetCustomersByProductCode", customerNames, "001");
    
    List<Customer> customers = new List<Customer>(
        bizLogic.GetCustomersByProductName("Hoge")
    );
    // 5 件返ってくる
    Assert.That(customers, Has.Property("Count", 5));
    Assert.That(customers[0].ProductCode, Is.EqualTo("001")):
    // 1, 2 件目を調べる
    Assert.That(customers[0].CustomerName, Is.EqualTo("ジョン"));
    Assert.That(customers[1].CustomerName, Is.EqualTo("ボブ"));
    
    // メソッドが呼び出されたかチェック
    pService.Verify(); cService.Verify();
}

とまぁ、前置きが長くなりましたがインターフェースで分離するとこんな感じで単体テストがし易くなります(このクラスを単体テストする必要があるかどうかという問題は別として)。

次からSpring.NETの基本的な使い方について解説しようかと思います。

*1:単体テストフレームワークにはNUnit 2.4 系列を使用しています

*2:あれ?結局これが利点になってる