2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Dependency Injectionを使って、クラスから文脈(コンテキスト)を分離する

Last updated at Posted at 2022-11-14

はじめに

Dependency Injectionの「依存性」とは、何に対する依存なのだろうか。

そこがふわっとしたままでは、「依存性の注入」と言われても、どういうシーンで使えば良いのかイメージしづらい。

この記事ではDIを「文脈(コンテキスト)に対する依存性」という観点から見て、そのメリットとインパクトの大きさを説明する。

コンテキストとは、例えばWebシステムにおける「セッション」などが該当する。あるプログラムが、セッションのログイン状態に依存した動作をする場合、それは「セッションというコンテキストに依存している」と言える。

また、そのシステムが動作しているシステムの「環境設定」なども該当する。あるプログラムが、環境設定から取得したDB接続文字列を用いてデータベースのオープンを行っている場合、そのプログラムは「環境設定というコンテキストに依存している」のである。

コンテキストを変更すれば、依存しているプログラムの挙動も変化する。
逆に言えば、あるプログラムが特定のコンテキストに依存した挙動を行う場合、そのようなプログラムが増えれば増えるほど、そのコンテキストを変更した場合の影響範囲が広がっていく、ということだ。

そのようなプログラムは、そのコンテキスト以外で利用することはできないだろう。

だから、できる限り、特定のコンテキストに依存しないプログラムの書き方をすべきである。
そのようにして作られたプログラムは、コンテキストに対して堅牢・安全であり、テストしやすく、様々なシーンで再利用できるものとなる。

そして、その為に活躍するのがDependency Injection、ということだ。

Dependency Injectionを採用する時、どのインスタンスを注入し、どのインスタンスを注入すべきでないかを判断する基準として、「コンテキストに依存するインスタンスかどうか」を用い、コンテキストに依存する場合にはそれをDIすることで、対象のクラスをコンテキストに依存しない形で記述できるようになる。

例を挙げて説明しよう。

システム日付に依存するプログラム

商品を出荷した際に、現在日時を出荷日としてDBに記録する出荷プログラムがあるとする。

class ItemShipment {
	... 省略 ...

	public void Ship(ItemID id){
		// システム日付を出荷日として取得
		var shippingDate = DateTime.Now();
		
		// 出荷処理
		... 
	}
}

この時、そのプログラムが出荷日をシステム日付から直接取得していると、そのプログラムは「サーバーのシステム日付」という環境、つまりコンテキストに依存していることになる。

このプログラムに対して「8月15日に出荷した場合のテスト」を行いたければ、そのサーバーのシステム日付自体を8月15日に変更しなければならない。

かといって、出荷プログラムのShipメソッドの引数にDateTimeを追加するのはおかしい。
「出荷日」を取得する責務は出荷プログラムのものだからである。出荷日を外部から得るようにすると、外部が「出荷日」というものに対する責務を負うことになる。それでは仕様とは異なるし、恐らく、その「外部のプログラム」が、同様の問題を抱えるだけになるだろう。

この場合の問題は、DateTime.Now()の呼び出しが、実行環境というコンテキストに依存していることだ。
出荷プログラムの仕様は「サーバーマシンのシステム日付を出荷日として記録する」ではなく、「現在日時を出荷日として記録する」である。

現在日時は、環境というコンテキストに依存している。
であれば、現在日時のインスタンスをクラス内で取得するのではなく、現在日時という型を定義し、そのインスタンスをDIすべきだ。

現在日時とは静的なものというより、実行時に決まるリアルタイムなものなので、それはデータというより関数オブジェクトの形をとるだろう。
C#でいえば、それは引数無しの、DateTime型を返すデリゲートとして表現される。

// 現在日時を取得するデリゲートの定義
public delegate DateTime CurrentTime();
// DIコンテナに現在日時の生成仕様を定義
services.AddSingleton<CurrentTime>(() => DateTime.Now());
class ItemShipment {
	private readonly CurrentTime _currentTime;

	... 省略 ...

	public ItemShipment(CurrentTime currentTime){
		// 現在日時を注入
		_currentTime = currentTime;
	}

	public void Ship(ItemID id){
		// 現在日時を出荷日として取得
		var shippingDate = _currentTime();
		
		// 出荷処理
		... 
	}
}

これを見て、「現在日時を扱う全てのクラスのコンストラクタ引数にCurrentTimeを追加する気か? 正気なのか?」と思う人もいるかもしれない。

