目的
依存性注入(DI)体験ツアーへようこそ。
複雑化していくシステム開発の現場において、DIの導入はもはや不可避と言えるでしょう。
このツアーでは、依存性注入(Dependency Injection)と聞いて、よく聞くけどなんだか難しそうだと感じている方の為に、DIコンテナを使わずにシンプルにそのメリットを解説します。
言語はC#を用いますがどの言語にも応用が利く話で、特定のDIコンテナに依存しない汎用的な知識としてお伝えします。
それでは参りましょう。
最もシンプルな依存性注入の例
まずは、依存性注入されていない例から始めます。
依存性注入されていない例
Userのリストを取得する為のUserLogicクラス、そしてデータベースへのアクセスを隠蔽する為のUserDaoクラスがあります。
UserLogic -> UserDao -> MyDbContext
という参照関係にあります。
// モデルクラス(簡略化しています)
class User
{
public string Id { get; set; }
public string Name { get; set; }
}
// ロジッククラス
class UserLogic
{
public IEnumerable<UserDto> GetList()
{
var dao = new UserDao();
return dao.GetList();
}
}
// データアクセスクラス
class UserDao
{
public IEnumerable<UserDto> GetList()
{
MyDbContext db = new MyDbContext();
return db.Users.Select(
user=> new User(){ Id = user.Id, Name = user.Name }).ToList();
}
}
// メイン処理
class Application
{
static void Main()
{
var logic = new UserLogic();
var users = logic.GetList();
foreach (User user in users)
{
Console.WriteLine($"Id={user.Id}, Name={user.Name}");
}
}
}
大まかな流れは、メイン処理の中を見るとわかるかと思いますが、UserLogicに「Userのリストをちょうだい」とお願いすると、UserLogicの中でUserDaoが生成され、UserDaoの中でMyDbContextが生成され、DBからデータを取得した後、遡ってUserのリストが呼び元に戻されていく、という感じになります。
UserDaoがMyDbContextを生成しないようにしたい
UserDaoは、内部でMyDbContextを生成して利用していますが、トランザクション管理を考えるとこれではいろいろと不都合があることが分かり、MyDbContextは外部から与えるように変更する必要が出てきました。
そこで、UserDaoに対してMyDbContextを依存性注入するよう変更します。
具体的には、UserDaoのコンストラクタにMyDbContextを渡すようにします。
// データアクセスクラス
class UserDao
{
private MyDbContext _db; // メンバに持つ
public UserDao(MyDbContext db){
_db = db; // コンストラクタ引数からMyDbContextを受け取る
}
public IEnumerable<UserDto> GetList()
{
return _db.Users.Select(
user=> new User(){ Id = user.Id, Name = user.Name }).ToList(); // メンバ変数の_dbを使う
}
}
これで、UserDaoはMyDbContextの生成の管理責任から解き放たれました。
コンストラクタで渡されたMyDbContextを、好きな時に使いっぱなしにすればOKです。
UserDaoは、MyDbContextの生成に関する仕様変更に、今後一切影響されません。独立性が向上したことで再利用性が高まり、テストもしやすくなり、バグも入り込みにくくなりました。
これを「依存性注入」と呼び、コンストラクタ経由で注入することを「コンストラクタ注入(コンストラクタ・インジェクション)」と呼びます。一般的に、依存性注入にはコンストラクタ注入が用いられます。
依存性の注入(Dependency Injection)とは、難しく考えなければこの程度のことです。
UserDaoの変更に合わせてUserLogicも変更する
さて、UserDaoのコンストラクタの仕様が変わりましたので、それを呼び出しているUserLogicも変更しましょう。
// ロジッククラス
class UserLogic
{
public IEnumerable<UserDto> GetList()
{
var db = new MyDbContext(); // UserDaoの為のMyDbContextを生成
var dao = new UserDao(db); // UserDaoにdbを渡す
return dao.GetList();
}
}
おや? これはいけません。UserLogicはこれまでMyDbContextとは完全に切り離されてきたのに、UserDaoがその生成責任を外部に丸投げしてしまったので、UserLogicがその責務を負う形になってしまいました。
それでは、UserLogicにも同様にMyDbContextを依存性注入してみましょう。
// ロジッククラス
class UserLogic
{
private MyDbContext _db; // メンバ変数にMyDbContextを持つ
public UserLogic(MyDbContext db)
{
_db = db; // コンストラクタ引数経由でMyDbContextを受け取る
}
public IEnumerable<UserDto> GetList()
{
var dao = new UserDao(_db);
return dao.GetList();
}
}
…なんだか変ですね。あまりよくなったように見えません。なぜUserLogicが、MyDbContextのインスタンスをメンバに持っていなければいけないのでしょうか? UserLogicには全然関係ないのに…。
そうです、これは依存性注入するインスタンスを間違えています。
UserLogicに依存性注入すべきは、UserDao自身です。UserDaoの生成責任をUserLogicが負っているから、このような事になっているのです。
さあ、UserLogicにUserDaoを依存性注入するよう変更しましょう。
// ロジッククラス
class UserLogic
{
private UserDao _dao;
public UserLogic(UserDao dao)
{
_dao = dao;
}
public IEnumerable<UserDto> GetList()
{
return _dao.GetList();
}
}
元通り、UserLogicからMyDbContextへの依存が消えました。そして、UserDaoの生成責任からも解き放たれています。
UserLogicからUserDaoへの依存をインタフェースで制限する
UserLogicをよく見ると、UserDaoの生成責任から解き放たれたことにより、もはやUserDaoのGetList()というメソッドにしか依存しなくなっています。
どうせなら、UserDaoへの依存をもっと下げ、UserDaoの実装が存在しなくてもコンパイルできるようにしてしまいましょう。
// UserDaoインタフェース
interface IUserDao
{
IEnumerable<UserDto> GetList();
}
// データアクセスクラス
class UserDao : IUserDao // インタフェースを実装
{
private MyDbContext _db;
public UserDao(MyDbContext db){
_db = db;
}
public IEnumerable<UserDto> GetList()
{
return _db.Users.Select(
user=> new User(){ Id = user.Id, Name = user.Name }).ToList();
}
}
// ロジッククラス
class UserLogic
{
private IUserDao _dao; // インタフェースに変更
public UserLogic(IUserDao dao) // インタフェースに変更
{
_dao = dao;
}
public IEnumerable<UserDto> GetList()
{
return _dao.GetList();
}
}
このように、依存性注入するオブジェクトをインタフェースで参照することで、注入される側のクラスが、注入する側の実装に影響されないようにすることができます。
これは例えばUserLogicの実装者とUserDaoの実装者が異なっている時や、UserDaoの実装がまだ終わっていない時にUserLogicをコンパイルしなくてはならない時などに便利です。
また、IUserDaoを実装するFakeUserDaoをテスト用に作ってそれをUserLogicに注入してテストを行うということも、UserLogicを全くいじらずにできるようになります。
クラスの独立性が高いということはこういうメリットがあり、依存性注入はそれを強く推し進めることができます。
DIで何が良くなったのか
これまでの変更をメイン処理にも反映すると、以下のようになります。
// メイン処理
class Application
{
static void Main()
{
// 注入する各インスタンスの生成
var db = new MyDbContext();
var dao = new UserDao(db);
var logic = new UserLogic(dao);
// 処理の実行
var users = logic.GetList();
foreach (User user in users)
{
Console.WriteLine($"Id={user.Id}, Name={user.Name}");
}
}
}
これまで各所に散らばっていた「インスタンスの生成に関する仕様」が、このMain()に全て集まっていることがお分かりでしょうか。
依存性注入を用いると、最終的にはこのように、インスタンスの生成に関する仕様を一点に集中させることができます。
このようになっていると、例えば一部をテスト用のインスタンスに差し替えたり、一時的にロジックを差し替えたり、といったことが、各クラスにまったく手を加えずにできるようになります。
また、ここでは詳しく説明していませんが、アプリケーション全体で共有するインスタンス(これまでシングルトンパターンで実装していたようなもの)も、必要な箇所にここで注入することができるようになります。
シングルトンパターンは一見便利ですが、これはグローバル変数であり、ある意味「システムの全てがこのシングルトンに依存する」状態に成り得ます。システムが複雑化すればするほどこのシングルトンへの変更を行いづらくなり、「ここはもう触ってはいけない」という聖域になっていきます。
しかし、依存性注入にしておけば、必要な箇所でのみそれらを参照することができ、場合によっては「このクラスにだけは別のインスタンスを渡して処理をする」というような事も可能になります。
まとめ
依存性注入のメリットが分かっていただけたでしょうか。
依存性注入は決して難しいものではありません。DIコンテナがなくても実現可能なものであり、むしろ特定のDIコンテナになるべく依存させないよう、サービスロケーターパターンを極力避けるべきものです(サービスロケーターパターンについては興味があったら調べてみてください)。
参考: LazyでDependency Injectionを遅延評価してサービスロケーターパターンを避ける
おまけ:DIコンテナを使うとどうなるのか
おまけとして、ASP.NETでDIコンテナを使うとどうなるのか、一例をお見せします。
先ほど作ったUserLogic、UserDaoは、以下のようにStartup.csでDIコンテナに登録しておきます。
DIコンテナでは一般的に、管理対象のインスタンスを「サービス」と呼びます。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<MyDbContext>(
options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
services.AddTransient<UserLogic>();
services.AddTransient<IUserDao, UserDao>();
}
そうしておくと、あとは何もしなくても、コントローラーのコンストラクタ引数に自動的に注入してくれます。
public class UserController : Controller
{
private readonly UserLogic _logic;
public UserController(UserLogic logic) // UserLogicをコンストラクタ注入
{
_logic = logic;
}
public IActionResult GetList()
{
var users = _logic.GetList();
return View(users);
}
}
いやはや便利!
「DIコンテナがなくとも依存性注入は実現できる」とは書きましたが、DIコンテナがあった方が格段に便利なのは間違いありません。
DIコンテナは、例えば「HTTPリクエスト単位でインスタンスを1度だけ生成して使いまわす」とか、「システムで1回だけ生成して使いまわす(シングルトン)」などの、「インスタンスのライフタイム管理」も行ってくれます。
これで興味を持たれた方は、ぜひDIを始めてみてください。
旧ASP.NET MVCでもDIコンテナは使えます
ASP.NET MVC Core(.NET Core/5+用のMVCフレームワーク)には、上記で紹介したMicrosoft.Extensions.DependencyInjectionが標準で統合されていますが、旧ASP.NET MVC(.NET Framework版)には標準のDIコンテナはありません。
しかし、旧ASP.NET MVCにマイクロソフトのDIコンテナを統合することは可能です。
以下の記事にまとめてありますので、よろしければご活用下さい。