nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Prism for Xamarin.Forms入門 DependencyService with Prism

さて、前回はすこし「よりみち」をしてDependencyServiceのおさらいをしました。
今回はPrismにおける、DependencyServiceの取り扱いについてお話ししたいと思います。
そのうえで、なぜそういった機構が必要なのかを解説したいと思います。

「DependencyServiceって何よ?」って方は、ぜひ一つ前のエントリーからお読みください。

www.nuits.jp

なお、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では上記の方法を推奨しておらず、以下の何れかの手法を推奨しています。(非推奨の理由は後ほど)

  1. IDependencyService(Iが付いている点に注意)をインジェクションさせて利用する
  2. 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関連のクラスを抜き出して図示したのが以下のモデルです。

f:id:nuitsjp:20160905005523p:plain

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のアプローチを図示したものが以下になります。

f:id:nuitsjp:20160912153505p:plain

これは前述の一つ目のアプローチですね。
MainPageViewModelからDependencyServiceへ依存関係があったのが、IDependencyServiceへの依存に変更になっており、DependencyServiceへの依存はPrism内にカプセル化されています。
これによって、MainPageViewModelのテスト容易性が確保されました。
二つ目のアプローチ(ITextToSpeechをインジェクションさせるアプローチ)ではIDependencyServiceへの依存もなくなりますので、よりテストが容易になります。

総括

DependencyServiceの回はこれにて終了です。
テスト容易性云々の話は、少し分かり難かったかもしれません。
いずれどこかで、ユニットテストと絡めた説明ができたらと思います。

と言う分けで、今回はここまでです。 次回の内容は未定ですが、Commandの話を少しするかもしれません?
それではまた!