しかし、いきなり最初から、現在日時をDIする必要はない。現在日時の扱いにDIが必要になりそうな時点で、それをコンストラクタ引数から受け取れるように変更すればよい。

それよりも重要なことは、ItemShipmentの呼び出し側のクラスの実装を、内部でItemShipmentを直接生成せず、DIするように最初から作っておくことだ。
そうなっていれば、上記の変更はどこにも影響を与えずに行うことができる。

セッション中のログイン情報に依存するプログラム

「ログインユーザーの出社時間を記録する」というユースケースがあったとする。

class EmpAttendance {
	... 省略 ...

	public void Attend() {
		string = empid = _loginUser.EmpId;
		
		// 社員IDに対して出社時間を記録する
	}
}

そのユースケースを実現するプログラムでは、何らかの形で「ログインユーザー」という情報をどこからか取得する必要がある。

Webシステムであれば、それは通常、セッションオブジェクトの中に持たせることだろう。
ASP.NET Coreならば、HttpContext である。

そうなると、上記のプログラムはDIを用いて以下のように実装されることになるかもしれない。

class EmpAttendance {
	... 省略 ...
	
	private IHttpContextAccessor _httpContextAccessor;

	public EmpAttendance(IHttpContextAccessor httpContextAccessor){
		// HttpContextAccessorを保持
        _httpContextAccessor = httpContextAccessor;
	}

	public void Attend() {
        // HttpContextからLoginUserを取得
        var loginUser = new LoginUser(_httpContextAccessor.HttpContext);
		string = empid = _loginUser.EmpId;
		
		// 社員IDに対して出社時間を記録する
	}
}

ASP.NET Coreでは、IHttpContextAccessorは自動的にDIコンテナから取得できる為、上記のプログラムはそのまま動作する。

しかしそもそも、EmpAttendanceは「ログインユーザーはHttpContextに保持されている」という仕様を知っているべきだろうか。

EmpAttendanceが知りたいのは「ログインユーザーの情報」であって、それがセッション内にあるかどうかなどに関心を持つべきではない。その仕様は「システム環境という文脈によって決まったもの」であり、今回実現したいユースケースとは無関係である。

だから、LoginUserを取得するためのLoginUserRepositoryを、次のように単純にDIすればよい(いちおうインタフェース経由としている)。

// DIコンテナにLoginUserの生成仕様を定義
services.AddScoped<ILoginUserRepository, LoginUserRepository>();
// LoginUserリポジトリのインタフェース
interface ILoginUserRepository {
	ILoginUser Get();
}

// LoginUserリポジトリの実装
class LoginUserRepository : ILoginUserRepository {
	private IHttpContextAccessor _httpContextAccessor;

	public LoginUserRepository(IHttpContextAccessor httpContextAccessor){
		// HttpContextAccessorを保持
        _httpContextAccessor = httpContextAccessor;	
	}

	public ILoginUser Get() => new LoginUser(_httpContextAccessor.HttpContext);
}
class EmpAttendance {
	... 省略 ...
	
	private ILoginUserRepository _loginUserRepos;

	public EmpAttendance(ILoginUserRepository loginUserRepos){
		_loginUserRepos = loginUserRepos;
	}

	public void Attend() {
        // リポジトリからLoginUserを取得
        var loginUser = _loginUserRepos.Get();

		string = empid = _loginUser.EmpId;
		
		// 社員IDに対して出社時間を記録する
	}
}

このように定義されたEmpAttendanceは、ログインユーザーがどこにあるかという仕様から完全に分離される。
リポジトリから取得したログインユーザーを処理をするだけである。

もしこのクラスをテストしたければ、必要な情報を持つログインユーザーを生成するダミーのリポジトリを渡せば良いだろう。いちいちHttpContextにログインユーザーの情報を設定してからテストをするような面倒なことをする必要はない。

まとめ

Dependency Injectionを、「コンテキストからの分離」という観点で利用する記事をまとめてみた。

「テストしやすくする為」とか「内部処理を外側から切り替える為」なとのようなふわっとした観点とは違い、この観点からDIを利用すると、システム全体を包括する、プログラムの作り方自体が変わるようなインパクトがあると思っている。

サンプルを2件掲載しているが、今後さらに追加するかもしれない。

他に良い活用シーンがあれば、コメント欄で教えて頂ければ幸いである。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?