55

昨日、メソッドを動的に呼び出すためにリフレクションまたはストラテジーパターンを使用することについて質問しました。

しかし、それ以来、私はメソッドを共通のインターフェースを実装する個々のクラスに変更することにしました。その理由は、各クラスは、いくつかの類似点を持ちながら、そのクラスに固有の特定のメソッドも実行するためです。

私はそのような戦略を使用していました:

switch (method)
{
    case "Pivot":
        return new Pivot(originalData);
    case "GroupBy":
        return new GroupBy(originalData);
    case "Standard deviation":
        return new StandardDeviation(originalData);
    case "% phospho PRAS Protein":
        return new PhosphoPRASPercentage(originalData);
    case "AveragePPPperTreatment":
        return new AveragePPPperTreatment(originalData);
    case "AvgPPPNControl":
        return new AvgPPPNControl(originalData);
    case "PercentageInhibition":
        return new PercentageInhibition(originalData);
    default:
        throw new Exception("ERROR: Method " + method + " does not exist.");
}

ただし、潜在的なクラスの数が増えるにつれて、新しいクラスを追加し続ける必要があります。そのため、変更のためのクローズドルールが破られます。

代わりに、私はそのような解決策を使用しました:

var test = Activator.CreateInstance(null, "MBDDXDataViews."+ _class);
       ICalculation instance = (ICalculation)test.Unwrap();
       return instance;

事実上、_classパラメーターは実行時に渡されるクラスの名前です。これはこれを行うための一般的な方法ですか?これにパフォーマンスの問題はありますか?

私は振り返りにかなり慣れていないので、あなたのアドバイスを歓迎します。

4

6 に答える 6

77

リフレクションを使用するときは、最初にいくつかの質問をする必要があります。これは、保守が難しい複雑なソリューションになってしまう可能性があるためです。

  1. 汎用性またはクラス/インターフェイスの継承を使用して問題を解決する方法はありますか?
  2. 呼び出しを使用して問題を解決できますかdynamic(.NET 4.0以降のみ)?
  3. パフォーマンスは重要ですか。つまり、反映されたメソッドまたはインスタンス化呼び出しは1回、2回、または100万回呼び出されますか?
  4. テクノロジーを組み合わせて、スマートでありながら実行可能/理解可能なソリューションを実現できますか?
  5. コンパイル時の型の安全性を失っても大丈夫ですか?

汎用性/動的

あなたの説明から、私はあなたがコンパイル時にタイプを知らないと思います、あなたはそれらがインターフェースを共有することだけを知っていますICalculation。これが正しければ、上記の番号(1)と(2)はシナリオでは不可能である可能性があります。

パフォーマンス

これは重要な質問です。リフレクションを使用するオーバーヘッドは、400倍を超えるペナルティを妨げる可能性があります。これにより、適度な量の呼び出しでも速度が低下します。

解決は比較的簡単です。を使用する代わりにActivator.CreateInstance、ファクトリメソッドを使用し(すでにそれがあります)MethodInfo、デリゲートの作成を検索し、キャッシュして、それ以降デリゲートを使用します。これにより、最初の呼び出しでペナルティが発生するだけで、後続の呼び出しのパフォーマンスはネイティブに近くなります。

テクノロジーを組み合わせる

ここでは多くのことが可能ですが、この方向性を支援するために、あなたの状況をもっと知る必要があります。多くの場合、dynamicキャッシュされたリフレクションを使用して、ジェネリックスと組み合わせることになります。情報隠蔽を使用する場合(OOPで通常行われているように)、高速で安定した、それでも十分に拡張可能なソリューションが得られる可能性があります。

コンパイル時の型安全性の喪失

5つの質問のうち、これはおそらく最も重要な質問です。リフレクションの間違いについて明確な情報を提供する独自の例外を作成することは非常に重要です。つまり、入力文字列またはその他のチェックされていない情報に基づくメソッド、コンストラクター、またはプロパティへのすべての呼び出しは、try/catchでラップする必要があります。特定の例外のみをキャッチします(いつものように、つまり、自分Exception自身をキャッチしないでください)。

