mercredi 5 mai 2021

CQRS design w/ business & domain logic

So as I've been attempting to add cross-cutting concerns in a web service (e.g., logging) I've come across refactoring my DDD (Domain Driven Design) with the CQRS pattern in mind.

Refactoring the query-focused repositories/functionality has been relatively straightforward. However, as I've begun refactoring the command side of my application, I'm hitting a wall. I'll begin with a simplified example and then address my previous implementation and where I'm struggling with the new architecture.

Example Scenario

Let's say you have the following domain object representing historical data:

public abstract class DomainData
{
    public IDomainItem ItemInfo; // metadata info about the object

    public IList<DateTime> Dates;
    public IList<DataRecord> TotalData;
    public IDictionary<string, IList<DataRecord>> DataByLevel1;

    public async Task<IEnumerable<DataRecord>> LoadData( object queryParams ) { }
    public async Task SetTotalData( IEnumerable<DataRecord> rawData ) { }
    public async Task SetLevel1Data( IEnumerable<DataRecord> rawData_Level1 ) { }

    public abstract void Calculate( object domainParams );

    public virtual void Fill() { } // make sure that all data properties have the same set of dates
}

public interface IDomainItem
{
    Guid Id { get; set; }
    string Name { get; set; }
    //...
}
public class DataRecord
{
    DateTime Date;
    double Value;
    //...
}

In order to completely build any of the concrete children the following must be done:

  1. get the data for each data property, either by:
    • loading the data from the db - LoadData( object queryParams )
    • calculating given some configuration 'domainParams'
  2. set the data properties, either:
    • loading from the db - Set...Data() methods
    • calculating the concrete child using domain & business logic - Calculate()
  3. call business logic such as Fill() to make sure dates are aligned across the levels

DDD implementation

Given the redundancies across instantiating the concrete children (building the common data props, always needing to fill, etc.), I previously offloaded this work to domain/application services, such as the following factory pattern:

public interface IDomainDataFactoryService
{
    Task<ConcreteDomainData_A> Build( IDomainItem_A itemInfo );
    Task<ConcreteDomainData_B> Build( IDomainItem_B itemInfo );
    Task<TDomainData> Build<TDomainData>( Enum dbType, int dbId ) where TDomainData : DomainData;

    Task<ConcreteDomainData_A> Create( object domainParams );
}

I would inject this factory into the necessary controllers, and use it to build the data before validation, storage, etc.

Current Confusion / Issues

My end goal is to implement cross-cutting concerns (like logging) outside of domain/business logic. This quickly led me down the decorator route, which is how I ended up with the CQRS pattern. However, I'm struggling to see how complex creation of domain models can be achieved using the classic command/handler pattern. Given that handlers shouldn't return domain objects or modify the state of the command passed in, I'm struggling to grasp how I would apply this kind of complex creational logic.

I have looked into both the Chain of Responsibility and Composite command pattern; however, both would contradict the previous principles. In the following discussions it mentions injecting application / domain services into command handlers:

However, if I keep the creational logic in a service, I'm back at square one (similar violations of ISP, OCP, SRP, LSP; as well as the future developments requiring sweeping changes). I'm failing to understand how I can break these steps of building the DomainData object (see the 3 steps under the example code above) into distinct commands given that handlers are not supposed to modify the state of the command objects nor return a result.

I refrained from linking code and examples in places I felt like it was pretty straightforward given the context... If any of it is unclear, let me know and I'm happy to provide a further examples in an online repo.

Aucun commentaire:

Enregistrer un commentaire