web-dev-qa-db-fra.com

Interface générique C # et modèle d'usine

J'essaie de créer une interface générique où le type de paramètre de l'une des méthodes est défini par le générique

MODIFIER

J'ai légèrement changé la question après avoir réalisé que j'avais probablement confondu les choses en spécifiant un paramètre de type dans la méthode de création Factory. Ce que j'ai, c'est deux types d'appels d'API que je dois faire à une API tierce. Le premier récupère un enregistrement de l'API à l'aide d'un ID qui est un int. Le second récupère également un enregistrement de l'API mais l'ID est une chaîne (guid). J'ai une classe pour chaque type d'enregistrement (ClientEntity et InvoiceEntity) qui implémentent toutes deux une interface générique où je passe le type d'ID

Ceci est l'interface dans laquelle je déclare une méthode avec un paramètre id

public interface IGeneric<TId>
{
    void ProcessEntity(TId id);
}

J'implémente l'interface dans quelques classes, l'une définit l'id comme un int, l'autre une chaîne.

public class ClientEntity: IGeneric<int> // Record with Id that is an int
{
    public void ProcessEntity(int id)
    {
        Console.WriteLine(id);
        // call 3rd party API with int Id
    }
}

public class InvoiceEntity: IGeneric<string> // Record with Id that is a string (guid)
{
    public void ProcessEntity(string id)
    {
        Console.WriteLine(id);
        // call 3rd party API with string Id
    }
}

Ce que je voudrais savoir, c'est comment l'utiliser dans un modèle d'usine?

public static class GenericFactory
{
    public static IGeneric<WhatGoesHere> CreateGeneric(string recordType)
    {
        if (recordType == "Client")
        {
            return new ClientEntity();
        }
        if (type == "Invoice")
        {
            return new InvoiceEntity();
        }

        return null;
    }

}

L'objectif est d'utiliser la fabrique pour instancier la bonne classe afin que je puisse appeler la méthode ProcessEntity

MODIFIER

Je ne veux pas avoir à passer le type générique à la méthode d'usine car la classe créée par l'usine devrait gérer cela. Lorsque je crée l'objet, je ne sais pas quel type d'ID est requis, je veux que l'usine le gère

par exemple.

   var myGeneric = GenericFactory.CreateGeneric("Client");
   myGeneric.ProcessEntity("guid")

ou

   var myGeneric = GenericFactory.CreateGeneric("Invoice");
   myGeneric.ProcessEntity(1234)

J'espère que cela à du sens

12
Nick Smith

Vous devriez pouvoir faire quelque chose comme ceci:

public static class GenericFactory
{
    public static IGeneric<T> CreateGeneric<T>()
    {
        if (typeof(T) == typeof(string))
        {
            return (IGeneric<T>) new GenericString();
        }

        if (typeof(T) == typeof(int))
        {
            return (IGeneric<T>) new GenericInt();
        }

        throw new InvalidOperationException();
    }
}

Vous l'utiliseriez comme ceci:

var a = GenericFactory.CreateGeneric<string>();
var b = GenericFactory.CreateGeneric<int>();

Notez que cela utilise un appel fortement typé plutôt que de passer le nom du type sous forme de chaîne (qui peut ou non être ce que vous voulez réellement).


Si à la place vous voulez passer une chaîne pour le nom du type, vous devrez retourner un object car il n'y a aucun moyen de retourner le type réel:

public static object CreateGeneric(string type)
{
    switch (type)
    {
        case "string": return new GenericString();
        case "int":    return new GenericInt();
        default:       throw new InvalidOperationException("Invalid type specified.");
    }
}

De toute évidence, si vous avez un object, vous devrez normalement le convertir au bon type afin de l'utiliser (ce qui nécessite que vous connaissiez le type réel).

Vous pouvez également utiliser la réflexion pour déterminer les méthodes qu'elle contient et les appeler ainsi. Mais alors, vous auriez encore besoin de connaître le type afin de passer un paramètre du bon type.

Je pense que ce que vous essayez de faire ici n'est pas la bonne approche, que vous découvrirez une fois que vous commencerez à essayer de l'utiliser.