TargetException(メソッドは存在しません)、TargetInvocationException(メソッドは存在しますが、呼び出されると例外が発生します)、、TargetParameterCountExceptionMethodAccessException適切な特権ではなく、ASP.NETで多く発生します)、InvalidOperationException(ジェネリック型で発生します)に焦点を当てます。必ずしもすべてをキャッチしようとする必要はありません。予想される入力と予想されるターゲットオブジェクトによって異なります。

まとめると

を削除Activator.CreateInstanceし、MethodInfoを使用してfactory-createメソッドを見つけ、を使用Delegate.CreateDelegateしてデリゲートを作成およびキャッシュします。Dictionaryキーがサンプルコードのクラス文字列と等しい静的な場所に保存するだけです。以下は、タイプの安全性をあまり失うことなく、これを安全に行うための迅速ですがそれほど汚くない方法です。

サンプルコード

public class TestDynamicFactory
{
    // static storage
    private static Dictionary<string, Func<ICalculate>> InstanceCreateCache = new Dictionary<string, Func<ICalculate>>();

    // how to invoke it
    static int Main()
    {
        // invoke it, this is lightning fast and the first-time cache will be arranged
        // also, no need to give the full method anymore, just the classname, as we
        // use an interface for the rest. Almost full type safety!
        ICalculate instanceOfCalculator = this.CreateCachableICalculate("RandomNumber");
        int result = instanceOfCalculator.ExecuteCalculation();
    }

    // searches for the class, initiates it (calls factory method) and returns the instance
    // TODO: add a lot of error handling!
    ICalculate CreateCachableICalculate(string className)
    {
        if(!InstanceCreateCache.ContainsKey(className))
        {
            // get the type (several ways exist, this is an eays one)
            Type type = TypeDelegator.GetType("TestDynamicFactory." + className);

            // NOTE: this can be tempting, but do NOT use the following, because you cannot 
            // create a delegate from a ctor and will loose many performance benefits
            //ConstructorInfo constructorInfo = type.GetConstructor(Type.EmptyTypes);

            // works with public instance/static methods
            MethodInfo mi = type.GetMethod("Create");

            // the "magic", turn it into a delegate
            var createInstanceDelegate = (Func<ICalculate>) Delegate.CreateDelegate(typeof (Func<ICalculate>), mi);

            // store for future reference
            InstanceCreateCache.Add(className, createInstanceDelegate);
        }

        return InstanceCreateCache[className].Invoke();

    }
}

// example of your ICalculate interface
public interface ICalculate
{
    void Initialize();
    int ExecuteCalculation();
}

// example of an ICalculate class
public class RandomNumber : ICalculate
{
    private static Random  _random;

    public static RandomNumber Create()
    {
        var random = new RandomNumber();
        random.Initialize();
        return random;
    }

    public void Initialize()
    {
        _random = new Random(DateTime.Now.Millisecond);
    }

    public int ExecuteCalculation()
    {
        return _random.Next();
    }
}
于 2011-03-10T17:51:09.250 に答える
18

ファクトリ実装にメソッドを与えることをお勧めしますRegisterImplementation。したがって、すべての新しいクラスはそのメソッドの呼び出しであり、ファクトリコードを変更することはありません。

更新:
私が意味するのは次のようなものです:

計算を定義するインターフェースを作成します。あなたのコードによると、あなたはすでにこれをしました。完全を期すために、残りの回答では次のインターフェイスを使用します。

public interface ICalculation
{
    void Initialize(string originalData);
    void DoWork();
}

工場は次のようになります。

public class CalculationFactory
{
    private readonly Dictionary<string, Func<string, ICalculation>> _calculations = 
                        new Dictionary<string, Func<string, ICalculation>>();

