CQRS et ses modèles

La séparation des responsabilités entre les commandes -les actions qui changent l'état d'un système- et les requêtes -les actions qui n'en changent pas l'état- est un pattern intéressant mais il semble aller généralement de paire avec l'utilisation de deux modélisations distinctes pour les éléments manipulés par le système. Et je trouve cela un peu perturbant.

Je suis loin de maîtriser les détails des approches CQRS. Ma connaissance se limite à la lecture de quelques articles. Le plus connu est probablement celui de Martin Fowler où la séparation entre les deux modèles est omniprésente.

Essayons de voir ce que cela donne sur un exemple simple de gestion de bibliothèque. D'un côté j'ai un ensemble de commandes pour créer des adhérents et traiter le flux de leurs emprunts.

namespace Commandes
{
  public interface IBibliotheque
  {
    void CréerAdhérent (Adhérent adhérent);
    void DémarrerEmprunt (Emprunt emprunt);
    void CloreEmprunt (Emprunt emprunt);
  }
}

Ces commandes utilisent un modèle "riche" dans lequel sont implémentées les règles de gestion nécessaires aux commandes : types d'adhérents, date limite de retour des emprunts, etc.

namespace Commandes
{
  public class Adhérent
  {
    public long Numéro;
    public string Nom;
    public string Ville;
    
    public bool EstEtranger {
      get {
        return Ville == "Paris";
      }
    }
  }
}
namespace Commandes
{
  public class Emprunt
  {
    public long Identifiant;
    public long NuméroAdhérent;
    public string Description;
    public DateTime DateDébut;
    
    public DateTime DateLimite {
      get {
        return DateDébut.AddMonths (1);
      }
    }
  }
}

D'un autre côté, on a un ensemble de requêtes qui permettent d'interroger le système.

namespace Requêtes
{
  public interface IBibliotheque
  {  
    IEnumerable<RésuméAdherent> RechercheAdherents (string recherche);
    IEnumerable<RésuméEmprunt> RechercheEmpruntsEnCours (string recherche);
  }
}

Ces requêtes, qui, en l'occurrence, vont principalement servir à de l'affichage d'ensembles d'éléments nécessitent un modèle manipulant beaucoup moins d'informations et peu ou pas de règles de gestion.

namespace Requêtes
{
  public class RésuméAdherent
  {
    public long Numéro;
    public string Nom;
  }
}
namespace Requêtes
{
  public class RésuméEmprunt
  {
    public long Identifiant;
    public string Description; 
    public string NomAdhérent; 
  }
}

Les commandes et les requêtes vivent donc dans deux mondes séparés. Elles n'ont techniquement rien en commun. Elles ne se rencontrent que lors de l’enchaînement de plusieurs actions au plus près de l'interface utilisateur.
Voici quelques "histoires utilisateur" qui pourraient exister dans une application utilisant ces commandes et ces requêtes :

public void UnAdhérentVientEmprunter ()
{
  var adhérents = requêtes_.RechercheAdherents ("Dupont");
  var adhérentTrouvé = adhérents.ElementAt (12);
  var emprunt = new Commandes.Emprunt
  {
    NuméroAdhérent = adhérentTrouvé.Numéro,
    Description = "Un livre",
    DateDébut = DateTime.Now
  };
  commandes_.DémarrerEmprunt (emprunt);
}
public void UnAdhérentVientRapporter ()
{
  var emprunts = requêtes_.RechercheEmpruntsEnCours ("Un livre");
  var empruntTrouvé = emprunts.ElementAt (5);
  var emprunt = new Commandes.Emprunt
  {
    Identifiant = empruntTrouvé.Identifiant
  };
  commandes_.CloreEmprunt (emprunt);
}

Tout cela est bien sympathique, mais me laisse un goût d'inachevé.

Pourquoi, dans une partie de code qui raconte une histoire, et qui devrait donc ne parler strictement que du "métier", je me retrouve à introduire une distinction entre les commandes et les requêtes ?
Est-ce que je vais parler de cette distinction à un utilisateur du système si nous voulons ensemble valider de tels scénarios d'utilisation ?
Et pourquoi suis-je obligé dans ces scénarios de faire intervenir le numéro des adhérents ou les identifiants des emprunts ?
D'ailleurs, que se passerait-il si le métier n'avait que faire de ces données d'identification ? Devrais-je tout de même les inclure dans les deux modèles ?
L'identifiant des emprunts est, en pratique, complètement inutile pour notre utilisateur. Il n'est là que pour exprimer le lien entre nos deux modèles.
On pourrait presque dire la même chose du numéro d'adhérent. Il aura peut être un sens pour l'utilisateur mais on pourrait aussi se trouver dans des cas où une identification plus naturelle le rendrait complètement insignifiant.[1]

Bref, en ce qui me concerne, le code ci-dessus, n'est pas du code métier.
Si je voulais des scénarios se limitant au langage de l'utilisateur, il me faudrait écrire quelque chose dans ce genre :

public void UnAdhérentVientEmprunter ()
{
  var adhérents = bibliotheque_.RechercheAdherents ("Dupont");
  var adhérentTrouvé = adhérents.ElementAt (12);
  var emprunt = bibliotheque_.NouvelEmprunt;
  emprunt.Emprunteur = adhérentTrouvé;
  emprunt.Description = "Un livre";
  emprunt.DateDébut = DateTime.Now;
  bibliotheque_.DémarrerEmprunt (emprunt);
}
public void UnAdhérentVientRapporter ()
{
  var emprunts = bibliotheque_.RechercheEmpruntsEnCours ("Un livre");
  var empruntTrouvé = emprunts.ElementAt (5);
  bibliotheque_.CloreEmprunt (empruntTrouvé);
}

