さて、前回はすこし「よりみち」をしてDependencyServiceのおさらいをしました。
今回はPrismにおける、DependencyServiceの取り扱いについてお話ししたいと思います。
そのうえで、なぜそういった機構が必要なのかを解説したいと思います。
「DependencyServiceって何よ?」って方は、ぜひ一つ前のエントリーからお読みください。
なお、Prism for Xamarin.Forms入門、以下に目次がありますので、他のエントリーもご覧いただけると嬉しいです。
【Xamarin】Prism.Forms入門 目次 - nuits.jp blog
それでは早速進めましょう。
Quickstart
さて、本エントリーでは入力された文字列を読み上げる「Text to Speech」という機能利用した、「TextSpeaker」というアプリケーションを題材に説明していきたいと思います。
詳細な作りに関しては、前回のエントリーをご覧いただくか、Github上にコードを上げていますので「TextSpeaker.sln」のコードを参照してください。
まず、以下がXamarin.Formsの標準の方法でDependencyServiceを利用している箇所です。
public ICommand SpeachCommand { get; } public MainPageViewModel() { SpeachCommand = new DelegateCommand(() => { DependencyService.Get<ITextToSpeech>().Speak(Title); }); }
Prismでは上記の方法を推奨しておらず、以下の何れかの手法を推奨しています。(非推奨の理由は後ほど)
- IDependencyService(Iが付いている点に注意)をインジェクションさせて利用する
- DependencyServiceから取得しているインターフェース(前述の例ではITextToSpeech)を直接インジェクションさせて利用する
まずはIDependencyServiceをインジェクションさせる例を見てみましょう。
public ICommand SpeachCommand { get; } private readonly IDependencyService _dependencyService; public MainPageViewModel(IDependencyService dependencyService) { _dependencyService = dependencyService; SpeachCommand = new DelegateCommand(() => { _dependencyService.Get<ITextToSpeech>().Speak(Title); }); }
実行時、IDependencyServiceにはXamarin.Forms.DependencyServiceをラップしたインスタンスが渡されるため、インジェクションさせる箇所以外は、DependencyServiceを扱うのと同様に利用できます。
そして二つ目の、DependencyServiceから取得しているインターフェースを直接インジェクションさせている例が以下になります。
public ICommand SpeachCommand { get; } private readonly ITextToSpeech _textToSpeech; public MainPageViewModel(ITextToSpeech textToSpeech) { _textToSpeech = textToSpeech; SpeachCommand = new DelegateCommand(() => { _textToSpeech.Speak(Title); }); }
正直、これだけ見るとなぜわざわざPrismでこんな仕組み(とくに一つ目)を用意しているのか、疑問に思う方もいらっしゃるかと思います。
という事で、Prismの提供するDependencyServiceについて、もう少し掘り下げて見てみたいと思います。
Deep Dive
このプロジェクト内のDependencyService関連のクラスを抜き出して図示したのが以下のモデルです。
PCLプロジェクト内のMainPageViewModelは、ITextToSpeechインターフェースを利用します。
その際に、実体はDependencyServiceクラスを経由して取得します。
DependencyServiceクラスでは、実行されるプラットフォーム毎に、そのプラットフォーム用のITextToSpeechの実装クラスをインスタンス化して、MainPageViewModelへ返却してあげます。
この為、PCLプロジェクトからは各プラットフォームの実装を知らずにインターフェースのみで利用することが可能になっています。
さて、それでは一体何が課題なのでしょうか?
鋭い人はお気づきでしょう。
「MainPageViewModelがDependencyServiceクラスに直接依存していること」
が課題です。
これがなぜ良くないかというと
「DependencyServiceを直接利用しているとテストがやり難く、保守性が悪いから」
です。
こうなってしまうのは、いくつか要因があります。
- DependencyServiceクラスは利用前にXamarin.Forms.Forms.Initメソッドが呼び出されている必要がある
- Windowsデスクトップから利用可能なXamarin.Forms.Formクラスは存在しない?
- そもそも最初からXamarin.FormsはWindowsデスクトップをサポートしていないので、利用できてもすべきではない
と言う分けで、自動テストプロジェクトからDependencyServiceを利用するのは避けた方が良いでしょう。
もちろんXamarinの各プラットフォーム用のテストの仕組みを利用すれば可能でしょうが、PCLをテストするのにその手段は決して「テストしやすい」とは口が裂けても言えません。
ちなみにこれは、Xamarin.Formsの設計や実装が悪いという話ではありません。
この問題を解決するためには、アプリケーションのアーキテクチャの領域に踏み込む必要があります。
- モジュール間の結合には何を使うのか?DIコンテナ?ServiceLocator?
- DIコンテナを利用するとして、具体的にどのコンテナを使うのか?Unity?Autofac?Ninject?
- UnitTestの方針は?
つまり、ある程度アプリケーションの構築方法を限定する必要があります。
このため、Xamarin.Forms側で規定せず、アプリケーション側で決定できる余地を残しているのだと思います。思います!そんな事はどこにも書いてないですけどね。
実際、PrismではXamarin.Forms.DependencyServiceを利用しつつ、シンプルな形でこの問題を解決しており
「基盤を提供しつつアプリケーション側で柔軟に利用方法を選択できる」
という、絶妙ところに落ち着けていると思います。
実際にPrismのアプローチを図示したものが以下になります。
これは前述の一つ目のアプローチですね。
MainPageViewModelからDependencyServiceへ依存関係があったのが、IDependencyServiceへの依存に変更になっており、DependencyServiceへの依存はPrism内にカプセル化されています。
これによって、MainPageViewModelのテスト容易性が確保されました。
二つ目のアプローチ(ITextToSpeechをインジェクションさせるアプローチ)ではIDependencyServiceへの依存もなくなりますので、よりテストが容易になります。
総括
DependencyServiceの回はこれにて終了です。
テスト容易性云々の話は、少し分かり難かったかもしれません。
いずれどこかで、ユニットテストと絡めた説明ができたらと思います。
と言う分けで、今回はここまでです。
次回の内容は未定ですが、Commandの話を少しするかもしれません?
それではまた!