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. 性能是否重要,即我的反射方法或实例化调用会被调用一次、两次还是一百万次?
  4. 我可以结合技术来获得一个智能但可行/可理解的解决方案吗?
  5. 我可以失去编译时类型安全吗?

泛型/动态

根据您的描述,我假设您在编译时不知道类型,您只知道它们共享接口ICalculation。如果这是正确的,那么上面的数字 (1) 和 (2) 在您的场景中可能是不可能的。

表现

这是一个重要的问题。使用反射的开销可以阻止超过 400 倍的惩罚:即使是中等数量的调用也会减慢。

解决方法相对简单:使用工厂方法(您已经拥有)代替使用Activator.CreateInstance,查找MethodInfo创建委托,缓存它并从那时起使用委托。这只会对第一次调用产生惩罚,随后的调用具有接近本机的性能。

结合技术

这里有很多可能,但我真的需要更多地了解你的情况才能在这个方向上提供帮助。通常,我最终会结合dynamic泛型和缓存反射。使用信息隐藏(在 OOP 中很常见)时,您最终可能会得到一个快速、稳定且可扩展性良好的解决方案。

失去编译时类型安全

在这五个问题中,这可能是最需要担心的一个。创建自己的异常以提供有关反射错误的清晰信息非常重要。这意味着:基于输入字符串或其他未经检查的信息对方法、构造函数或属性的每次调用都必须包装在 try/catch 中。只捕获特定的异常(一如既往,我的意思是:永远不要捕获Exception自己)。

关注TargetException(方法不存在),TargetInvocationException(方法存在,但在调用时增加一个 exc.)TargetParameterCountException,,MethodAccessException(不是正确的权限,在 ASP.NET 中经常发生),InvalidOperationException(发生在泛型类型中)。您并不总是需要尝试捕获所有这些,这取决于预期的输入和预期的目标对象。

把它们加起来

摆脱你Activator.CreateInstance并使用 MethodInfo 找到工厂创建方法,并使用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 Expression 编写,部分代码取自这里

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 也可以实现相同的功能。

此外,这些类不需要从相同的接口或基类继承。

谢谢, 维塔利

于 2011-03-10T20:14:22.327 回答
6

我在这种情况下使用的一种策略是使用特殊属性标记我的各种实现以指示其键,并使用该键扫描活动程序集的类型:

[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”发送给他们,而不会产生任何反射怪异。

我还建议使用像 Ninject 这样的依赖注入框架,而不是使用 Activator.CreateInstance。这样,您的操作实现可以使用构造函数注入来处理它们的各种依赖项。

于 2011-03-10T16:34:48.637 回答
1
  1. 这里没有错误检查。您绝对确定 _class 将解析为有效的类吗?您是在控制所有可能的值,还是最终用户以某种方式填充了此字符串?
  2. 反思通常比避免反思成本更高。性能问题与您计划以这种方式实例化的对象数量成正比。
  3. 在你使用依赖注入框架之前,请阅读对它的批评。=)
于 2011-03-10T16:39:20.630 回答
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 回答