読者です 読者をやめる 読者になる 読者になる

Managed Extensibility Framework でプラグイン機能を実装する紆余曲折

.net

.NET Framework 4 で提供されている Managed Extensibility Framework (以下 MEF) は、シンプルな DI 機能を提供していて、アプリにプラグイン機能を実装するのに使えます。私も最近、MEF を使ったプラグイン機能の実装を検討し始めました。


MEF を使ってプラグイン機能を実装するにしても、あちらこちらで MEF の CompositContainer を new するわけにはいけないので、プラグインを管理するクラスを用意することになると思います。そのクラスの実装で現在悩み中。


まず最初に書いたのは超シンプルなファクトリクラス。もうほとんど CompositContainer をラップしただけ。

public static class PluginFactory
{
    private readonly static CompositContainer _container;

    static PluginFactory()
    {
        var path = Path.Combine(AppDomain.Current.BaseDirectory, "plugins");
        var catalog = new DirectoryCatalog(path);
        _container = new CompositContainer(catalog);
    }

    public static T Create<T>()
    {
        return _container.GetExportedValue();
    }
}

これだとコンテナに T に指定したクラスが登録されていないとき例外が出てしまいます。


例外が出ないように修正。

public static class PluginFactory
{
    private readonly static CompositContainer _container;

    static PluginFactory()
    {
        var path = Path.Combine(AppDomain.Current.BaseDirectory, "plugins");
        var catalog = new DirectoryCatalog(path);
        _container = new CompositContainer(catalog);
    }

    public static T Create<T>()
    {
        var instance = _container.GetExportedValueOrDefault();
        if (instance == null)
        {
            instance = Activator.CreateInstance<T>();
        }
        return instance;
    }
}

GetExportedValueOrDefault を使えば、コンテナに登録されてないとき null が返ってきます。Activator.CreateInstance でインスタンス作成してるけど、これだとインタフェースや抽象クラスのとき例外が出ます。これだけだとイマイチ。


デフォルトで使うクラスを指定できるようにしてみます。

public static class PluginFactory
{
    private readonly static CompositContainer _container;

    static PluginFactory()
    {
        var path = Path.Combine(AppDomain.Current.BaseDirectory, "plugins");
        var catalog = new DirectoryCatalog(path);
        _container = new CompositContainer(catalog);
    }

    public static T CreateOrDefault<T, TDefault>()
        where TDefault : T, new()
    {
        var instance = _container.GetExportedValueOrDefault();
        if (instance == null)
        {
            instance = new TDefault();
        }
        return instance;
    }
}

デフォルトで使うクラスは、コンストラクタに引数が必要、なんてケースがあるかもしれない。


いっそ、関数を渡せるようにしてしまえ。

public static class PluginFactory
{
    private readonly static CompositContainer _container;

    static PluginFactory()
    {
        var path = Path.Combine(AppDomain.Current.BaseDirectory, "plugins");
        var catalog = new DirectoryCatalog(path);
        _container = new CompositContainer(catalog);
    }

    public static T CreateOrDefault<T>(Func<T> createDefault)
    {
        var instance = _container.GetExportedValueOrDefault();
        if (instance == null)
        {
            instance = createDefault();
        }
        return instance;
    }
}

毎回生成用関数を引数で渡すのは面倒かな…。


そんな紆余曲折があって、いまのところこんな感じです。

public static class PluginFactory
{
    private readonly static CompositContainer _container;

    static PluginFactory()
    {
        var path = Path.Combine(AppDomain.Current.BaseDirectory, "plugins");
        var catalog = new DirectoryCatalog(path);
        _container = new CompositContainer(catalog);
    }

    public static T Create<T>()
    {
        var instance = _container.GetExportedValueOrDefault();
        if (instance == null)
        {
            instance = Activator.CreateInstance<T>();
        }
        return instance;
    }

    public static T CreateOrDefault<T>(Func<T> createDefault)
    {
        var instance = _container.GetExportedValueOrDefault();
        if (instance == null)
        {
            instance = createDefault();
        }
        return instance;
    }
}

GetExportedValue 系のメソッドでは、コンストラクタに渡す引数を指定できないから、しっくりきてないけど。


Prism が MEF を使い倒してるみたいなので、ソースコード読んでみようかな。