我希望能够指定可以与查询一起使用的类型化接口。我最终希望能够做到这样的事情:
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
{
}
显示意图,然后让用户创建自己喜欢的内容(因为他们而不是工厂会调用正确的查询)。
然而,最后一个选项不是很好,因为它不会让我装饰查询(例如使用缓存装饰器动态缓存它们)。
答案 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" });
}