    public void RegisterCalculation<T>(string method)
        where T : ICalculation, new()
    {
        _calculations.Add(method, originalData =>
                                  {
                                      var calculation = new T();
                                      calculation.Initialize(originalData);
                                      return calculation;
                                  });
    }

    public ICalculation CreateInstance(string method, string originalData)
    {
        return _calculations[method](originalData);
    }
}

この単純なファクトリクラスには、単純さのためにエラーチェックがありません。

更新2:
アプリケーション初期化ルーチンのどこかで次のように初期化します。

CalculationFactory _factory = new CalculationFactory();

public void RegisterCalculations()
{
    _factory.RegisterCalculation<Pivot>("Pivot");
    _factory.RegisterCalculation<GroupBy>("GroupBy");
    _factory.RegisterCalculation<StandardDeviation>("Standard deviation");
    _factory.RegisterCalculation<PhosphoPRASPercentage>("% phospho PRAS Protein");
    _factory.RegisterCalculation<AveragePPPperTreatment>("AveragePPPperTreatment");
    _factory.RegisterCalculation<AvgPPPNControl>("AvgPPPNControl");
    _factory.RegisterCalculation<PercentageInhibition>("PercentageInhibition");
}
于 2011-03-10T16:36:03.270 に答える
7

コンストラクターに初期化を追加する方法の例として、次のようにします。

似たようなもの:

Activator.CreateInstance(Type.GetType("ConsoleApplication1.Operation1"), initializationData);
しかし、Linq式で書かれているので、コードの一部はここにあります:

public class Operation1 
{
    public Operation1(object data)
    { 
    }
}

public class Operation2 
{
    public Operation2(object data)
    {
    }
}

public class ActivatorsStorage
{
    public delegate object ObjectActivator(params object[] args);

    private readonly Dictionary<string, ObjectActivator> activators = new Dictionary<string,ObjectActivator>();

    private ObjectActivator CreateActivator(ConstructorInfo ctor)
    {
        Type type = ctor.DeclaringType;

        ParameterInfo[] paramsInfo = ctor.GetParameters();
        ParameterExpression param = Expression.Parameter(typeof(object[]), "args");

        Expression[] argsExp = new Expression[paramsInfo.Length];

        for (int i = 0; i < paramsInfo.Length; i++)
        {
            Expression index = Expression.Constant(i);
            Type paramType = paramsInfo[i].ParameterType;

            Expression paramAccessorExp = Expression.ArrayIndex(param, index);

            Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType);

            argsExp[i] = paramCastExp;
        }

        NewExpression newExp = Expression.New(ctor, argsExp);

        LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator), newExp, param);

        return (ObjectActivator)lambda.Compile();
    }

    private ObjectActivator CreateActivator(string className)
    {
        Type type = Type.GetType(className);
        if (type == null)
            throw new ArgumentException("Incorrect class name", "className");

        // Get contructor with one parameter
        ConstructorInfo ctor = type.GetConstructors()
            .SingleOrDefault(w => w.GetParameters().Length == 1 
                && w.GetParameters()[0].ParameterType == typeof(object));

        if (ctor == null)
                throw new Exception("There is no any constructor with 1 object parameter.");

        return CreateActivator(ctor);
    }

    public ObjectActivator GetActivator(string className)
    {
        ObjectActivator activator;

        if (activators.TryGetValue(className, out activator))
        {
            return activator;
        }
        activator = CreateActivator(className);
        activators[className] = activator;
        return activator;
    }
}

使用法は次のとおりです。

ActivatorsStorage ast = new ActivatorsStorage();
var a = ast.GetActivator("ConsoleApplication1.Operation1")(initializationData);
var b = ast.GetActivator("ConsoleApplication1.Operation2")(initializationData);

同じことがDynamicMethodsでも実装できます。

また、クラスは同じインターフェイスまたは基本クラスから継承する必要はありません。

ありがとう、Vitaliy

于 2011-03-10T20:14:22.327 に答える
6

