SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

UWP挑戦記 その3 DIとDIコンテナについて

現在、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を利用する利点

箇条書きですが、以下のようなものが挙げられます。

  • そのクラスが要求する機能が明確になる
  • 変更に強くなる
  • 単体テスト時に、環境等に依存しにくくなる*3
  • ライブラリ間の依存が減る*4

DIの欠点について

一言で言うと、「ソースが複雑化し、追いにくくなる」ことです。

DIで、依存関係を注入する方法としては、

  • コンストラクタ注入
  • メソッド注入
  • プロパティセッター注入

があると言われていますが、コンストラクタ注入が最も良いと言われています。
理由は、初期化時点で格納したら、以降変更されないことです。

さて、上のほうで

一般的には、「抽象化した、単純な機能のオブジェクト」を渡します。

と書いた通り、細かく機能を分けていくと、当然、コンストラクタで渡すべきオブジェクトも増えます。

以下の例は、Prismのサンプルの一部です。

github.com

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>();

という一文で、必要なインスタンス等ができています。*8

…えっと、すみません。
黒魔術的なものですか!?

と思うぐらい、よくできています。
手動では面倒な、オブジェクトのライフサイクル管理についても、指定できます。

なお、DIコンテナは、最上層にあるクラス(画面がわかりやすい)から参照を辿り、必要なインスタンス全てを作成してくれます。
MVVMパターンでいうと、ViewModelだけでなくModelも作成してくれますし、さらに深い階層に至るまで、全て作成されます。

使い方に関しては、まだまだ勉強が必要だと感じています。

DIコンテナを使うべきか

上で書いたように、DIコンテナは、まるで黒魔術のように参照を解決し、インスタンスを返してくれます。

じゃあ、これを常に使うべきかという問題が起こります。

結論から言うと、場合によるが、大規模なアプリケーションなら使ったほうが良いとなります。

DIコンテナを導入せず、単にDIを使うだけでも、テスト自動化等の恩恵にはあずかれます。
DIコンテナは、あくまで「設定を楽にするための機構」であるため、大して複雑ではないアプリケーションなら、不要でしょう。 むしろ、ソースの複雑性を上げる、可読性を下げる、生産性が下がる*9等の問題点が発生し得ます。

大規模な開発なら、初期に少々の学習コストを払ってでも、計画的に使っておくべきでしょう。

余談

ふと、自分が書いた記事を眺めていて、そもそも、依存性の反転についての記述がない!と気づきました。
それは、またの機会に…
あと、そもそもUWP関係なくね?とも思わなくもないです。次はいい加減、開発しているものを載せたい。

*1:UWPの本質とは、何ら関係ない

*2:コンストラクタで具象クラスを受け取る、ということは、「そのクラスじゃなきゃダメ!」ということ。今回はログを残すことが目的であるため、Text、SQL等の手段には言及する必要はない。

*3:この目的のためにDIを導入する場合が多い

*4:実装が抽象に依存する。依存関係逆転の原則。

*5:この機能をまとめたインターフェイスを用意する、という手もあるが、アンチパターンなのでよろしくない

*6:筆者が嫌いな言語であるVBAでも、できなくはない。ただ、DIを使うほど難しいシステムなら、VBAで作らないほうが良い

*7:ゲームエンジンではない

*8:このソースで解決するには、実際にはViewのXAMLを編集する必要がある。説明のために簡略化した。

*9:開発者全員が、DIコンテナについて知らなければならなくなる