Holistic Abstractions (Take 2)

In this post I will outline an updated perspective on Commands and Queries with respect to applying cross-cutting concerns (aka Aspects or Decorators). If you are unfamiliar with these two patterns then please see these posts here and here for a great introduction.

Bertrand Meyer defines CQS as: every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, asking a question should not change the answer.

Command-Query Separation (CQS)

Commands initiate state change by executing the appropriate behaviour on the domain

public interface ICommand { }

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

Queries report on the current state of the domain

public interface IQuery<TResult> { }

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

I love using these abstractions and they have been an integral part of my developer toolbox for the last few years. But, I have encountered numerous misunderstandings around exactly what a Command and Query are, where they should or should not be used, and what should be kept separate and when.

  • Can Commands reference other Commands? Maybe.
  • Is a Command an atomic unit of work? Maybe.
  • Can a command reference a query? Maybe.
  • Can a query reference a command? NO.
  • Can a command decorator reference another command? Maybe.
  • Can an event subscriber reference a command. Maybe.

It is very easy for these abstractions to grow to the point of failure by not have a clear set of rules. The problem goes something like this:

  • Create a set of Queries to read data via a UnitOfWork
  • Create a set of Commands to write data via a UnitOfWork
  • Create a Commit Transaction Command Decorator that commits the UnitOfWork at the end of the command
  • Create a Command that really just needs to use 2 other commands
  • Create a Mediator abstraction
  • Create a Commit Transaction Mediator Decorator that commits the UnitOfWork at the end of the command
  • Create a mediator that really just needs to use another Mediator or 2
  • ….

    Our favourite abstractions are in danger of becoming abstractions that do too much!

My solution to this problem involves a single golden rule.

Golden Rule Part 1:

Commands and Queries are Holistic Abstractions. Holistic Abstractions are concerned with the whole transaction; they are not a part of a transaction.

This simple definition makes Commands and Queries the same thing. They are the same size. They are the same scale. It’s easy enough to see a Command as a holistic abstraction, but less so a Query. To bring a holistic view to queries we need to see them as a whole thing:

Queries are a stale view of data; they are a view of the domain at a point in time. Querying the current state of the domain should not be confused with reading data from the database. The reporting database may not yet be in sync with the domain.

Golden Rule Part 2:

A Query should never reference a Command. A Command should never reference a Query.

There are many of us, myself included, that have always used queries within commands, and queries within queries. (And possibly even called commands from within other commands.) Commands and Queries are cool the way many of us have been using them all this times so why would we need this control, this limitation, on Queries when we’ve been using them in Commands for ever?

For one thing, Commands and Queries are cool because they offer a simple, consistent and convenient way to apply cross-cutting concerns. But it is this same feature that can get us into so much trouble. Not all cross-cutting concerns are equal. Some are concerned with the transaction, others are only concerned with a small piece of a transaction. To resolve this dilemma We need multiple layers of abstractions.

  1. A holistic abstraction, an abstraction that acts as the whole of each transaction
  2. Lower level abstractions that are part a of something (you could almost consider these abstractions to be strategies).

E.g.

Actions that change something and return nothing

public interface ICommandStrategyHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

public interface IDataCommandHandler<TCommand> where TCommand : IDataCommand
{
    void Handle(TCommand command);
}

Actions that return something and change nothing

public interface IQueryStrategyHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

public interface IDataQueryHandler<TQuery, TResult> where TQuery : IDataQuery<TResult>
{
    TResult Handle(TQuery query);
}

Abstractions such as the examples shown above allow us to easily separate logic that is the whole from logic that is the part. The examples can be arranged into a logical heirarchy which can be visualised as:

1. ICommandHandler<>
    2. ICommandStrategyHandler<>
        3. IDataCommandHandler<>
        3. IDataQueryHandler<,>
    2. IQueryStrategyHandler<,>
        3. IDataQueryHandler<,>

1. IQueryHandler<,>
    2. IQueryStrategyHandler<,>
        3. IDataQueryHandler<,>

We can expand this further to show the allowable dependencies with cross-cutting concerns (decorators)

  • for Query and Command Handlers
