動的なDLLのロード

とあるシステムをC#で開発している人から、

実装の詳細は異なるけど同じ働きをする同じ名前のクラスを別々のDLLに入れといて、実行時にそれらをロードして簡単に切り替えて使いたいけど、何か良い方法は無いかな?

と尋ねられたので考えてみた。要は、何通りかの実装でどれが良いか検討するためらしい。ってことで、パフォーマンスは二の次で良いとのこと。

以下、僕が考えてみたもの。IEchoableインターフェースを実装したClass1を別々のdllに用意して、それらを動的に使ってみるテスト。


Interface.dll

namespace Interface
{
  public interface IEchoable
  {
    void Echo();
  }
}


Library1.dll

using System;
using System.Windows.Forms;
using Interface;

namespace Library1
{
  public class Class1 : MarshalByRefObject, IEchoable
  {
    string _msg;

    public Class1(string msg)
    {
      _msg = msg;
    }

    public void Echo()
    {
      MessageBox.Show(_msg);
    }
  }
}


Library2.dll

using System;
using Interface;

namespace Library2
{
  public class Class1 : MarshalByRefObject, IEchoable
  {
    Action _action;

    public Class1(Action echoAction)
    {
      _action = echoAction;
    }

    public void Echo()
    {
      _action();
    }
  }
}


dllを使うところ(IEchoableを使うために、Interface.dllへの参照を追加すべし)

using System;
using Interface;
using System.Reflection;

namespace SomeDllHandler
{
  class Program
  {
    static void Main(string[] args)
    {
      //
      // DLLを動的に読み込んで、そのDLL内部のクラスのオブジェクトを
      // インスタンス化するサンプル
      //

      AppDomain lib1 = AppDomain.CreateDomain("lib1", null);
      IEchoable obj1 = null;
      try { obj1 = CreateInstanceForLibrary1(lib1); }
      catch (Exception e) { Console.WriteLine(e.Message); }
      if (null != obj1)
      {
        // 1. IEchoable経由でアクセス
        obj1.Echo();
        // 2. リフレクションによるアクセス
        BindingFlags flags = BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public;
        obj1.GetType().InvokeMember("Echo", flags, null, obj1, new object[] { });

        AppDomain.Unload(lib1);
      }

      AppDomain lib2 = AppDomain.CreateDomain("lib2", null);
      IEchoable obj2 = null;
      try { obj2 = CreateInstanceForLibrary2(lib2); }
      catch (Exception e) { Console.WriteLine(e.Message); }
      if (null != obj2)
      {
        // 1. IEchoable経由でアクセス
        obj2.Echo();
        // 2. リフレクションによるアクセス
        BindingFlags flags = BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public;
        obj2.GetType().InvokeMember("Echo", flags, null, obj2, new object[] { });

        AppDomain.Unload(lib2);
      }
    }

    // Library1.Class1のインスタンスを作成
    static IEchoable CreateInstanceForLibrary1(AppDomain dom)
    {
      string asmName = "Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
      string typeName = "Library1.Class1";
      object[] args = new object[] { "Hello!" };
      return InvokeConstructor(dom, false, asmName, typeName, args);
    }

    // Library2.Class1のインスタンスを作成
    static IEchoable CreateInstanceForLibrary2(AppDomain dom)
    {
      string asmName = "Library2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
      string typeName = "Library2.Class1";
      object[] args = new Action[] { () => Console.WriteLine("Foooo!!") };
      return InvokeConstructor(dom, false, asmName, typeName, args);
    }

    static IEchoable InvokeConstructor(AppDomain dom, bool ignoreCase, string asmName, string typeName, object[] args)
    {
      // .NET 4以降ではObsoleteなメソッドになってる
      // 理解できてない引数が多数...
      // http://msdn.microsoft.com/ja-jp/library/system.appdomain.createinstanceandunwrap.aspx
      return
        dom.CreateInstanceAndUnwrap(
           asmName,
           typeName,
           ignoreCase,
           BindingFlags.Default,
           null,
           args,
           null,
           null,
           null
        ) as IEchoable;
    }
  }
}

Library1.dllとLibrary2.dllはどこに置いたらいいのか分からなかったけど、exeと同じ位置に置いてテケトーにやってみたらうまくいった。
Mainメソッドでobj1とobj2を利用する際に、compile-timeな型はobject型のみで通すならば、リフレクションによるメソッド呼び出しが使える。もしcompile-timeな型を利用したいなら、共通のインターフェースを実装するしかないのかな。
AppDomain + リフレクションは相当コストが高い処理ぽい。一方で、IEchoableのように、共通のインターフェースを実装することはメソッド等のシグネチャを揃えなければならないという制約もあるなぁ。
リファレンス等を読まずにやってみたので、相当間違ったこと書いてるかもしれないけど。

参考にしたページ:匣の向こう側 - あまりに.NETな

で、

今日(11月11日)また少し話をしたら、同一のDLL内で、クラス名変えて、インターフェース被せてやるからもういいよって言われた...