「MyUtilクラス」のような、多くのクラスから参照されるユーティリティクラスは、業務システム開発では良く見かけるものだと思います。
開発の序盤、このMyUtilクラスはコンストラクタ引数がなく、MyUtilクラスを仕様する側のクラスで、次のように生成して使うようになっていることが多いです。
public class MyClass {
private MyUtil _util;
public MyClass() {
// MyUtilを生成
_util = new MyUtil();
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
ところが、このMyUtilクラスはその後何年も様々なシステムで利用されることになり、システム毎に微妙に異なる処理を求められるようになってきます。
すると、初期化の為に様々なオプションが必要になってきます。これもまた、よく見かける風景だと思います。
public class MyClass {
private MyUtil _util;
public MyClass() {
// MyUtilを初期化
_util = new MyUtil( true, 5, MyUtilMode.InDevelopment, "jp" );
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
そして、リファクタリングを重ね、ある時に、「パラメータは設定ファイルから読み込めるようにしよう」となります。
public class MyClass {
private MyUtil _util;
public MyClass() {
// 設定を読み込む
MySettings settings = new MySettings( @"D:\hogehoge\settings.json" );
// MyUtilを初期化
_util = new MyUtil( settings );
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
しかし、このような設定ファイルの情報をMyClassのような個々のクラスが意識するのは本来おかしいことです。
この状況を改善する為、ある時、「MyUtilを生成するためのグローバル関数(Singleton取得関数)を用意したので、それを呼び出すようにしてください」といわれるのもまた、いつもの風景でしょう。
public class MyClass {
private MyUtil _util;
public MyClass() {
// MyUtilを初期化
_util = MySystemGlobal.GetMyUtil();
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
しかし、これによってMyClassは、MyUtilクラスだけでなく、MySystemGlobalクラスにも依存するようになってしまいました。
もはや、このシステム以外ではコードレベルでコピペしてしてすら再利用することができない、汎用性の低いソースコードになってしまっています。
シンプルになったように見えて、このシステムの複雑度は以前より増しています。
そしてこのような事の積み重ねによって、システムはどんどん「ゴミの山」に埋もれていくのです。
では、本来はどうするべきだったのでしょうか。
MyClassはMyUtilの生成仕様に関心がない
MyClassはそもそも、MyUtilの生成に関する仕様に関わる必要がありません。MyUtilのいくつかのメソッドにのみ用事があり、「そのシステムの為の初期化処理」などは、そのシステムの設定時に解決してほしいことのはずです。
なぜMyClassがその尻拭いをしなくてはいけないのでしょうか。
その根本的な部分に手を入れず、グローバル関数やグローバル定数のような誤魔化しでソースコードを複雑にしていくのは、現代では非常に古いやり方だと言わざるを得ません。
MyClassからMyUtilの生成に関する仕様を分離する方法の一つに、「コンストラクタ・インジェクション(コンストラクタ注入)」という手法があります。
つまり、生成した後のインスタンスをコンストラクタから受け取ることで、利用したいクラスの生成に関する仕様を意識しないようにします。
public class MyClass {
private MyUtil _util;
public MyClass(MyUtil util) {
// MyUtilを初期化
_util = util;
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
非常にすっきりしました。このMyClassにはもはやMySystemGlobalは存在せず、適切に初期化したMyUtilさえ渡せば正しく動作します。コードから余計な部分が消えることで、コードの目的も明確になり、メンテナンス性も向上しました。
これが、「Dependency Injection(依存性の注入)」と呼ばれるものです。
わかってみればとても単純な話です。
しかし、Dependency Injectionを良く知らない開発者がなぜ上記のようなコンストラクタ・インジェクションを用いないかというと、結局それは、MyClassの呼び出し側に生成責任を押し付けているだけで、問題の解決になっていないと考えているのではないでしょうか。
確かにMyClassの修正によって、それを呼び出すMyControllerは以下のように複雑なものになってしまいました。
public class MyController : Controller {
public async Task<IActionResult> GetSometing(MyModel model) {
MyClass mc = new MyClass(
new MyUtil(
new MySettings( @"D:\hogehoge\settings.json" )
)
);
model.Result = mc.GetSomething(model.A, model.B);
return View(model);
}
}
こんな状況よりは、MyClassの中でグローバル関数を使ってMyUtilを生成する方が、よほどシンプルだ、ということでしょう。
ではこの問題を、DIはどう解決しているのでしょうか。
全てをDIにする
DIは、一部だけ対応させてもあまり意味がありません。
この場合ですと、MyControllerも、必要なインスタンスをDIすればよいのです。
MyControllerは、MyClassを生成するためにMyUtilが必要で、さらにそのMyUtilを生成するためにMySettingsが必要、という状況になっています。
しかし、MyControllerが本来必要なのはMyClassだけです。ですから、MyClassだけをDIします。
public class MyController : Controller {
private MyClass _mc;
public MyController(MyClass mc) {
_mc = mc;
}
public async Task<IActionResult> GetSometing(MyModel model) {
model.Result = _mc.GetSomething(model.A, model.B);
return View(model);
}
}
こちらも非常にシンプルになりました。
このように、各クラスが必要なインスタンスを外部から受け取る形で実装を進めていくと、各クラスの実装は非常にシンプルになる代わりに、クラス呼び出し階層の根本部分に「各クラスの生成仕様」が集まってきます。
さて、上記のようにMyControllerを書いたことで、MyControllerを呼び出す側が、MyController、MyClass、MyUtil、MySettings全ての生成仕様を担うことになりました。
そして近年では、多くのミドルウェア/フレームワークで、この部分を自動化する仕組みが備わっています。
例えばASP.NET Coreでは、MyControllerの呼び出しはフレームワークが行ってくれます。この時、ASP.NET Coreは、MyControllerのコンストラクタ引数を「DIコンテナ」と呼ばれる機能を使って自動的に生成してくれます。
DIコンテナには、事前に「その型のクラスを生成するときはどうするのか(=クラスの生成仕様)」を登録しておきます。
ASP.NET Coreの場合、これはStartup.csで定義します。
まずは、MySettingsの生成仕様をDIコンテナに定義します。
services.AddScoped<MySettings>(p => new MySettings( @"D:\hogehoge\settings.json" ));
MySettingsの型に対して、こうやって生成するんだよと定義します。シンプルですね。
次に、MyUtilの生成に関する仕様も教えてあげます。
services.AddScoped<MyUtil>();
型名しか教えてあげていないことに驚く人もいるかもしれません。MyUtilだって、引数にMySettingsがあるのですから、ちゃんと「MySettingをどうやって生成するか」を教えてあげないとダメなはず・・・あっ。そうです、それはもうさっき教えていますね。
DIコンテナは賢いので、コンストラクタ引数にある「知ってる型」のインスタンスは、既知の方法で生成してくれます。
最後に、MyClassの生成に関する仕様も伝えます。
もうわかりますよね。
services.AddScoped<MyClass>();
これで、MyClass→MyUtil→MySettingsのように、連鎖的にDIコンテナが生成してくれるようになりました。
ちなみに、
services.AddScoped<MyController>();
については、services.AddControllers()
が、全てのコントローラ・クラスについてDIコンテナへの登録を行ってくれるので、明示的に教える必要はありません。
何がよくなったのか
1. MySettingsの生成仕様がStartup.csに集約された
MySettingsに与える「@"D:\hogehoge\settings.json"」は、そのシステム固有のものです。このようなシステム固有の仕様を個別のクラスにばらまくのはアンチ・パターンです。
それを回避するためにグローバル定数やグローバル関数を用いてその仕様を隠蔽したとしても、結局その定数や関数に個別のクラスが依存していることに変わりはなく、根本的な問題解決になっていません。
DIによって、各クラスはシステム固有の仕様から解き放たれました。
2. MyUtilの生成仕様に利用側が依存しなくなった
MyUtilのような共通機能クラスは多くのクラスから利用されている為、仕様変更の影響範囲が非常に大きく、生成に関する仕様が各クラスから分離されたことは大きなメリットと言えるでしょう。グローバル定数、グローバル関数からも分離されました。
これによってMyClassのような各クラスは、特定の設定で生成されたMyUtilに依存するのではなく、任意の設定で初期化されたMyUtilを利用可能となりました。
これはテストの際にも有用ですし、別のシステムや処理へのMyClassの再利用も促進します。
3. そのクラスの本来の責務に集中することができるようになった
1.や 2.の「生成仕様の分離」によって、各クラスのコードから本来の責務以外のコードがなくなり、コードの見通しが良くなりました。これは生産性に直結し、不具合の混入を減らし、開発スピードを高めます。
トータルで見た時、大きなコスト削減となる上に、プログラマーにかけていた余計な負担を軽減してくれるでしょう。
まとめ
これまでの流れから、以下のことがわかります。
- 必要なインスタンスの生成仕様をコンストラクタ・インジェクションで分離すると、クラスがとてもシンプルになり、メンテナンス性・再利用性が大きく向上する。
- クラスの呼び出し階層全てにそれを適用していくと、最終的に階層の根本部分に「各クラスの生成仕様」が集約される。
- 集約された「各クラスの生成仕様」は、DIコンテナを利用することでシンプルに管理することができる。
DIの話をすると、2.や3.の話まで行くまでに、1.の時点で「そんなことをやっていたら、クラスのコンストラクタが引数だらけになって逆に使いにくくなる」という人が出てきます。
しかし、2.や3.までやれば、それは杞憂だとわかるでしょう。
もしそれでもまだ「クラスのコンストラクタが引数だらけになった」というのであれば、恐らくそのクラスの責務が大きすぎるのでしょう。適切に責務を分離すべきクラスが判明し、むしろ良い兆候と言えます。
サービスロケーターパターンについて
DIのメリットを享受する目的でDIコンテナを用いているシステム開発の現場で、やってはいけない「サービスロケーター」というアンチパターンがあります。
クラスのコンストラクタ引数が増えてくると、不安になるのか、次のようなコードを書く人がいます。
public class MyClass {
private IServiceProvider _serviceProvider;
public MyClass(IServiceProvider serviceProvider) {
// DIコンテナを取得して保存
_serviceProvider = serviceProvider;
}
public string GetSomething(int a, int b)
{
MyUtil util = _serviceProvider.GetService<MyUtil>();
return _util.GetABValue( a, b );
}
}
IServiceProviderは、ASP.NET CoreにおけるDIコンテナのインタフェースです。
この「GetService()」を呼び出すと、T型のクラスをDIコンテナから取得してくれます。
上記のように、DIコンテナだけをコンストラクタ・インジェクションし、他のクラスは必要に応じてDIコンテナから取得するようにすれば、いちいちコンストラクタ引数を変更せずとも、気軽にDIできて超便利、というわけです。
このやり方を「サービスロケーターパターン」といい、これはDIにおけるアンチパターンと言われています。
この方法を用いると、クラスがDIコンテナに依存してしまいます。
何のためにDIしたかというと、そのクラスから「生成に関する仕様」を分離する為でした。DIコンテナは「生成に関する仕様」の一部です。そのシステムがDIコンテナを使っているという「前提」に依存してしまい、もはやこのクラスはDIコンテナ無しでは利用できなくなってしまいました。
正しくDIしているクラスと比較すると、その問題が良くわかります。
public class MyClass {
private MyUtil _util;
public MyClass(MyUtil util) {
// MyUtilを初期化
_util = util;
}
public string GetSomething(int a, int b)
{
return _util.GetABValue( a, b );
}
}
上記のMyClassを利用する場合、別にDIコンテナは必須ではありません。MyUtilを自前で生成してコンストラクタに渡せば良いのです。
しかし、引数にDIコンテナだけを渡すMyClassは、そのコンストラクタだけ見ても、一体MyClassの中で何のインスタンスが必要なのかわかりません。
ソースコードの中身を見て、「MyUtilをDIコンテナから取得している」ことを突き止めた上で、正しく初期化したDIコンテナを引数に渡さなければ、MyClassは動いてくれません。
何年も経てば、もはやDIコンテナを正しく初期化する方法を知っているものはいなくなり、DIコンテナの初期化コードを触れるものは誰一人いなくなるでしょう。
このような方法で作られたものは、いくらDIコンテナを使っていても「DI」とは呼びません。
もし誰かがサービスロケーターパターンを使って「DIだ」と言い張っていたら、やさしく「それはアンチパターンだ」と教えてあげましょう。
あわせて読みたい