Solution Hacky: utilisez dynamic

Néanmoins, il existe un moyen d'obtenir quelque chose proche de ce que vous voulez: Utilisez dynamic comme suit (en supposant que vous utilisez la méthode d'usine object CreateGeneric(string type) ci-dessus):

dynamic a = GenericFactory.CreateGeneric("string");
dynamic b = GenericFactory.CreateGeneric("int");

a.ProcessEntity("A string");
b.ProcessEntity(12345);

Sachez que dynamic utilise la réflexion et la génération de code en arrière-plan, ce qui peut ralentir les appels initiaux.

Sachez également que si vous transmettez le mauvais type à une méthode accessible via dynamic, vous obtiendrez une exception d'exécution désagréable:

dynamic a = GenericFactory.CreateGeneric("string");
a.ProcessEntity(12345); // Wrong parameter type!

Si vous exécutez ce code, vous obtenez ce type d'exception d'exécution:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: The best overloaded method match for 'ConsoleApplication1.GenericString.ProcessEntity(string)' has some invalid arguments
   at CallSite.Target(Closure , CallSite , Object , Int32 )
   at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
   at ConsoleApplication1.Program.Main() in D:\Test\CS6\ConsoleApplication1\Program.cs:line 71
11
Matthew Watson

Habituellement pour cette usine en utilisant un conteneur DI (DI peut être utile, par exemple, lorsque GenericInt ou GenericString a des dépendances), mais pour montrer juste Idea comment vous pouvez résoudre ce problème:

void Main()
{
    GenericFactory.CreateGeneric<int>();
    GenericFactory.CreateGeneric<string>();
}

public static class GenericFactory
{
    private static Dictionary<Type, Type> registeredTypes = new Dictionary<System.Type, System.Type>();

    static GenericFactory()
    {
        registeredTypes.Add(typeof(int), typeof(GenericInt));
        registeredTypes.Add(typeof(string), typeof(GenericString));
    }

    public static IGeneric<T> CreateGeneric<T>()
    {
        var t = typeof(T);
        if (registeredTypes.ContainsKey(t) == false) throw new NotSupportedException();

        var typeToCreate = registeredTypes[t];
        return Activator.CreateInstance(typeToCreate, true) as IGeneric<T>;
    }

}

public interface IGeneric<TId>
{
    TId Id { get; set; }

    void ProcessEntity(TId id);
}

public class GenericInt : IGeneric<int>
{
    public int Id { get; set; }

    public void ProcessEntity(int id)
    {
        Console.WriteLine(id);
    }
}

public class GenericString : IGeneric<string>
{
    public string Id { get; set; }

    public void ProcessEntity(string id)
    {
        Console.WriteLine(id);
    }
}
14
tym32167

La réponse marquée correcte est correcte si vous souhaitez utiliser la classe statique, mais qu'en est-il si vous souhaitez renvoyer un type injecté DI au lieu de créer un nouvel objet? Je suggère ce qui suit!

public interface IGenericFactory
{
    IGeneric<T> GetGeneric<T>() where T : class;
}

public class GenericFactory: IGenericFactory
{
    private readonly IGeneric<int> intGeneric;
    private readonly IGeneric<string> stringGeneric;
    public GenericFactory(IGeneric<int> intG, IGeneric<string> stringG)
    {
        intGeneric = intG;
        stringG = stringG;
    }

    public IGeneric<T> GetGeneric<T>() where T : class
    {
        if (typeof(T) == typeof(IGeneric<int>))
            return (IGeneric<T>)Convert.ChangeType(intGeneric, typeof(IGeneric<T>));

        if (typeof(T) == typeof(IGeneric<string>))
            return (IGeneric<T>)Convert.ChangeType(stringGeneric,typeof(IGeneric<T>));
        else 
            throw new NotSupportedException();
    }
}

Veuillez noter que j'ai simplement injecté les deux types de retour attendus pour plus de clarté dans le constructeur. J'aurais pu implémenter l'usine en tant que dictionnaire et injecter les objets de retour dans ce dictionnaire. J'espère que cela aide.

1
Dzidzai