通用接口的通用工厂方法

时间:2012-10-10 05:48:38

标签: c# generics factory-pattern

我希望能够指定可以与查询一起使用的类型化接口。我最终希望能够做到这样的事情:

var query = _queryfactory.Create<IActiveUsersQuery>();
var result = query.Execute(new ActiveUsersParameters("a", "b"));
foreach (var user in result)
{
    Console.WriteLine(user.FirstName);
}

看起来很简单,嗯?请注意,查询获得了类型化参数和类型化结果。为了能够将查询工厂限制为仅包含查询,我们必须指定类似的内容:

public interface IQuery<in TParameters, out TResult>
    where TResult : class
    where TParameters : class
{
    TResult Invoke(TParameters parameters);
}

但这会像癌症一样蔓延:

// this was easy
public interface IActiveUsersQuery : IQuery<ActiveUsersParameters, UserResult>
{

}

//but the factory got to have those restrictions too:
public class QueryFactory
{
    public void Register<TQuery, TParameters, TResult>(Func<TQuery> factory)
        where TQuery : IQuery<TParameters, TResult>
        where TParameters : class
        where TResult : class
    {
    }

    public TQuery Create<TQuery, TParameters, TResult>()
        where TQuery : IQuery<TParameters, TResult>
        where TParameters : class
        where TResult : class
    {
    }
}

最终会导致工厂调用,如:

factory.Create<IActiveUsersQuery, ActiveUsersParameters, UserResult>();

不是很好,因为用户必须指定参数类型和结果类型。

我试图控制它太多了吗?我应该只创建一个虚拟接口:

public interface IQuery
{

}

显示意图,然后让用户创建自己喜欢的内容(因为他们而不是工厂会调用正确的查询)。

然而,最后一个选项不是很好,因为它不会让我装饰查询(例如使用缓存装饰器动态缓存它们)。

3 个答案:

答案 0 :(得分:5)

我完全理解你在这里要做的事情。您正在应用SOLID原则,因此查询实现是从消费者那里抽象出来的,消费者只是发送消息(DTO)并获得结果。通过为查询实现通用接口,我们可以使用装饰器包装实现,装饰器允许各种有趣的行为,例如事务行为,审计,性能监视,缓存等等。

执行此操作的方法是为messsage(查询定义)定义以下接口:

public interface IQuery<TResult> { }

为实现定义以下界面:

public interface IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

IQuery<TResult>是某种标记接口,但这允许我们静态定义查询返回的内容,例如:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }

    public bool IncludeInactiveUsers { get; set; }
}

实现可以定义如下:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly IUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(IUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        // example
        return (
            from user in this.db.Users
            where user.Name.Contains(query.SearchText)
            where user.IsActive || query.IncludeInactiveUsers
            select user)
            .ToArray();
    }
}

消费者不能依赖IQueryHandler<TQuery, TResult>来执行查询:

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> handler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> handler)
    {
        this. handler = handler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.handler.Handle(query);

        return this.View(users);
    }
}

这允许您向查询处理程序添加横切关注点,而无需消费者知道这一点,这为您提供了完整的编译时支持。

然而,这种方法(IMO)的最大缺点是你很容易得到大型构造函数,因为你经常需要执行多个查询,(而不是真正违反SRP)。

要解决此问题,您可以在使用者和IQueryHandler<TQuery, TResult>界面之间引入抽象:

public interface IQueryProcessor
{
    TResult Execute<TResult>(IQuery<TResult> query);
}

注入多个IQueryHandler<TQuery, TResult>实现的Instread,您可以注入一个IQueryProcessor。消费者现在看起来像这样:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Execute(query);

        return this.View(users);
    }
}

IQueryProcessor实现可能如下所示:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Execute<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