このような場合に使用する戦略の1つは、さまざまな実装にそのキーを示す特別な属性のフラグを立て、アクティブなアセンブリをスキャンしてそのキーのタイプを探すことです。

[AttributeUsage(AttributeTargets.Class)]
public class OperationAttribute : System.Attribute
{ 
    public OperationAttribute(string opKey)
    {
        _opKey = opKey;
    }

    private string _opKey;
    public string OpKey {get {return _opKey;}}
}

[Operation("Standard deviation")]
public class StandardDeviation : IOperation
{
    public void Initialize(object originalData)
    {
        //...
    }
}

public interface IOperation
{
    void Initialize(object originalData);
}

public class OperationFactory
{
    static OperationFactory()
    {
        _opTypesByKey = 
            (from a in AppDomain.CurrentDomain.GetAssemblies()
             from t in a.GetTypes()
             let att = t.GetCustomAttributes(typeof(OperationAttribute), false).FirstOrDefault()
             where att != null
             select new { ((OperationAttribute)att).OpKey, t})
             .ToDictionary(e => e.OpKey, e => e.t);
    }
    private static IDictionary<string, Type> _opTypesByKey;
    public IOperation GetOperation(string opKey, object originalData)
    {
        var op = (IOperation)Activator.CreateInstance(_opTypesByKey[opKey]);
        op.Initialize(originalData);
        return op;
    }
}

そうすれば、新しいキー文字列を使用して新しいクラスを作成するだけで、ファクトリコードをまったく変更することなく、ファクトリに自動的に「プラグイン」できます。

また、特定のコンストラクターを提供するために各実装に依存するのではなく、クラスが実装することを期待するインターフェイスにInitializeメソッドを作成したことにも気付くでしょう。彼らがインターフェースを実装している限り、私は「originalData」を反射の奇妙さなしに彼らに送ることができます。

また、Activator.CreateInstanceを使用する代わりに、Ninjectのような依存性注入フレームワークを使用することをお勧めします。このようにして、操作の実装は、さまざまな依存関係に対してコンストラクターインジェクションを使用できます。

于 2011-03-10T16:34:48.637 に答える
1

基本的に、ファクトリパターンが必要なようです。この状況では、入力から出力タイプへのマッピングを定義してから、実行時にタイプをインスタンス化します。

例:

X個のクラスがあり、それらはすべてIDoSomethingの共通のインターフェースを共有しています。

public interface IDoSomething
{
     void DoSomething();
}

public class Foo : IDoSomething
{
    public void DoSomething()
    {
         // Does Something specific to Foo
    }
}

public class Bar : IDoSomething
{
    public void DoSomething()
    {
        // Does something specific to Bar
    }
}

public class MyClassFactory
{
     private static Dictionary<string, Type> _mapping = new Dictionary<string, Type>();

     static MyClassFactory()
     {
          _mapping.Add("Foo", typeof(Foo));
          _mapping.Add("Bar", typeof(Bar));
     }

     public static void AddMapping(string query, Type concreteType)
     {
          // Omitting key checking code, etc. Basically, you can register new types at runtime as well.
          _mapping.Add(query, concreteType);
     }

     public IDoSomething GetMySomething(string desiredThing)
     {
          if(!_mapping.ContainsKey(desiredThing))
              throw new ApplicationException("No mapping is defined for: " + desiredThing);

          return Activator.CreateInstance(_mapping[desiredThing]) as IDoSomething;
     }
}
于 2011-03-10T16:37:40.787 に答える
1
  1. ここでエラーチェックはありません。_classが有効なクラスに解決されることを絶対に確信していますか?可能なすべての値を制御していますか、それともこの文字列はエンドユーザーによって何らかの形で入力されていますか?
  2. リフレクションは、一般的に、回避するよりもコストがかかります。パフォーマンスの問題は、この方法でインスタンス化する予定のオブジェクトの数に比例します。
  3. 依存性注入フレームワークを実行して使用する前に、その批判を読んでください。=)
于 2011-03-10T16:39:20.630 に答える