遵循CQRS(Command Query Responsibility Segregation)的概念,我直接在我的MVC应用程序中引用DAL并通过ViewModel进行所有读取。 然而,我的一位同事问我,在读取时必须应用任何业务逻辑时你会做什么。 对于例如如果您需要在以下场景中计算百分比值:
//Employee domain object
class Employee
{
string EmpName;
Single Wages;
}
//Constant declared in some utility class. This could be stored in DB also.
const Single Tax = 15;
//View Model for the Employee Screen
class EmployeeViewModel
{
string EmpName;
Single GrossWages;
Single NetWages;
}
// Read Facade defined in the DAL
class ReadModel
{
List<EmployeeViewModel> GetEmployeeList()
{
List<EmployeeViewModel> empList = new List<EmployeeViewModel>;
string query = "SELECT EMP_NAME, WAGES FROM EMPLOYEE";
...
..
while(reader.Read())
{
empList.Add(
new EmployeeViewModel
{
EmpName = reader["EMP_NAME"],
GrossWages = reader["WAGES"],
NetWages = reader["WAGES"] - (reader["WAGES"]*Tax)/100 /*We could call a function here but since we are not using the business layer, the function will be defined in the DAL layer*/
}
);
}
}
}
在上面的例子中,在读取期间发生了在DAL层中发生的计算。我们可以创建一个函数来进行计算,但由于我们已经绕过业务层进行读取,因此该函数将位于DAL中。更糟糕的是,如果Tax的值存储在DB中,有人可能会直接在存储过程中的DB中执行此操作。因此,我们在其他层面可能存在潜在的业务逻辑泄漏。
您可能会说为什么在执行命令时不将计算值存储在列中。所以让我们稍微改变一下场景。让我们假设您在报告中显示员工的潜在净工资,并且当前的税率和工资尚未支付。
您将如何处理CQRS?
答案 0 :(得分:4)
请注意,报告本身可以是一个完整的 Bounded Context 。因此,它的体系结构可能与您为核心域选择的体系结构完全不同。
也许CQRS非常适合核心域,但不适合报告领域。特别是当您想在报告生成之前根据不同的方案应用各种计算时。想想BI。
请记住,CQRS可能不适用于整个应用程序。一旦您的应用程序足够复杂,您应该识别其有界上下文并对每个应用程序应用适当的架构模式,即使它们使用相同的数据源。
答案 1 :(得分:3)
我的理解是CQRS与DDD相结合会产生一个查询端,它跨越有界上下文聚合数据,命令端执行命令严格地针对该特定命令的有界上下文。
这会让您的报告检索其所需的数据。
然后,您可以将一些ICalculator注入读取端的查询处理程序,以进行业务逻辑计算。
E.g:
public class EmployeeQueryHandler : EmployeeIQueryHandler
{
private readonly INetWageCalculator _calculator;
private readonly IEmployeeRepository _repo;
public Repository(INetWageCalculator calculator, IEmployeeRepository repo)
{
_calculator = calculator;
_repo = repo;
}
public List<EmployeeViewModel> ExecuteQuery()
{
var employees = _repo.GetEmployeeList();
foreach(var emp in employees)
{
// You have to get tax from somewhere, perhaps its passed in as
// a parameter...
emp.NetWages = _calculator.Calculate(emp.GrossWages, Tax);
}
return employees;
}
}
public class EmployeeRepository : IEmployeeRepository
{
List<EmployeeViewModel> GetEmployeeList()
{
List<EmployeeViewModel> empList = new List<EmployeeViewModel>;
string query = "SELECT EMP_NAME, WAGES FROM EMPLOYEE";
...
..
while (reader.Read())
{
empList.Add(
new EmployeeViewModel
{
EmpName = reader["EMP_NAME"],
GrossWages = reader["WAGES"],
// This line moves to the query handler.
//NetWages = reader["WAGES"] - (reader["WAGES"] * Tax) / 100 /*We could call a function here but since we are not using the business layer, the function will be defined in the DAL layer*/
}
);
}
}
}
这允许您重复使用相同计算器服务在其他地方计算净工资的业务逻辑。
出于性能考虑,如果您不想两次遍历结果,也可以将计算器注入存储库。
答案 2 :(得分:0)
对于您的第一个场景,我不知道为什么您需要在查询时进行该计算,您也不需要使用计算字段。当适当的员工交易在域上完成时,域可以产生计算的净工资。生成的数据被查询端消耗,并存储在准备查询的视图模型字段中。
如果税率发生变化,收到通知(事件)后,查询方将不得不重新计算所有员工视图模型的净工资字段。这将作为保存的一部分(从域事务异步)而不是作为查询请求的一部分发生。虽然查询方正在进行此计算,但它是根据域提供的数字进行的,所以我没有看到问题。
要点:所有计算都应在任何查询之前通过域或查询端事件处理程序完成。
编辑 - 基于评论
所以对于那个特定的&#39;假设&#39;分析场景,假设所需的数据已经在查询方面 - 即有一个&#39; EmployeeTimesheet&#39;包含员工工作小时数的表格,有两个选项:
在查询方面有一个组件定期轮询员工数据,并将数据汇总/汇总到“潜在工资”中。查看模型表,准备好管理层查看当前的工资支出。此轮询的频率取决于所需信息的频率。也许他们需要这些数据在一小时内有效,或者每天都是令人满意的。
再次,有一个&#39; PotentialWages&#39;表,但是当员工更新其时间表或员工的工资发生变化时,会随时更新。使用此选项,数据将保持接近实时。
无论哪种方式,计算出的聚合数据都是使用域生成的数据,并且在查询之前完成,这样查询就非常简单,最重要的是超快速。
编辑2 - 总结
在我看来,域名应负责进行计算,以便做出决策需要此类计算的结果。查询/读取方面为了求和总计而进行计算是绝对正确的。聚合数据以提供屏幕/报告所需的数据,只要这不是查询本身的一部分。