依存性の注入(DI)について解説してみる
1. はじめに
こんにちは、エンジニアの末武です。
最近は主に製品のユニットテストの整備を行っているので、ユニットテストを書く上で重要な概念である依存性の注入(DI)について解説してみようと思います。
2. 依存性の注入とは
依存性の注入というのは、「Dependency Injection(DI)」の直訳です。パッと見で何のことかが非常にわかりづらいですが、やりたいこととしては「依存性があるプログラムは保守・テストがしづらいので、それを解決する」ということです。
では依存性というのは何かというと、よく「プログラムが別のプログラムに依存している状態」を指す言葉として説明されますが、これもまた強烈にわかりづらいですね。
私は「プログラムが別のプログラムの「メソッドの実装やインスタンス変数」に依存している」と解釈して考えています。
具体的には↓のようなコードが「依存性のあるコード」とされるものです。
public class Cart
{
private string userId;
public Cart(string userId)
{
this.userId = userId;
}
public bool CheckValidCart()
{
var userRepository = new UserRepository();
var isAvailableUser = userRepository.CheckAvailableUserForPurchase(this.userId);
return isAvaliable;
}
}
public class UserRepository
{
public bool CheckAvailableUserForPurchase(string userId)
{
///DBの会員情報にアクセスして有効かを判定するコードを記述
}
}
このコードにおいて、CartクラスのCheckValidCartはUserRepositoryに依存しています。より正確に表現するなら「CheckValidCartメソッドの実装は、UserRepositoryのCheckAvailableUserForPurchaseメソッドの実装に依存している」です。
CheckValidCartメソッドのテストを書くならば↓のようになりますが、色々と問題があります。
[TestClass()]
public class CartTest
{
[TestMethod()]
public void CheckValidCart()
{
var cart = new Cart("0001");
var isValidForPurchase = cart.CheckValidCart();
// テスト用のDBのデータによって成否が変わる
isValidForPurchase.IsTrue("failed message");
}
}
参照するDBのデータによって結果が変わることとなり、ユニットテストとして不安定になり、テスト用のDBのデータが変更された場合などに、「製品コードは問題ないのにテストが失敗する」などの事態が発生します。(テスト用のDBをしっかり管理すればテスト自体は可能ですが、管理するリソースが増える・他のリソースへアクセスするためテスト実行が遅くなるなど、いずれにせよユニットテストとしては課題があります)
3. 依存性の注入を行う
先ほど作成したユニットテストは、外部リソース(DB)を参照していて不安定かつパフォーマンスも良くありません。
解決するためには、CheckValidCartの実装からDBアクセスへの依存を無くす必要があります。そのために行うのが「依存性の注入」であり、CartクラスにUserRepositoryのインタフェースを渡すことで、UserRepositoryの実装を切り話します。
public class Cart
{
private string userId;
private IUserRepository userRepository;
public Cart(string userId, IUserRepository userRepository)
{
this.userId = userId;
this.userRepository = userRepository;
}
public bool CheckValidCart()
{
var isAvailableUser = this.userRepository.CheckAvailableUserForPurchase(this.userId);
return isAvaliable;
}
}
public interface IUserRepository
{
bool CheckAvailableUserForPurchase(string userId);
}
public class UserRepositoryDb : IUserRepository
{
public bool CheckAvailableUserForPurchase(string userId)
{
//DBの会員情報にアクセスして有効かを判定するコードを記述
}
}
public class UserRepositoryMock : IUserRepository
{
public bool CheckAvailableUserForPurchase(string userId)
{
// テスト用のデータを返すようにする
}
}
これにより、CartはUserRepositoryのインターフェース(stringを引数にboolを返す)のみに関心を持つようになり、UserRepositoryの実装(DBアクセス)には依存しないようなテストを書くことができます。
製品コードではCartのコンストラクタに「UserRepositoryDb」、ユニットテストでは「UserRepositoryMock」を渡すことで、テスト時にはUserRepositoryDbの実装に依存せずにテストをすることができるようになります。
4. まとめ
「依存性の注入(Dependency Injection)」と書くと、かなりわかりづらいので私は「オブジェクト(実装)の注入」という風に解釈しています。他のクラスのデータ(インスタンス変数)や実装(各メソッドの処理)に依存している部分を、オブジェクトやインターフェースを対象クラスにコンストラクタやSetterメソッドで外部から渡せるようにすることで解決するものです。
ユニットテストのコーディングで力を発揮するように書きましたが、DIを行うことでクラス間の結合が疎結合になり、プログラム自体のメンテナンス性も向上することになるので、リファクタリングなどをする際には意識して実践したいテクニックだと思いました。