CQRS: business logic on the query side

馋奶兔 提交于 2019-12-21 23:18:18

问题


Following the concept of CQRS (Command Query Responsibility Segregation), I am directly referring the DAL in my MVC application and doing all reads via the ViewModels. However a colleague of mine is asking me what will you do when any business logic has to be applied when doing a read. For e.g. if you need to compute a percentage value in scenario like below:

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

In above example, there is a calcuation occuring during the read which is occuring in the DAL layer. We could have created a function to do the calculation but again since we have bypassed the business layer for our read, the function will be located in the DAL. Even worse, someone might do it directly in the DB in a stored proc if the value of Tax is stored in the DB. So we have a potential leakage of business logic here in other layers.

You might say why don't you store the computed value in a column while doing the command. So let us change the scenario a bit. Let us say you are showing the potential Net Wages for the employee in a report with the current Tax rate and the Wages are yet to be paid.
How would you handle this in CQRS ?


回答1:


Please take into consideration that reporting can be a whole Bounded Context in its own right. Therefore its architecture can be completely different from the one you chose for your Core Domain.

Maybe CQRS is a good fit for the Core Domain but not for the domain of reporting. Especially when you want to apply various calculations based on different scenarios prior to report generation. Think BI.

Please remember that CQRS probably shouldn't be applied across your whole application. Once your application is complex enough you should identify its Bounded Contexts and apply an appropriate architectural pattern to each individually, even if they're using the same data source.




回答2:


My understanding is that CQRS combined with DDD would produce a query side that aggregated data across bounded context and a command side the executed commands strictly against the bounded context for that particular command.

This would leave your reporting to retrieve its data however it needed.

You could then inject some ICalculator into the query handler of the read side to do your business logic calculations.

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

This allows you to reuse the business logic of calculating net wages elsewhere using the same calculator service.

For performances sake, you could also inject the calculator into the repository if you didn't want to loop through the results twice.




回答3:


For your first scenario, I don't see why you need to do that calculation at the point of querying, neither do you need to use a calculated field. The domain could produce the calculated net wage when the appropriate employee transaction completes on the domain. The data produced gets consumed by the query side and gets stored in a view model field ready for querying.

If the tax rate was changed, upon receiving notification (event) the query side would have to recalculate the net wage field for all the employee view models. This would happen as part of the save (asynchronously from the domain transaction) and not as part of a query request. Although the query side is doing this calculation, it is doing so based on numbers provided by the domain, so I don't see a problem with that.

The main point: All calculations should be done via the domain or by query side event handlers prior to any queries.

EDIT- Based on comment

So for that particular 'what-if' analysis scenario, assuming that the data required is already in the query side - i.e. there is a 'EmployeeTimesheet' table that contains hours worked by employees, there's two options:

  1. Have a component on the query side that polls the employee data periodically and aggregates/sums the data into a 'Potential Wages' view model table, ready for management to see the current wage expenditure. The frequency of this polling would depend on how often the information was required. Perhaps they need this data to be valid within the hour, or perhaps daily is satisfactory.

  2. Again, have a 'PotentialWages' table, but that gets updated anytime an employee updates their time sheet or any time an employee's wage is changed. With this option the data would be kept close to real time.

Either way, the aggregated data calculated is using figures produced by the domain and is done prior to the query so that the query is super simple and most importantly, super fast.

EDIT 2 - Just to summarise

In my mind, the domain should be responsible for doing calculations whereby the result of such calculations is required for decisions to be made. It's absolutely fine for the query/read side to be doing calculations for the sake of summing totals & aggregating data to give screens/reports the data they need, as long as this isn't part of the query itself.



来源:https://stackoverflow.com/questions/11465025/cqrs-business-logic-on-the-query-side

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!