現在、UWP
の勉強の一環で、アプリを作成しています。
二つ浮かんで、一つは本当にしょうもないのですが、まぁ練習ですし。
エラーチェック等は入れたいので、Prism
を使っています。
そんな中、DIとDIコンテナ
についてまとめてみます。
なんだか、どんどんUWP
から離れている気がしますが…*1
まあ、知っておいたほうが良いことなので、まとめてみます。
DI(Dependency Injection)について
以前の記事でも書いていますが、DIは「依存性の注入」と呼ばれ、「そのクラスが使うものは、外部から渡す」ようにすることです。
そもそも、これは、依存関係逆転の原則(SOLID原則
のD
)の実現方法の一つです。
一般的には、「抽象化した、単純な機能のオブジェクト」を渡します。
よくある例
ログを取得する場合です。
とある作業を行った際、ログを残すとします。
雑にソースを書くと、以下のような感じですかね。
public class Sample { private TextLogWriter _logger = new TextLogWriter(); public bool DoSomething(){ bool success = TaskDo(); if(success){ _logger.Write("タスクは正常終了しました。") }else{ _logger.Write("タスクは異常終了しました。") } return success; } }
このクラスは、既に単体テストが自動化されています。
そして、これが各クラスに散りばめられている状況で、
ログは本番だと、データベースに入れるんだよ!
ということが判明したとします。
順調にすべての出現場所を調べ、
public class Sample { private SqlLogWriter _logger = new SqlLogWriter(); public bool DoSomething(){ bool success = TaskDo(); if(success){ _logger.Write("タスクは正常終了しました。") }else{ _logger.Write("タスクは異常終了しました。") } return success; } }
と修正し、満足したところ…ログを書いていたすべての単体テストが失敗しました。
原因は言うまでもなく、データベース接続でした。
DIを適用する
public class Sample { private SqlLogWriter _logger; public Sample(SqlLogWriter logger){ _logger = logger; } public bool DoSomething(){ bool success = TaskDo(); if(success){ _logger.Write("タスクは正常終了しました。") }else{ _logger.Write("タスクは異常終了しました。") } return success; } }
と直すこともできます。
しかし、これでは、
ログはやっぱり、Webサービス用意して、そっちに渡して!
というような事案に対応する場合、再び地獄を見ます。
あと、そもそも、上記クラスにとっては、どこにログを書くかは、重要な関心事ではないです。
ログを残せ、と指示されたところにログを残しているだけで、それを自分で決めているのはおかしな話です。*2
すなわち、「ログを書く」という機能を受け取れば、すっきりします。
public class Sample { private ILogWriter _logger; public Sample(ILogWriter logger){ _logger = logger; } public bool DoSomething(){ bool success = TaskDo(); if(success){ _logger.Write("タスクは正常終了しました。") }else{ _logger.Write("タスクは異常終了しました。") } return success; } }
単体テスト用のダミーオブジェクトは、以下のような感じになります。
public class FakeLogWriter: ILogWriter{ private List<string> _logs = new List<string>(); //インターフェイス実装 public void Write(string log) { _logs.Add(log); } //ログ内容の検証用 public IEnumerable<string> Logs{ get{ return _logs; } } }
DIを利用する利点
箇条書きですが、以下のようなものが挙げられます。
DIの欠点について
一言で言うと、「ソースが複雑化し、追いにくくなる」ことです。
DIで、依存関係を注入する方法としては、
- コンストラクタ注入
- メソッド注入
- プロパティセッター注入
があると言われていますが、コンストラクタ注入が最も良いと言われています。
理由は、初期化時点で格納したら、以降変更されないことです。
さて、上のほうで
一般的には、「抽象化した、単純な機能のオブジェクト」を渡します。
と書いた通り、細かく機能を分けていくと、当然、コンストラクタで渡すべきオブジェクトも増えます。
以下の例は、Prism
のサンプルの一部です。
public PaymentMethodPageViewModel(
IPaymentMethodUserControlViewModel paymentMethodViewModel,
ICheckoutDataRepository checkoutDataRepository,
IResourceLoader resourceLoader,
IAlertMessageService alertMessageService,
IAccountService accountService,
INavigationService navigationService)
{
_paymentMethodViewModel = paymentMethodViewModel;
_checkoutDataRepository = checkoutDataRepository;
_resourceLoader = resourceLoader;
_alertMessageService = alertMessageService;
_accountService = accountService;
_navigationService = navigationService;
SaveCommand = DelegateCommand.FromAsyncHandler(SaveAsync);
}
引数が6つ!
徹底的にやっていくと、こんな感じになっていくようです。
このクラスを使うためには、どこかで具象クラスのインスタンスを作ったり、事前準備したりと、ものすごく面倒な手順が必要になると思われます。*5
こんな面倒な設定をやってくれるのが、DIコンテナ
です。
DIコンテナについて
先述の通り、DIの導入によって複雑になりがちな設定を、代わりにやってくれる機能を指します。
IoCコンテナ
と呼ぶ場合もあります。
DI自体は、インターフェイスとコンストラクタ引数が使えれば、大抵の言語でそのまま適用できます。*6
一方、DIコンテナを自作しようとすると、大変な労力が必要であり、多くの場合はライブラリを使用します。
Java
ならSpring Framework
が該当するでしょうし、.NET
ならUnity
*7が該当します。
ライブラリによって、書き方は異なるでしょうが、Unity
の場合は、こんな感じです。(上記のサンプルから拝借)
Container.RegisterType<IAccountService, AccountService>(new ContainerControlledLifetimeManager()); Container.RegisterType<ICredentialStore, RoamingCredentialStore>(new ContainerControlledLifetimeManager()); Container.RegisterType<ICacheService, TemporaryFolderCacheService>(new ContainerControlledLifetimeManager()); Container.RegisterType<ISecondaryTileService, SecondaryTileService>(new ContainerControlledLifetimeManager()); Container.RegisterType<IAlertMessageService, AlertMessageService>(new ContainerControlledLifetimeManager());
こういう記述が何十行か続きます。
最終的には
var resourceLoader = Container.Resolve<IResourceLoader>();
…えっと、すみません。
黒魔術的なものですか!?
と思うぐらい、よくできています。
手動では面倒な、オブジェクトのライフサイクル管理についても、指定できます。
なお、DIコンテナは、最上層にあるクラス(画面がわかりやすい)から参照を辿り、必要なインスタンス全てを作成してくれます。
MVVMパターン
でいうと、ViewModel
だけでなくModel
も作成してくれますし、さらに深い階層に至るまで、全て作成されます。
使い方に関しては、まだまだ勉強が必要だと感じています。
DIコンテナを使うべきか
上で書いたように、DIコンテナは、まるで黒魔術のように参照を解決し、インスタンスを返してくれます。
じゃあ、これを常に使うべきかという問題が起こります。
結論から言うと、場合によるが、大規模なアプリケーションなら使ったほうが良いとなります。
DIコンテナを導入せず、単にDIを使うだけでも、テスト自動化等の恩恵にはあずかれます。
DIコンテナは、あくまで「設定を楽にするための機構」であるため、大して複雑ではないアプリケーションなら、不要でしょう。
むしろ、ソースの複雑性を上げる、可読性を下げる、生産性が下がる*9等の問題点が発生し得ます。
大規模な開発なら、初期に少々の学習コストを払ってでも、計画的に使っておくべきでしょう。
余談
ふと、自分が書いた記事を眺めていて、そもそも、依存性の反転についての記述がない!と気づきました。
それは、またの機会に…
あと、そもそもUWP関係なくね?とも思わなくもないです。次はいい加減、開発しているものを載せたい。
*1:UWPの本質とは、何ら関係ない
*2:コンストラクタで具象クラスを受け取る、ということは、「そのクラスじゃなきゃダメ!」ということ。今回はログを残すことが目的であるため、Text、SQL等の手段には言及する必要はない。
*3:この目的のためにDIを導入する場合が多い
*4:実装が抽象に依存する。依存関係逆転の原則。
*5:この機能をまとめたインターフェイスを用意する、という手もあるが、アンチパターンなのでよろしくない
*6:筆者が嫌いな言語であるVBAでも、できなくはない。ただ、DIを使うほど難しいシステムなら、VBAで作らないほうが良い
*8:このソースで解決するには、実際にはViewのXAMLを編集する必要がある。説明のために簡略化した。
*9:開発者全員が、DIコンテナについて知らなければならなくなる