通用接口的通用工厂方法

jga*_*fin 4 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

但这会像癌症一样蔓延:

// 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
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

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

factory.Create<IActiveUsersQuery, ActiveUsersParameters, UserResult>();
Run Code Online (Sandbox Code Playgroud)

由于用户必须指定参数类型和结果类型,因此不是很好.

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

public interface IQuery
{

}
Run Code Online (Sandbox Code Playgroud)

显示意图,然后让用户创建他们喜欢的任何东西(因为他们,而不是工厂,将调用正确的查询).

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

Ste*_*ven 5

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

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

public interface IQuery<TResult> { }
Run Code Online (Sandbox Code Playgroud)

并为实现定义以下接口:

public interface IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}
Run Code Online (Sandbox Code Playgroud)

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

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

    public bool IncludeInactiveUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

实现可以定义如下:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

消费者不能依赖于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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

public interface IQueryProcessor
{
    TResult Execute<TResult>(IQuery<TResult> query);
}
Run Code Online (Sandbox Code Playgroud)

注入多个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

IQueryProcessor实际上是你的QueryFactory.

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

您可以在此博客文章中找到有关此设计的更多信息:同时...在我的架构的查询方面.