かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Managed Extensibility Framework入門 その8「遅延初期化」

さて、前回は、リハビリも兼ねてLazyクラスの簡単な使い方を説明しました。今回はMEFでのLazyの使い方を説明します。

そもそも拡張可能アプリケーションでの遅延初期化とは?

そもそも、なんで遅延初期化が必要なのかというと前回でも言ったかもしれませんが、拡張可能なアプリケーションという観点で少し説明してみたいと思います。拡張可能なアプリケーションは、小規模なものだと数個のプラグインで完結するかもしれませんが、大規模なものになってくると数百以上のプラグインから構成されることも珍しくありません。さらに、1つ1つのプラグインも、そこそこ高機能だったりします。
これらのプラグインが全て起動時に初期化されるとどうなるでしょう?アプリケーションの起動時間だけでコーヒーを飲んでこれるくらい時間がかかってしまうかもしれません。このような問題を回避するために、とりあえず不要なものは初期化しないというアプローチがよくとられます。起動時には必要最低限の初期化処理だけやって、必要になったタイミングでプラグインのインスタンスを生成するという感じです。MEFでは前回紹介した遅延初期化のためのクラスのLazyを使用することで、これらの遅延初期化の機能をサポートしています。

サンプルプログラムの解説

とりあえず、ApplicationにIPluginというインターフェースを実装したクラスをプラグインとして差し込む小さなプログラムで動作について説明します。

IPluginインターフェースの定義

サンプルなのでExecuteという何かしらの処理を実行するというだけのシンプルなインターフェースです。特に解説は必要ないと思います。

// Applicationへのプラグインのインターフェース
interface IPlugin
{
    void Execute();
}
IPluginの実装クラス

上記のIPluginを実装したクラスを2つ用意しました。(何個でもかまいませんが、サンプルの動作の説明には個数は特に関係ないので)
両方ともIPlugin型でExportしています。あと、インスタンスが生成されたタイミングを確認するために、コンストラクタで標準出力にメッセージを出力するようにしています。Executeメソッドでも、簡単な文字列を標準出力に出力しています。

// プラグインの実装その1
[Export(typeof(IPlugin))]
class PluginA : IPlugin
{
    public PluginA()
    {
        // コンストラクタが実行されたことを示すログ
        Console.WriteLine("PluginA#ctor called.");
    }

    public void Execute()
    {
        // PluginAの主処理
        Console.WriteLine("PluginA#Execute called");
    }
}

// プラグインの実装その2
[Export(typeof(IPlugin))]
class PluginB : IPlugin
{
    public PluginB()
    {
        // コンストラクタが実行されたことを示すログ
        Console.WriteLine("PluginB#ctor called.");
    }

    public void Execute()
    {
        // PluginBの主処理
        Console.WriteLine("PluginB#Execute called");
    }
}
Applicationクラスの作成

まずは、Lazyを使用しないケースのアプリケーションクラスを作成します。アプリケーションクラスではImportManyでIPlugin型をMEFから差し込んでもらうようにしています。Runメソッドで、すべてのプラグインの処理を実行しています。

// 遅延初期化ではない場合
[Export]
class Application
{
    // Importの段階でクラスのインスタンスが作成される
    [ImportMany]
    public IEnumerable<IPlugin> Plugins { get; set; }

    public void Run()
    {
        foreach (var plugin in this.Plugins)
        {
            plugin.Execute();
        }
    }
}
Mainメソッドの実装

では、MEFの初期化をしてApplicationクラスのRunメソッドを呼び出してみます。

Console.WriteLine("遅延初期化をしていない場合");
var c = new CompositionContainer(
    new AssemblyCatalog(Assembly.GetExecutingAssembly()));

var app = c.GetExportedValue<Application>();
app.Run();

上記のプログラムを実行すると、以下のような結果になります。

遅延初期化をしていない場合
PluginA#ctor called.
PluginB#ctor called.
PluginA#Execute called
PluginB#Execute called

まず、PluginAとPluginBのインスタンスが作成されてからRunメソッド内で呼び出されたExecuteメソッドのログが出力されています。これが通常時の動作です。

Lazyを使用したアプリケーション

では、次にLazyを使用して遅延初期化を実現します。やりかたは簡単で、ImportするときにIPlugin型を指定していたところをLazyにするだけです。では、Lazyを使用するLazyApplicationクラスを以下に示します。

// 遅延初期化の場合
[Export]
class LazyApplication
{
    [ImportMany]
    public IEnumerable<Lazy<IPlugin>> Plugins { get; set; }

    public void Run()
    {
        foreach (var plugin in this.Plugins)
        {
            // ここで初めてクラスのインスタンスが作成される
            plugin.Value.Execute();
        }
    }
}

そして、LazyApplicationを使用するようにMainメソッドを書き換えます。

Console.WriteLine("遅延初期化の場合");
var c = new CompositionContainer(
    new AssemblyCatalog(Assembly.GetExecutingAssembly()));

var app = c.GetExportedValue<LazyApplication>();
app.Run();

プログラムを実行すると、以下のような結果になります。

遅延初期化の場合
PluginA#ctor called.
PluginA#Execute called
PluginB#ctor called.
PluginB#Execute called

最初の例ではPluginAとPluginBのインスタンスが最初に生成されていましたが、今回の例ではExecuteが呼ばれる直前までPluginAとPluginBの初期化が遅延していることがわかります。

まとめ

MEFとLazyを使えば遅延初期化の仕組みが簡単に作れます。これは結構素敵なことじゃないかと思います。やることは以下のことだけです。

  • Importする側をLazyにする
    • LazyのValueプロパティにアクセスしたタイミングでインスタンスが作られる

サンプルプロジェクトの入手

コチラからダウンロードできます。