sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# CommunityToolkit.Mvvm の学習8 IoC

IoC (Inversion of control)

MVVM パターンを使用するアプリケーションのコードベースのモジュール性を高めるに利用できる一般的なパターンは、なにか「制御の反転 (Inversion of control)」を使用することです。

もっとも一般的なソリューションのひとつは、「依存性の注入 (dependency injection)」です。バックエンドの class に注入されるサービスの作成で、依存関係を注入します。(viewmodel のコンストラクターにパラメータとして渡す)

これにより、サービスを使用するコードは、実装の詳細に依存しません。また、これらのサービスの具体的な実装を交換することが簡単になります。また、このパターンは、サービスを通じて抽象化することで、プラットフォーム固有の機能をバックエンドのコードで実装できるようにすることもできます。

MVVM Toolkit は、このパターンを使うことを簡単にするような仕組みの API を提供していません。Microsoft.Extensions.DependencyInjection パッケージのような、専用ライブラリがすでに存在するからです。

このライブラリは、完全な機能を備えた強力な DI の API を提供し、簡単にセットアップ/使用できる IServiceProvider として機能します。以下のガイドでは、ライブラリを参照して MVVM パターンを使ってアプリケーションにまとめる例を示します。

APIs:

サービスの構成と解決 (Configure and resolve services)

最初のステップは IServiceProvider を宣言します。(通常はスタートアップのタイミングで)必要なすべてのサービスを初期化します。例えば、UWP は次のとおり(他のフレームワークでも同じようにセットアップできる):

public sealed partial class App : Application
{
    public App()
    {
        Services = ConfigureServices();

        this.InitializeComponent();
    }

    /// <summary>
    /// 現在使用している <see cref="App"/> インスタンスを取得します。
    /// </summary>
    public new static App Current => (App)Application.Current;

    /// <summary>
    /// アプリケーションサービスを解決するための <see cref="IServiceProvider"/> インスタンスを取得します。
    /// </summary>
    public IServiceProvider Services { get; }

    /// <summary>
    /// アプリケーションのサービスを設定します。
    /// </summary>
    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<IFilesService, FilesService>();
        services.AddSingleton<ISettingsService, SettingsService>();
        services.AddSingleton<IClipboardService, ClipboardService>();
        services.AddSingleton<IShareService, ShareService>();
        services.AddSingleton<IEmailService, EmailService>();

        return services.BuildServiceProvider();
    }
}

この例では、アプリケーションの起動時に Services プロパティが初期化されます。すべてのアプリケーションのサービスと viewmodel が登録されます。また、アプリケーション内の他の view から Services プロパティに簡単にアクセスできるようにするための Current プロパティもまた用意してあります。たとえば:

IFilesService filesService = App.Current.Services.GetService<IFilesService>();

// Use the files service here...

ここで重要なことは、各サービスがプラットフォーム固有の API を使っている可能性が非常に高いこと、という点です。

しかし、私たちのコードで使っているインターフェースによって、それらはすべて抽象化しています。プラットフォーム固有の具体的な操作は、(抽象化された)インスタンスの操作を実行するだけなので、気にする必要はありません。

コンストラクタの注入 (Constructor injection)

強力な機能として "constructor injection" があります。これは DI プロバイダーがリクエストした型のインスタンス生成を自動的に解決できること(登録されたサービス間の間接的な依存関係において)を意味しています。次のサービスの例を考えてみます:

public class FileLogger : IFileLogger
{
    private readonly IFilesService FileService;
    private readonly IConsoleService ConsoleService;

    public FileLogger(
        IFilesService fileService,
        IConsoleService consoleService)
    {
        FileService = fileService;
        ConsoleService = consoleService;
    }

    // Methods for the IFileLogger interface here...
}

このコードの例は IFileLogger インターフェースを実装した FileLogger です。これは IFilesServiceIConsoleService を必要としています。

"constructor injection" は DI サービスプロバイダーが自動的に(そのコードの部分で必要とする)サービスを集めてくる操作のことを意味しています:

/// <summary>
/// アプリケーションのサービスを設定します。
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFilesService, FilesService>();
    services.AddSingleton<IConsoleService, ConsoleService>();
    services.AddSingleton<IFileLogger, FileLogger>();

    return services.BuildServiceProvider();
}

// constructor injection を使ってロガーサービスを取得する
IFileLogger fileLogger = App.Current.Services.GetService<IFileLogger>();

DI サービスプロバイダは必要なサービスがすべて登録されているかどうかを自動的にチェックします。そして登録されている IFileLogger の実装された型のコンストラクタを呼び出して、返却するインスタンスを渡します。

Viewmodel は "constructor injection" て、どうなの? (What about viewmodels?)

サービスプロバイダは名前に "service" とついていますが、実際はどんなクラスのインスタンスでも解決することができるので viewmodel も含まれます!

"constructor injection" を含めて、ここまでで説明してきたことと同じコンセプトが適用できます。

ContactsViewModel 型があったとして、IContactsService および IPhoneServiceインスタンスをコンストラクタで使用する、とします。この場合は、次のような ConfigureServices メソッドを持つことが可能です:

/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    // Services
    services.AddSingleton<IContactsService, ContactsService>();
    services.AddSingleton<IPhoneService, PhoneService>();

    // Viewmodels
    services.AddTransient<ContactsViewModel>();

    return services.BuildServiceProvider();
}

ContactsView で以下のように データコンテキストを割り当てします:

public ContactsView()
{
    this.InitializeComponent();
    this.DataContext = App.Current.Services.GetService<ContactsViewModel>();
}

「What about viewmodels?」はサンプルプログラムには存在しません。「MS Learn」にありました。

参考

レガシーコードとどう付き合うか

レガシーコードとどう付き合うか

Amazon