CQS, le principe qui sépare ce que ton code dit de ce que ton code fait
Tu appelles une fonction qui ressemble à une lecture. Et au passage, elle modifie quelque chose. Trois mois plus tard, un bug arrive sans prévenir et personne ne sait par où commencer. Le principe Command Query Separation existe pour rendre ce genre de scène impossible.
Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.
La plupart des bugs subtils qu'on rencontre en équipe ne viennent pas d'un mauvais algorithme. Ils viennent d'une fonction qui ne fait pas ce que son nom laisse croire. Tu lis le code, tu comprends ce qu'il devrait faire, et la production te montre que tu as mal compris.
Toi, tu as déjà vécu ça. Un appel banal qui déclenche une mise à jour cachée. Une méthode getUser qui crée l'utilisateur s'il n'existe pas. Un compteur qui s'incrémente parce que tu as juste voulu lire sa valeur. À chaque fois, la même frustration : tu pensais poser une question, tu as donné un ordre sans le savoir.
Le problème n'est pas la complexité du code. C'est le mélange des intentions. Lire et modifier sont deux opérations qui n'ont rien à voir, et pourtant on les fait cohabiter dans la même fonction par habitude ou par paresse. C'est exactement ce que CQS propose de séparer une fois pour toutes.
Dans la suite, tu vas voir d'où vient ce principe, comment il s'applique sur du code que tu écris déjà, et où il commence à coincer si tu le pousses trop loin.
Le problème que tu rencontres tous les jours sans le nommer
Imagine une application bancaire. Un développeur écrit une méthode qui s'appelle getAccountBalance. Tu la lis, tu comprends qu'elle retourne le solde du compte. Tu l'utilises dans ton code, satisfait.
Sauf que cette méthode, en interne, enregistre aussi la consultation dans une table d'audit. Et au passage, si le solde est négatif depuis plus de trente jours, elle déclenche l'envoi d'un mail à l'utilisateur. Tu n'as rien demandé de tout ça. Tu voulais juste un nombre.
Le jour où tu écris un test unitaire qui appelle getAccountBalance dix fois pour vérifier la cohérence, tu te retrouves avec dix lignes d'audit en base et dix mails partis chez le client. Personne n'a écrit ça nulle part. Le nom de la méthode mentait, et toi tu as fait confiance au nom.
Ce genre de situation a un coût qu'on sous-estime toujours. Tu ne peux plus lire le code rapidement. Tu dois ouvrir chaque fonction et vérifier ce qu'elle fait vraiment. La vélocité de l'équipe s'effondre, et personne ne sait pourquoi. Cette idée rejoint d'ailleurs un autre principe que j'ai déjà couvert dans l'article sur le principe de moindre surprise, parce qu'au fond CQS en est une application particulière.
Le principe, énoncé simplement
CQS signifie Command Query Separation, séparation entre les commandes et les requêtes. Le principe a été formulé par Bertrand Meyer dans les années 1980, dans son livre Object-Oriented Software Construction. La règle tient en une phrase :
Une requête, c'est une question posée au système. Elle retourne quelque chose et ne change rien. Tu peux l'appeler dix fois, mille fois, l'état du programme reste identique. En anglais, on parle de fonction pure ou de fonction sans effet de bord.
Une commande, c'est un ordre donné au système. Elle change quelque chose (une variable, un fichier, une ligne en base) et idéalement ne retourne rien, ou retourne juste un signal indiquant si l'opération s'est bien passée.
La conséquence pratique est radicale : quand tu lis une signature de fonction, tu sais immédiatement à quoi t'attendre. Pas besoin d'ouvrir le corps. Pas besoin de lire les tests. Le nom et le type de retour suffisent à te dire si tu vas modifier quelque chose ou seulement observer.
Exemples concrets, avant et après
Cas 1 : la méthode qui ment sur son nom
Voici un exemple en PHP qui viole CQS sans en avoir l'air :
class UserRepository
{
public function getUser(int $id): User
{
$user = $this->db->find($id);
if ($user === null) {
$user = new User($id);
$this->db->save($user);
}
return $user;
}
}
À lire, c'est une requête. Le nom commence par get, le type de retour est un User. Mais en réalité, c'est aussi une commande, parce que la méthode crée et sauvegarde un nouvel utilisateur si l'identifiant n'existe pas. Tu ne peux pas le deviner sans ouvrir le code.
La version qui respecte CQS sépare les deux intentions :
class UserRepository
{
public function findUser(int $id): ?User
{
return $this->db->find($id);
}
public function createUser(int $id): User
{
$user = new User($id);
$this->db->save($user);
return $user;
}
}
Le code qui appelle ces méthodes devient explicite. Si tu veux récupérer un utilisateur sans le créer, tu utilises findUser. Si tu veux le créer, tu utilises createUser. Et si tu veux le comportement de l'ancienne méthode, tu l'écris toi-même, mais cette fois c'est visible dans le code appelant.
Cas 2 : la pile qui retourne ET dépile
Tu connais peut-être la méthode pop d'une pile en Python ou en JavaScript. Elle retire le dernier élément et le retourne en même temps. C'est commode, mais c'est exactement la violation que CQS dénonce :
stack = [1, 2, 3]
value = stack.pop() # retourne 3 et modifie la pile
Une version stricte CQS aurait deux méthodes : top pour lire le dernier élément sans le retirer, et remove pour le retirer sans le retourner. Eiffel, le langage de Bertrand Meyer, fait exactement ça. Python et JavaScript ont choisi la commodité.
Ce cas montre que CQS n'est pas une loi physique. C'est une discipline. Les langages eux-mêmes la transgressent parfois pour des raisons pratiques. Mais quand tu écris ton propre code métier, la transgression a un coût qui se paie en bugs.
Cas 3 : l'API HTTP qui mélange tout
CQS s'applique aussi aux API. Quand tu conçois une route REST, tu fais déjà du CQS sans le savoir. GET est une requête, POST, PUT, PATCH, DELETE sont des commandes. Le verbe HTTP porte l'intention.
Le problème surgit quand un développeur cale du métier dans une route GET. Une route comme GET /api/users/123/refresh-token qui régénère le token au passage est une bombe à retardement. Les caches HTTP, les proxys, les bots de scraping peuvent tous appeler cette URL sans intention de la modifier, et invalider les tokens en chaîne.
Si tu veux creuser cette logique de séparation côté backend, ça fait partie des sujets qu'on aborde dans la formation Symfony 7 : initiation au framework PHP professionnel, où la séparation des responsabilités dans les controllers et les services est un fil rouge du cursus.
Pourquoi ça change ta façon de lire le code
Le bénéfice principal de CQS n'est pas dans le code que tu écris, c'est dans le code que tu lis. Quand toutes les méthodes d'une base de code respectent le principe, tu peux scanner un fichier en trente secondes et savoir ce qui modifie l'état et ce qui ne le modifie pas. Sans CQS, tu dois ouvrir chaque méthode, lire ligne par ligne, et tenir mentalement la liste des effets de bord cachés.
Le deuxième bénéfice est dans les tests. Une requête pure est triviale à tester. Tu lui donnes une entrée, tu vérifies la sortie. Pas de mock, pas de fixture compliquée. Une commande, elle, demande plus de plomberie, mais au moins tu sais qu'elle en demande. Tu n'as pas la mauvaise surprise d'un test de lecture qui modifie ta base.
Le troisième bénéfice concerne la mise en cache. Tu ne caches que des requêtes. Si tu respectes CQS, tu repères les méthodes cachables au premier coup d'oeil. Sinon, tu risques de cacher une commande et de produire des bugs invisibles qui ne se manifestent qu'en production sous charge.
Les pièges qui guettent quand tu appliques CQS
Piège 1 : devenir intégriste
CQS est une règle, pas une religion. Certains cas pratiques justifient la transgression. Une méthode pop de pile est plus simple que deux méthodes top et remove dans la plupart des contextes. Une opération atomique comme incrementAndGet en programmation concurrente doit retourner la nouvelle valeur sans faire deux appels séparés, sinon tu perds l'atomicité.
La bonne posture est de connaître la règle et de transgresser en conscience. Pas par paresse, pas par habitude. Tu transgresses quand le coût de la séparation dépasse le bénéfice de la clarté.
Piège 2 : confondre CQS et CQRS
CQS et CQRS sont deux choses différentes. CQS est un principe de méthode, à l'échelle d'une fonction. CQRS, c'est Command Query Responsibility Segregation, un pattern d'architecture qui sépare le modèle de lecture et le modèle d'écriture à l'échelle d'un système entier. CQRS s'inspire de CQS mais opère à un autre niveau, souvent avec des bases de données distinctes pour les lectures et les écritures.
La confusion est fréquente sur les forums et dans les entretiens techniques. Si on te parle de CQS, on parle de signatures de fonctions. Si on te parle de CQRS, on parle d'architecture distribuée. Les deux peuvent coexister, mais ils ne résolvent pas le même problème.
Piège 3 : les requêtes qui mentent en interne
Une requête peut avoir des effets de bord invisibles qui passent sous le radar. Un cache qui se remplit, une métrique qui s'incrémente, un log qui s'écrit. Techniquement, ce sont des modifications d'état. Faut-il les considérer comme des violations de CQS ?
La réponse pragmatique est non, à condition que ces effets soient idempotents et invisibles depuis l'extérieur. Un log qui s'écrit ne change pas le résultat de la prochaine requête. Un cache qui se remplit accélère les appels suivants sans modifier les valeurs. Tant que l'observateur externe ne peut pas distinguer une requête qui log d'une requête qui ne log pas, le principe est respecté en esprit.
Piège 4 : forcer le retour des commandes à void
Certains lisent CQS et concluent qu'une commande ne doit jamais retourner quoi que ce soit. C'est trop strict. Une commande peut légitimement retourner un statut (succès ou échec), un identifiant nouvellement créé, ou une exception. Ce qu'elle ne doit pas faire, c'est retourner une information métier que tu utiliserais comme si la méthode était une requête.
Un createUser qui retourne l'identifiant de l'utilisateur créé reste une commande honnête. Un updateUser qui retourne l'objet utilisateur entier commence à poser question : pourquoi retourner ce que tu pourrais aller chercher avec un findUser ? Le doute, c'est déjà le signe d'un design à revoir.
Comment appliquer CQS sur ton code dès demain
La première étape, c'est l'audit silencieux. Tu ouvres une classe que tu maintiens, tu listes ses méthodes, et tu poses pour chacune la question : est-ce une commande ou une requête ? Les méthodes ambiguës sont celles qui posent problème. Tu les marques pour refactor plus tard.
La deuxième étape, c'est la convention de nommage. Les requêtes commencent par get, find, is, has, count, search. Les commandes commencent par create, update, delete, add, remove, send, publish, save. Quand le nom commence par get et que la méthode modifie quelque chose, tu as un bug d'intention à corriger.
La troisième étape, c'est la séparation progressive. Tu ne refactores pas tout d'un coup. À chaque fois que tu touches une méthode ambiguë pour une autre raison, tu profites du passage pour la séparer en deux. Ce rythme rejoint la logique de la Boy Scout Rule : laisser le code un peu plus propre que tu l'as trouvé, sans bloquer la pull request pendant trois jours.
La quatrième étape, c'est l'éducation de l'équipe. Tu fais passer le mot lors des revues de code. Tu pointes les violations sans dramatiser. Au bout de quelques semaines, l'équipe commence à écrire du code CQS-compatible par réflexe, et les bugs liés aux effets de bord cachés disparaissent presque entièrement.
Ce que CQS dit vraiment de ton métier
CQS est un principe simple en apparence, profond en réalité. Il t'oblige à clarifier pour chaque action de ton code : est-ce que j'observe, ou est-ce que j'agis ? Cette distinction paraît évidente, mais elle ne l'est pas dans la pratique quotidienne, parce qu'on a tendance à empiler les responsabilités pour gagner du temps à court terme.
Le gain à long terme est énorme. Du code qui se lit en surface plutôt qu'en profondeur. Des tests qui se rédigent rapidement. Des bugs de production qui n'existent plus parce que les effets de bord ne se cachent plus derrière des noms innocents.
Quand tu intègres CQS dans tes réflexes, tu commences à voir les violations partout : dans les frameworks que tu utilises, dans le code des collègues, dans le tien d'il y a six mois. C'est désagréable au début. Puis tu te rends compte que c'est exactement ce que tu cherchais, un filtre mental qui te permet de juger un design en cinq secondes.
Questions fréquentes
Est-ce que CQS s'applique aux langages fonctionnels ?
Les langages fonctionnels purs comme Haskell vont plus loin que CQS, parce qu'ils interdisent par construction tout effet de bord en dehors de structures dédiées (les monades). Le principe CQS y est appliqué de force par le langage. Dans des langages plus permissifs comme JavaScript ou Python, CQS reste une discipline volontaire, mais l'esprit est le même.
CQS et SOLID, c'est lié ?
CQS n'est pas dans SOLID, mais il croise plusieurs de ses lettres. Le S de SOLID, Single Responsibility, recouvre partiellement CQS : une fonction qui fait deux choses (lire ET modifier) a plus d'une responsabilité. CQS est plus précis et plus pratique à appliquer au quotidien que la définition floue de Single Responsibility.
Comment savoir si ma méthode viole CQS ?
Pose-toi deux questions. Est-ce que cette méthode retourne quelque chose ? Si oui, regarde si elle modifie aussi un état (variable, base, fichier, réseau). Si elle fait les deux, elle viole CQS. Une autre méthode rapide consiste à imaginer l'appeler dix fois de suite : si le résultat change ou si l'état du système diverge, c'est probablement une commande déguisée en requête.
CQS ralentit-il le développement ?
À court terme, oui, légèrement. Tu écris parfois deux méthodes là où une seule suffisait. À moyen terme, tu gagnes énormément, parce que la dette mentale liée aux effets de bord cachés disparaît. Les développeurs qui pratiquent CQS depuis longtemps ne le vivent plus comme un effort, c'est devenu un réflexe au moment de nommer une fonction.
Est-ce que les ORM respectent CQS ?
Pas toujours. Doctrine et Hibernate ont des méthodes comme persist ou flush qui sont clairement des commandes, et des méthodes find qui sont clairement des requêtes. Mais certains patterns d'ORM, comme le lazy loading, peuvent déclencher des requêtes SQL au moment où tu accèdes à une propriété d'objet, ce qui brouille la frontière. Le mieux est de bien comprendre ce que ton ORM fait en coulisses avant de juger.
Quand transgresser CQS sans culpabiliser ?
Quand l'atomicité l'exige (incrementAndGet en concurrence), quand le coût de la séparation devient absurde (pop d'une pile dans un contexte trivial), ou quand le langage et son écosystème poussent à le faire. La règle est de transgresser en connaissance de cause, en documentant le choix, et en restant minoritaire dans le code. Une base où 95% des méthodes respectent CQS reste largement plus lisible qu'une base où le principe est ignoré.