C#泛型接口和工厂模式

31
我正在尝试创建一个通用接口,其中一个方法的参数类型由泛型定义。
编辑:我稍微改变了问题,意识到通过在工厂创建方法中指定类型参数可能会混淆问题。我有两种类型的 API 调用需要对第三方 API 进行访问。第一个使用 int 作为 Id 检索 API 中的记录。第二个也从 API 中检索记录,但 Id 是字符串 (guid)。我有两个类(ClientEntity 和 InvoiceEntity),它们都实现了一个通用接口,我在其中传入 Id 类型。
这是我声明一个带有 id 参数的方法的接口。
public interface IGeneric<TId>
{
    void ProcessEntity(TId id);
}

我在几个类中实现了这个接口,其中一个将id设置为整数,另一个将id设置为字符串。

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

我想知道如何在工厂模式中使用它?

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

}

目标是使用工厂实例化正确的类,以便我可以调用ProcessEntity方法。

编辑

我不想在工厂方法中传递泛型类型,因为由工厂创建的类应该处理它。当我创建对象时,我不知道需要什么类型的Id,我希望工厂来处理。

例如:

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


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

我希望那很有意义。


你的接口似乎有一个属性Id和一个以Id作为参数的方法,这让人感到困惑。你确定这是你想要做的吗?如果是,你应该明确两个值之间的区别。 - Rik
你可以忽略这个属性,它只是用来展示 T 的使用。 - Nick Smith
1
你提出的工厂方法的返回类型必须是 object,这对你来说可能没有太大用处... 除非你使用 dynamic,但我建议不要这样做... - Matthew Watson
谢谢@Mathew,但正如你所建议的,返回对象类型有点抵消了工厂模式的优势,因为我需要将其强制转换为某些东西才能调用方法。 - Nick Smith
6个回答

38
你应该能够做到这样:
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();
    }
}

您可以像这样使用它:
var a = GenericFactory.CreateGeneric<string>();
var b = GenericFactory.CreateGeneric<int>();

请注意,这里使用的是强类型调用,而不是将类型名称作为字符串传递(这可能是你实际想要的,也可能不是)。
如果您想传递类型名称的字符串,您将不得不返回一个对象,因为没有办法返回实际的类型:
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.");
    }
}

显然,如果你有一个object,通常需要将其转换为正确的类型才能使用它(这需要你知道实际的类型)。

或者,您可以使用反射来确定它包含哪些方法,并以此方式调用它们。但是,仍然需要知道类型才能传递正确类型的参数。

我认为你在这里尝试做的事情不是正确的方法,一旦你开始使用它,你就会发现这一点。

折中的解决方案:使用dynamic

尽管如此,你可以使用如下方法得到接近你想要的效果(假设你正在使用上面提供的object CreateGeneric(string type)工厂方法):

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

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

请注意,dynamic在幕后使用反射和代码生成,这可能会使最初的调用相对较慢。

还要注意,如果您向通过dynamic访问的方法传递了错误的类型,您将会得到一个不愉快的运行时异常:

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

如果您运行该代码,会得到这种运行时异常:
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

1
但是有时候在调用GenericFactory.CreateGeneric时,只提供了一个字符串类型的名称,而不是强类型的T - Cheng Chen
@DannyChen 我认为这只是原帖作者认为他必须这样做的方式。需要原帖作者确认。 - Matthew Watson
@Maarten 我知道这个。我只是在指出 OP 可能会遇到的一种情况。 - Cheng Chen
@NickSmith,如果你不知道类型,你就不能做你想做的事情 - 你唯一可能使用的返回类型是object,然后你需要将其强制转换来在某个时候使用它,或者使用反射来访问它的属性和方法。 - Matthew Watson
@NickSmith 我已经扩展了我的答案。 - Matthew Watson
显示剩余3条评论

23

通常工厂会使用一些依赖注入容器(例如,在GenericInt或GenericString具有依赖关系时,DI可能很有用),但为了演示如何解决此问题:

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

3

如果您想使用静态类,则标记为正确的答案是可以的,但如果您想返回一个 DI 注入的类型而不是新对象怎么办?我建议采用以下方式!

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

请注意,我只是为了在构造函数中更清晰地注入两种预期的返回类型。我可以将工厂实现为一个字典,并将返回对象注入到该字典中。希望这能帮助您理解。

0

我想你不想像LINQ方法一样输入类型参数。然而,这背后的魔法是因为类型参数在普通参数定义中使用。例如,在ToList<string>()方法中,你可以看到TSource在括号之间被使用。

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source);

这就是编译器知道当你从一个 IEnumerable<string> 调用 ToList() 时,你想要一个 List<string> 而不是 ToList<string>() 的方式。

然而,我认为你的工厂方法根本不需要泛型类型参数。你只需要创建一个非泛型版本的 TGeneric<TId> 即可。

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

CreateGeneric方法中删除<WhatGoesHere>

public static IGeneric CreateGeneric(string recordType)
{
    if (recordType == "Client")
    {
        return new ClientEntity();
    }
    if (recordType == "Invoice")
    {
        return new InvoiceEntity();
    }
    return null;
}

0

如果函数不知道类型,将其设为通用。

如果子类是不同类型的泛型(<int>,<string>),则在同一工厂类(Factory<T>)中返回对象并进行转换,这是通过typeof安全的。

个人而言,我更喜欢使用泛型指定类型,而不是使用额外的参数,例如字符串。

public class Program
{
    public static void Main(string[] args)
    {
        List<Number> something = new();
        Do(something);
    }
    public static void Do<T>(List<T> list) 
    {
        list.Add(Factory<T>.Create());
    }
}

public abstract class Factory<T>
{
     private static Object ConcreteF()
     {
          if (typeof(T) == typeof(Number))
                return new ChildGenericNumber();
          throw new Exception("");
     }
     public static T Create()
     {
          return (Factory<T>)ConcreteF()).Build();
     }
     protected abstract T Build();
}

目前你的回答不够清晰明了。请进行[编辑],添加更多细节以帮助他人理解这如何解决所提出的问题。你可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

0
所需的可能不是由工厂实现,而是一种处理器类型的实现。
假设下面是通用接口:
public interface IGeneric<TId>
{
    void ProcessEntity(TId id);
}

假设有两个实现它的类:
public class GenericInt : IGeneric<int>
{
        // DI Ctor
        public GenericInt(IDep1 dep, IDep2 dep2)
        {
            
        }
        public void ProcessEntity(int id)
        {
        }
}

public class GenericString : IGeneric<string>
{
        // DI Ctor
        public GenericString(IDep1 dep, IDep2 dep2)
        {
            
        }
        public void ProcessEntity(string id)
        {
        }
}

现在下面的类不完全是一个工厂,而更像是一个处理器:
public class Processor
{
    public Processor(IServiceProvider)
    {
       _serviceProvider = serviceProvider;
    }
    public void Execute(object o)
    {
        var handlerType = typeof(IGeneric<>).MakeGenericType(o.GetType());
        var handler = _serviceProvider.GetService(handlerType);
        var method = handlerType.GetMethod("ProcessEntity");
        method.Invoke(handler, new object[] { o });
    }
} 

可以按照以下方式使用:
   public class Program
    {
       public void Main()
       {
         // Here ioc is actually service provider -> IServiceProvider
         var handlerProvider = ioc.GetRequiredService<Processor>();
         handlerProvider.Execute("string"); // string
         handlerProvider.Execute(123); // int
       }
      
    }

上述方法不会返回指定类型的对象,但是以一种通用的方式,我们可以执行所需的功能,而不需要编写代码(在我们的情况下是程序)知道它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接