0.5 CommandHandlerDecorator<>
    1. DecoratedCommandHandler<>
        2. ICommandStrategyHandler<>
            3. IDataCommandHandler<>
            3. IDataQueryHandler<,>
        2. IQueryStrategyHandler<,>
            3. IDataQueryHandler<,>
    2. ICommandStrategyHandler<>
        3. IDataCommandHandler<>
        3. IDataQueryHandler<,>
    2. IQueryStrategyHandler<,>
        3. IDataQueryHandler<,>

0.5 QueryHandlerDecorator<,>
    1. DecoratedQueryHandler<,>
        2. IQueryStrategyHandler<,>
            3. IDataQueryHandler<,>
    2. ICommandStrategyHandler<>
        3. IDataCommandHandler<>
        3. IDataQueryHandler<,>
    2. IQueryStrategyHandler<,>
        3. IDataQueryHandler<,>
  • for Query Strategy and Command Strategy Handlers
1.5 CommandStrategyHandlerDecorator<>
    2. DecoratedCommandStrategyHandler<>
        3. IDataQueryHandler<,>
        3. IDataCommandHandler<>
    3. IDataQueryHandler<,>
    3. IDataCommandHandler<>

1.5 QueryStrategyHandlerDecorator<,>
    2. DecoratedQueryStrategyHandler<,>
        3. IDataQueryHandler<,>
    3. IDataQueryHandler<,>
    3. IDataCommandHandler<>

(Do note that objects do not know they are referencing decorated instances.)

You may have noticed something slightly odd about the numbering system. Command and Query Handler decorators are numbered as 0.5 in the heirarchy. This 0.5 signifies that these classes are before the real boundary of the transaction. They are the cross cutting concern for the entire transaction. An Aspect (i.e. a cross cutting concern) is senior in the heirarchy than the class that it decorates.

It’s all too easy to tie yourself in knots when you start to view the dependency graph of SOLID code with multiple decorators, especially when you reference Commands and Queries from other Commands and Queries. If you know what I mean then hopefully you’ll see simplicity in the following graph – this is as complex as it can get:

QueryHandlerDecorator<,>
    DecoratedQueryHandler<,>
        QueryStrategyHandlerDecorator<,>
            DecoratedQueryStrategyHandler<,>
                DataQueryHandlerDecorator<,>
                    DecoratedDataQueryHandler<,>
            DataQueryHandlerDecorator<,>
                DecoratedDataQueryHandler<,>
            DataCommandHandlerDecorator<>
                DecoratedDataCommandHandler<>
    CommandStrategyHandlerDecorator<>
        DecoratedCommandStrategyHandler<>
            DataQueryHandlerDecorator<,>
                DecoratedDataQueryHandler<,>
            DataCommandHandlerDecorator<>
                DecoratedDataCommandHandler<>
        DataQueryHandlerDecorator<,>
            DecoratedDataQueryHandler<,>
        DataCommandHandlerDecorator<>
            DecoratedDataCommandHandler<>
    QueryStrategyHandlerDecorator<,>
        DecoratedQueryStrategyHandler<,>
            DataQueryHandlerDecorator<,>
                DecoratedDataQueryHandler<,>
        DataQueryHandlerDecorator<,>
            DecoratedDataQueryHandler<,>
        DataCommandHandlerDecorator<>
            DecoratedDataCommandHandler<>

Summary

I am suggesting that we consider the original definition of Commands and Queries (ICommandHandler<> and IQueryHandler<,>) to be Holistic Abstractions that exist to define the boundary of a process. They can be decorated with transaction level cross-cutting concerns and should never be referenced as part of something. We define similar/equivalent lower level abstractions where we can apply lower level cross-cutting concerns, just not transaction level cross-cutting concerns. And, within any class, we must never reference another instance from the same level or higher in the heirarchy (with the exception of the decorated instance).

Commands and Queries are Holistic Abstractions. Holistic Abstractions are concerned with the whole transaction; they are not a part of a transaction. A Query should never reference a Command. A Command should never reference Query.

  • Command Handlers can depend on any lower level Query or Command Handler
  • Command Handler Decorators can depend on any lower level Query or Command Handler
  • Query Handlers can depend on any lower level Query Handler
  • Query Handler Decorators can depend on any lower level Query or Command Handler

Enjoy!

Leave a Reply