2

遵循 CQRS(命令查询职责分离)的概念,我在我的 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 的值存储在数据库中,有人可能会直接在存储过程中的数据库中执行此操作。因此,我们在其他层中存在潜在的业务逻辑泄漏。

您可能会说为什么不在执行命令时将计算值存储在列中。所以让我们稍微改变一下场景。假设您在具有当前税率的报告中显示员工的潜在净工资,而工资尚未支付。
您将如何在 CQRS 中处理此问题?

4

3 回答 3

5

我的理解是,CQRS 与 DDD 相结合将产生一个查询端,该查询端在有界上下文中聚合数据,而命令端则严格针对该特定命令的有界上下文执行命令。

这将使您的报告可以根据需要检索其数据。

然后,您可以将一些 ICalculator 注入到读取端的查询处理程序中以进行业务逻辑计算。

例如:

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

这允许您使用相同的计算器服务在其他地方重用计算净工资的业务逻辑。

出于性能考虑,如果您不想循环遍历结果两次,也可以将计算器注入存储库。

于 2016-06-16T14:15:29.137 回答
4

请注意,报告本身可以是一个完整的有界上下文。因此,它的架构可能与您为Core Domain选择的架构完全不同。

也许 CQRS 非常适合核心领域,但不适用于报告领域。尤其是当您想在生成报告之前基于不同场景应用各种计算时。想想 BI。

请记住,CQRS 可能不应该应用于整个应用程序。一旦您的应用程序足够复杂,您就应该识别它的限界上下文并对每个单独应用适当的架构模式,即使它们使用相同的数据源。

于 2012-07-13T14:58:09.067 回答
1

对于您的第一个场景,我不明白为什么您需要在查询时进行计算,也不需要使用计算字段。当适当的员工事务在域上完成时,域可以产生计算的净工资。产生的数据被查询端使用并存储在视图模型字段中以供查询。

如果税率发生变化,则在收到通知(事件)后,查询方必须重新计算所有员工视图模型的净工资字段。这将作为保存的一部分(与域事务异步)而不是作为查询请求的一部分发生。尽管查询端正在执行此计算,但它是根据域提供的数字执行此操作的,因此我认为这没有问题。

要点:所有计算都应在任何查询之前通过域或查询端事件处理程序完成。

编辑-基于评论

因此,对于那个特定的“假设”分析场景,假设所需的数据已经在查询端 - 即有一个包含员工工作时间的“EmployeeTimesheet”表,有两个选项:

  1. 在查询端有一个组件,它定期轮询员工数据并将数据聚合/汇总到“潜在工资”视图模型表中,以便管理层查看当前的工资支出。此轮询的频率将取决于需要信息的频率。也许他们需要这些数据在一小时内有效,或者也许每天都令人满意。

  2. 同样,有一个“PotentialWages”表,但只要员工更新他们的时间表或员工的工资发生变化,它就会更新。使用此选项,数据将保持接近实时。

无论哪种方式,计算的聚合数据都是使用域生成的数据,并且在查询之前完成,因此查询超级简单,最重要的是,超级快速。

编辑 2 - 只是总结一下

在我看来,域应该负责进行计算,因此做出决策需要此类计算的结果只要这不是查询本身的一部分,查询/读取端进行计算以汇总总计和聚合数据以提供屏幕/报告所需的数据是绝对可以的。

于 2012-07-13T08:35:25.830 回答