Quelle différence entre le pattern CQS et CQRS ?

Ces dernières années, on a entendu de plus en plus souvent parler du pattern CQRS (pour Command Query Responsability Segration) du fait sa démocratisation croissante. Mais on entend beaucoup moins parler du pattern CQS (pour Command Query Separation). Et pour cause, les deux concepts sont souvent assimilés à la même chose. Bien qu’ils partagent une même idée, il existe quelques différences fondamentales que nous allons voir dans ce billet.

Commençons par le pattern Command Query Separation (CQS). Ce dernier repose sur l’idée que chaque méthode/fonction de notre code doit soit modifier l’état du système (on parlera alors d’une commande), soit retourner une information (on parlera de requête), mais qu’aucune action ne devrait faire les deux à la fois.

J’avais déjà un peu évoqué cette notion dans un article précédent sur le nommage des messages dans le code.

Voici un exemple d’implémentation:

class OrderService
{
    // Command
    public function createOrder(string $customer, array $items): void
    {
        // ...
    }

    // Command
    public function addItemToOrder(string $orderId, string $productId, int $quantity, float $unitPrice): void
    {
        // ...
    }

    // Query
    public function getOrder(string $orderId): ?Order
    {
        // ...
    }
}

Même si d’un point de vue purement théorique, une commande ne doit pas retourner de données, il peut être pratique dans des cas précis d’enfreindre cette règle. C’est le cas des commandes qui vont créer une information en base de données et pour laquelle il est utile de pouvoir récupérer l’identifiant de la donnée qui a été insérée.

Si on reprend l’exemple précédent, la commande OrderService::createOrder pourrait être adaptée de cette manière:

class OrderService
{
    // Command
    public function createOrder(string $customer, array $items): int
    {
        $order = new Order(/* ... */);

        // ...

        return $order->getId();
    }

    // ....
}

Ce principe peut paraître simple et insignifiant, il est pourtant d’une efficacité redoutable et possède de nombreux avantages. Il permet de se concentrer sur la responsabilité unique de vos méthodes (le fameux S du principe S.O.L.I.D.). Ce qui permet de tendre à un code plus compréhensible (parce qu’il fait moins de choses) et facilitant ainsi la mise en place de tests.

De plus c’est une technique facile à mettre en place, il serait dommage de s’en priver.

Le pattern Command Query Responsability Segration (CQRS) peut quant à lui, être considéré comme une évolution du CQS puisque ce dernier reprend la même idée, mais en poussant le concept de séparation des lectures et des écritures au sein d’un système.

Reprenons notre exemple précédent de création de commande, cela reviendrait à avoir deux services distincts pour créer la commande et la récupérer:

// Command
class CreateOrderCommandService
{
    public function create(string $customer, array $items): int
    {
        $order = new Order(/* ... */);

        // ...

        return $order->getId();
    }
}

// Query
class GetOrderQueryService
{
    public function getOrderById(string $orderId): ?Order
    {
        // ...
    }
}

Comme on peut l’apercevoir dans l’exemple précédent, c’est un pattern plus lourd à mettre en place, puisqu’il va falloir découper le code plus finement. Néanmoins cette approche présente certains avantages très intéressants. Tout d’abord, le découpage supplémentaire permet de structurer son code tout en continuant de respecter le principe de responsabilité unique. Mais, la plus grande force du pattern CQRS est l’optimisation des performances qu’il est capable de mettre en place sur le projet.

En permettant de séparer lecture et écriture, il est possible de gérer chacun des modèles dans des tables, des bases de données, voir des moteurs de bases de données distincts. Si au démarrage d’un projet (le pragmatisme étant de rigueur), il est fort probable (et même conseillé pour éviter l’optimisation prématurée) que les commandes et les requêtes (au sens CQRS) tapent la même table de votre base de données, lorsque vous allez avoir de la volumétrie et/ou un trafic important, il sera intéressant de pouvoir mettre en place une version optimisée de chacun des modèles. Les écritures pouvant se faire dans une table relationnelle et les lectures sur une table dénormalisée par exemple. Suivant le niveau de performance requis, il est même possible de sortir un modèle dans une base de données distincte qui sera optimisé pour de la lecture. Faire ce travail de découpage est facilité avec ce pattern.

Cela permettra également par rebond d’améliorer votre scalabilité du fait que les opérations sont effectuées de manières séparées. Vous aurez ainsi la possibilité de faire évoluer les choses de manières indépendantes.

En résumé, CQS et CQRS sont des approches similaires qui vous permettent de tendre vers un code simple, découpé, tout en facilitant l’écriture de test. La mise en place des principes sous-jacents n’est pas complexe à mettre en place et peut être effectuée au démarrage d’un projet. Dans le cas du CQRS, on peut séparer les opérations de lecture et d’écrire dans le code, tout en partageant dans un premier temps les mêmes repositories qui manipulent une même table dans une base de données unique. Le découpage des modèles dans des tables ou des bases de données distinctes ne doit se faire qu’au fil du temps et des besoins d’un projet.

Chacune des approches a sa place et j’ai presque envie de dire que le CQS est un must have. Il y a juste une chose essentielle à retenir, c’est que le complexité du découpage de vos modèles doit se faire en fonction de vos besoins et non pas parce que vous souhaitez utiliser un principe jusqu’au bout.

Soyez pragmatique et ne soyez pas dogmatique!