昨日、動的にメソッドを呼び出すためのリフレクションまたは戦略パターンの使用に関する質問をしました。
ただし、それ以来、メソッドを共通のインターフェイスを実装する個々のクラスに変更することにしました。その理由は、各クラスがいくつかの類似点を持ちながら、そのクラスに固有の特定のメソッドを実行するためです。
私はそのような戦略を使用していました:
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パラメーターは実行時に渡されるクラスの名前です。これはこれを行う一般的な方法ですか、これにパフォーマンスの問題はありますか?
私は熟考するのが初めてなので、あなたのアドバイスを歓迎します。
リフレクションを使用する場合、最初にいくつかの質問を自問する必要があります。これは、保守が難しいオーバーザトップの複雑なソリューションになる可能性があるためです。
dynamic
呼び出し(.NET 4.0以降のみ)を使用して問題を解決できますか?あなたの説明から、私はあなたがコンパイル時に型を知らず、それらがインタフェースICalculation
を共有することだけを知っていると仮定します。これが正しい場合、上記の番号(1)および(2)はシナリオで使用できない可能性があります。
これは重要な質問です。リフレクションを使用するオーバーヘッドは、400倍を超えるペナルティを妨げる可能性があります。これは、適度な量のコールでもスローダウンします。
解決は比較的簡単です。Activator.CreateInstance
を使用する代わりに、ファクトリメソッドを使用し(既に持っています)、MethodInfo
を検索してデリゲートを作成し、キャッシュしてからデリゲートを使用します。これにより、最初の呼び出しでのペナルティのみが得られ、後続の呼び出しではネイティブに近いパフォーマンスが得られます。
ここでは多くのことが可能ですが、この方向で支援するためにあなたの状況をもっと知る必要があります。多くの場合、dynamic
をジェネリックと、キャッシュされたリフレクションと組み合わせます。情報隠蔽を使用する場合(OOPで通常行われているように)、高速で安定した、さらに拡張性の高いソリューションになる可能性があります。
5つの質問のうち、これがおそらく最も心配するべき重要な質問です。リフレクションの間違いについて明確な情報を提供する独自の例外を作成することは非常に重要です。つまり、入力文字列またはその他の未確認情報に基づくメソッド、コンストラクター、またはプロパティの呼び出しはすべて、try/catchでラップする必要があります。特定の例外のみをキャッチします(いつものように、Exception
自体はキャッチしません)。
TargetException
(メソッドは存在しない)、TargetInvocationException
(メソッドは存在するが、呼び出されると例外を引き起こした)、TargetParameterCountException
、MethodAccessException
(正しい権限ではない)に焦点を当てる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();
}
}
ファクトリ実装にメソッド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");
}
このような場合に使用する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のような依存性注入フレームワークを使用することをお勧めします。このようにして、操作の実装では、さまざまな依存関係にコンストラクター注入を使用できます。
基本的に、工場出荷時のパターンが必要なようです。この状況では、入力の出力タイプへのマッピングを定義し、実行中と同様に実行時にタイプをインスタンス化します。
例:
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;
}
}