4

我希望能够指定一个可用于查询的类型化接口。我最终希望能够做类似的事情:

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
{

}

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

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

4

3 回答 3

5

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

这样做的方法是为消息定义以下接口(查询定义):

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

Instread 注入多个IQueryHandler<TQuery, TResult>实现,您可以注入一个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是某种环境上下文),这使得单元测试更加困难。例如,当消费者运行一种新类型的查询时,您的单元测试仍将编译。

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

于 2012-10-10T08:42:25.963 回答
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"));

我不知道这是否会对您的实际情况有所帮助,但这至少是一个可以考虑的选择。

于 2012-10-10T06:07:58.120 回答
1

我可能是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" });
}
于 2012-10-10T07:05:34.583 回答