web-dev-qa-db-fra.com

C # à l'aide d'Activator.CreateInstance

Hier, j'ai posé une question concernant l'utilisation de la réflexion ou du modèle de stratégie pour appeler dynamiquement des méthodes.

Cependant, depuis lors, j'ai décidé de changer les méthodes en classes individuelles qui implémentent une interface commune. La raison en est que chaque classe, tout en présentant certaines similitudes, applique également certaines méthodes propres à cette classe.

J'avais utilisé une stratégie en tant que telle:

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.");
}

Cependant, à mesure que le nombre de classes potentielles augmentera, je devrai continuer d'en ajouter de nouvelles, brisant ainsi la règle de fermeture pour modification.

Au lieu de cela, j'ai utilisé une solution en tant que telle:

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

En effet, le paramètre _class est le nom de la classe transmise lors de l'exécution. Est-ce une façon courante de le faire, y aura-t-il des problèmes de performances avec cela?

Je suis assez nouveau dans la réflexion, donc vos conseils seraient les bienvenus.

50
Darren Young

Lorsque vous utilisez la réflexion, vous devez d'abord vous poser quelques questions, car vous pouvez vous retrouver dans une solution complexe exagérée qui est difficile à maintenir:

  1. Existe-t-il un moyen de résoudre le problème en utilisant la généricité ou l'héritage de classe/interface?
  2. Puis-je résoudre le problème en utilisant des invocations dynamic (uniquement .NET 4.0 et supérieur)?
  3. Les performances sont-elles importantes, c'est-à-dire que ma méthode réfléchie ou mon appel d'instanciation sera appelé une, deux ou un million de fois?
  4. Puis-je combiner des technologies pour arriver à une solution intelligente mais réalisable/compréhensible?
  5. Suis-je d'accord pour perdre la sécurité du type de temps de compilation?

Généricité/dynamique

D'après votre description, je suppose que vous ne connaissez pas les types au moment de la compilation, vous savez seulement qu'ils partagent l'interface ICalculation. Si cela est correct, les numéros (1) et (2) ci-dessus ne sont probablement pas possibles dans votre scénario.

Performance

C'est une question importante à poser. Les frais généraux liés à l'utilisation de la réflexion peuvent entraver une pénalité de plus de 400 fois: cela ralentit même une quantité modérée d'appels.

La résolution est relativement simple: au lieu d'utiliser Activator.CreateInstance, utilisez une méthode d'usine (vous l'avez déjà), recherchez le MethodInfo créez un délégué, mettez-le en cache et utilisez le délégué à partir de là. Cela ne produit qu'une pénalité lors de la première invocation, les invocations suivantes ont des performances quasi natives.

Combiner les technologies

Beaucoup est possible ici, mais j'aurais vraiment besoin d'en savoir plus sur votre situation pour vous aider dans cette direction. Souvent, je finis par combiner dynamic avec des génériques, avec une réflexion en cache. Lorsque vous utilisez le masquage d'informations (comme c'est normal dans la POO), vous pouvez vous retrouver avec une solution rapide, stable et toujours bien extensible.

Perte de sécurité du type de temps de compilation

Des cinq questions, celle-ci est peut-être la plus importante à craindre. Il est très important de créer vos propres exceptions qui donnent des informations claires sur les erreurs de réflexion. Cela signifie que chaque appel à une méthode, un constructeur ou une propriété basé sur une chaîne d'entrée ou des informations non contrôlées doit être encapsulé dans un try/catch. N'attrapez que des exceptions spécifiques (comme toujours, je veux dire: ne jamais attraper Exception lui-même).

Concentrez-vous sur TargetException (la méthode n'existe pas), TargetInvocationException (la méthode existe, mais une exc. Lorsqu'elle est invoquée), TargetParameterCountException, MethodAccessException (pas la droits, se produit souvent dans ASP.NET), InvalidOperationException (se produit avec les types génériques). Vous n'avez pas toujours besoin d'essayer de les attraper tous, cela dépend de l'entrée attendue et des objets cibles attendus.

Résumer

Débarrassez-vous de votre Activator.CreateInstance et utilisez MethodInfo pour trouver la méthode de création d'usine, et utilisez Delegate.CreateDelegate pour créer et mettre en cache le délégué. Stockez-le simplement dans un Dictionary statique où la clé est égale à la chaîne de classe dans votre exemple de code. Vous trouverez ci-dessous un moyen rapide mais pas si sale de le faire en toute sécurité et sans perdre trop de sécurité de type.

Exemple de code

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();
    }
}
71
Abel

Je vous suggère de donner à votre implémentation d'usine une méthode RegisterImplementation. Donc, chaque nouvelle classe n'est qu'un appel à cette méthode et vous ne changez pas le code de vos usines.

MISE À JOUR:
Ce que je veux dire est quelque chose comme ceci:

Créez une interface qui définit un calcul. Selon votre code, vous l'avez déjà fait. Pour être complet, je vais utiliser l'interface suivante dans le reste de ma réponse:

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

Votre usine ressemblera à ceci:

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);
    }
}

Cette classe d'usine simple manque de vérification d'erreur pour des raisons de simplicité.

MISE À JOUR 2:
Vous devez l'initialiser comme ceci quelque part dans la routine d'initialisation de vos applications:

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");
}
17
Daniel Hilgarth

Une stratégie que j'utilise dans des cas comme celui-ci consiste à signaler mes différentes implémentations avec un attribut spécial pour indiquer sa clé et à rechercher les types actifs avec cette clé dans les assemblys actifs:

[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;
    }
}

De cette façon, juste en créant une nouvelle classe avec une nouvelle chaîne de clés, vous pouvez automatiquement "vous connecter" à l'usine, sans avoir à modifier le code d'usine du tout.

Vous remarquerez également que plutôt que de dépendre de chaque implémentation pour fournir un constructeur spécifique, j'ai créé une méthode Initialize sur l'interface que j'attends des classes. Tant qu'ils implémentent l'interface, je pourrai leur envoyer les "données originales" sans aucune bizarrerie de réflexion.

Je suggérerais également d'utiliser un framework d'injection de dépendances comme Ninject au lieu d'utiliser Activator.CreateInstance. De cette façon, vos implémentations d'opération peuvent utiliser l'injection de constructeur pour leurs diverses dépendances.

6
StriplingWarrior

Essentiellement, il semble que vous souhaitiez le modèle d'usine. Dans cette situation, vous définissez un mappage des types d'entrée sur les types de sortie, puis instanciez le type au moment de l'exécution comme vous le faites.

Exemple:

Vous avez X nombre de classes, et elles partagent toutes une interface commune d'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;
     }
}
1
Tejs
  1. Il n'y a aucune erreur de vérification ici. Êtes-vous absolument sûr que _class se résoudra en classe valide? Contrôlez-vous toutes les valeurs possibles ou cette chaîne est-elle en quelque sorte remplie par un utilisateur final?
  2. La réflexion est généralement plus coûteuse que l'évitement. Les problèmes de performances sont proportionnels au nombre d'objets que vous prévoyez d'instancier de cette façon.
  3. Avant de vous enfuir et d'utiliser un framework d'injection de dépendances, lisez les critiques. =)
1
SFun28