它取决于容器(它是组合根的一部分)并使用dynamic键入(C#4.0)来执行查询。

IQueryProcessor实际上是您的QueryFactory

但是使用这个IQueryProcessor抽象有一些缺点。例如,您错过了让DI容器验证是否存在请求的IQueryHandler<TQuery, TResult>实现的可能性。您现在可以在请求根对象时调用processor.Execute。您可以通过编写额外的集成测试来解决此问题,该测试检查是否为实现IQueryHandler<TQuery, TResult>的每个类注册了IQuery<TResult>。另一个缺点是依赖关系不太清楚(IQueryProcessor是某种环境上下文),这使单元测试更加困难。例如,当消费者运行新类型的查询时,您的单元测试仍将进行编译。

您可以在此博文中找到有关此设计的更多信息:Meanwhile… on the query side of my architecture

答案 1 :(得分:2)

你工厂里真的需要TQuery吗?你能不能只使用:

public void Register<TParameters, TResult>
        (Func<IQuery<TParameters, TResult>> factory)
    where TParameters : class
    where TResult : class
{
}

public IQuery<TParameters, TResult> Create<TParameters, TResult>()
    where TParameters : class
    where TResult : class
{
}

此时,您仍然有两个类型参数,但假设您通常想要获取查询并立即执行它,您可以使用类型推断来允许类似:

public QueryExecutor<TResult> GetExecutor() where TResult : class
{
}

然后有一个通用的方法:

public IQueryable<TResult> Execute<TParameters>(TParameters parameters)
    where TParameters : class
{
}

所以你原来的代码就会变成:

var query = _queryfactory.GetExecutor<UserResult>()
                         .Execute(new ActiveUsersParameters("a", "b"));

我不知道这对您的实际案例是否有帮助,但至少可以考虑这一点。

答案 2 :(得分:0)

我可能是OT,但我需要根据我的评论做出回应。我会像这样实现整个查询对象系统:

系统类:

public class Context
{
    // context contains system specific behaviour (connection, session, etc..)
}

public interface IQuery
{
    void SetContext(Context context); 
    void Execute();
}

public abstract class QueryBase : IQuery
{
    private Context _context;

    protected Context Context { get { return _context; } }

    void IQuery.SetContext(Context context)
    {
        _context = context;
    }
    public abstract void Execute();
}

public class QueryExecutor
{
    public void Execute(IQuery query)
    {
        query.SetContext({set system context});
        query.Execute();
    }
}

具体查询类:

public interface IActiveUsersQuery : IQuery // can be ommited
{
    int InData1 { get; set; }
    string InData2 { get; set; }

    List<string> OutData1 { get; }
}

public class ActiveUsersQuery : QueryBase, IActiveUsersQuery
{
    public int InData1 { get; set; }
    public string InData2 { get; set; }

    public List<string> OutData1 { get; private set; }

    public override void Execute()
    {
        OutData1 = Context.{do something with InData1 and InData};
    }
}

你这样使用它:

QueryExecutor executor;

public void Example()
{
    var query = new ActiveUsersQuery { InData1 = 1, InData2 = "text" };
    executor.Execute(query);

    var data = query.OutData1; // use output here;
}

它仍然具有查询对象系统的相同优点。您仍然可以装饰特定查询或任何查询(您在设计中缺少这些查询)。它还将每个查询的对象减少到2,如果不需要特定的查询接口,则可以将其减少到只有1。并且看不到令人讨厌的仿制品。

以上例子的一个专业化:

public interface IQuery<TResult> : IQuery
{
    TResult Result { get; }
}

public class QueryExecutor
{
    // ..

    public TResult Execute<TResult>(IQuery<TResult> query)
    {
        Execute((IQuery)query);
        return query.Result;
    }
}

public class ActiveUsersQuery : QueryBase, IQuery<List<string>>
{
    public int InData1 { get; set; }
    public string InData2 { get; set; }

    public List<string> Result { get; private set; }

    public override void Execute()
    {
        //OutData1 = Context.{do something with InData1 and InData};
    }
}

然后使用减少为单行:

public void Example()
{
    var outData = executor.Execute(new ActiveUsersQuery { InData1 = 1, InData2 = "text" });
}