Comment puis-je faire évoluer ma conception pour en arriver là ?

Tout d'abord, il me faut un seul service donnant accès à l'ensemble des commandes et des requêtes. Rien de compliqué à cela. Si pour des raisons techniques, ils doivent rester séparés, cela restera caché à travers une façade.

L'autre changement concerne l'interopérabilité entre les modèles.
Dans mes deux scénarios, les commandes peuvent prendre en entrée des objets issus de requêtes, soit directement (CloreEmprunt), soit à travers un objet passé à une commande (l'adhérentTrouvé dans DémarrerEmprunt).
Cela nous ramène à la multiplicité des modèles. En ce qui me concerne, je préfère travailler avec un modèle unique car la réalité de l'utilisateur est unique. Pour cela, je définis un seul modèle avec un ensemble d'interfaces.

namespace Modèle
{
  public interface Adhérent
  {
    string Nom { get; set; }
    string Ville { get; set; }
    bool EstEtranger { get; }
  }
}
namespace Modèle
{
  public interface Emprunt
  {
    Adhérent Emprunteur { get; set; }
    string Description { get; set; }
    DateTime DateDébut { get; set; }
    DateTime DateLimite { get; }
  }
}

Les deux modèles précédents se transforment en implémentations techniques du modèle unique.

Pour les commandes, on retrouve à peu près la même implémentation incluant les règles de gestion.

namespace Commandes
{
  public class Adhérent : Modèle.Adhérent, Identification.Adhérent
  {
    public string Nom { get; set; }
    public string Ville { get; set; }
    
    public bool EstEtranger {
      get {
        return Ville == "Paris";
      }
    }
    
    public long Numéro { get; internal set; }
  }
}
namespace Commandes
{
  public class Emprunt : Modèle.Emprunt, Identification.Emprunt
  {
    public Modèle.Adhérent Emprunteur { get; set; }
    public string Description { get; set; }
    public DateTime DateDébut { get; set; }
    
    public DateTime DateLimite {
      get {
        return DateDébut.AddMonths (1);
      }
    }
    
    public long Identifiant { get; internal set; }
  }
}

Pour les requêtes, on rajoute simplement des garde-fous visant à expliciter que certaines opérations ne sont pas disponibles

namespace Requêtes
{
  public class RésuméAdherent : Modèle.Adhérent, Identification.Adhérent
  {
    public string Nom { get { return nom_; } set { throw NI (); } }
    public string Ville { get { throw NI (); } set { throw NI (); } }
    public bool EstEtranger { get { throw NI (); } }
    public long Numéro { get; internal set; }

    internal string nom_;
    
    private Exception NI ()
    {
      return new NotImplementedException ();
    }
  }
}
namespace Requêtes
{
  public class RésuméEmprunt : Modèle.Emprunt, Identification.Emprunt
  {
    public Modèle.Adhérent Emprunteur { get { return emprunteur_; } set { throw NI (); } }
    public string Description { get { return description_; } set { throw NI (); } }
    public DateTime DateDébut { get { throw NI (); } set { throw NI (); } }
    public DateTime DateLimite { get { throw NI (); } }
    public long Identifiant { get; internal set; }
    
    internal RésuméAdherent emprunteur_;
    internal string description_;
    
    private Exception NI ()
    {
      return new NotImplementedException ();
    }
  }
}

Au passage, remarquons l'ajout d'interfaces pour rendre, là aussi, explicite le besoin d'identification technique des entités. Cette identification ne fait pas partie du modèle mais reste un détail d'implémentation nous permettant de mettre en oeuvre une sérialisation pour de la persistance ou pour tout autre besoin.

On remarquera aussi que les règles métier n'ont pas de raison d'être cantonnées à l'implémentation "commandes". On pourrait aussi utiliser des mécanismes d'héritage multiple (mixins, traits...) pour les rendre disponible sur toutes les implémentations.

Je dois ici remercier Jérôme pour avoir lancer une discussion qui m'a amené à écrire ce billet :

Pour moi, la réponse est donc : non, la modélisation des requêtes ne se résume pas à des DTO car je veux avoir un modèle unique. C'est un moyen de garantir une cohérence intrinsèque entre les diverses implémentations du modèle au sein du système sans avoir à pousser tout ce travail sur les scenarios utilisateur.

Pour moi, CQRS reste un pattern technique dont la présence permet l'optimisation des performances, à travers 2 mécanismes de persistance dédiés respectivement à la modification et à la lecture seule, ou l'amélioration de la fiabilité, en utilisant par exemple un système non destructif sur la partie accessible en écriture.
Il permet probablement d'autres choses que je n'imagine même pas mais tant qu'il n'a aucune signification pour l'utilisateur, il n'a pas de raison valable pour transparaître dans la zone métier de mon système. C'est à cette condition de non ingérence des aspects de "technique informatique" que je pourrai faire évoluer ma modélisation du métier et sa validation par les utilisateurs.

Le code source qui a servi à écrire ce billet est disponible ici en intégralité.

Note

[1] Par exemple, dans une bibliothèque interne à un établissement scolaire on peut identifier "naturellement" l'élève avec son nom et le nom de sa classe