Commentaires
Et si on remettait le business au centre de nos applications ?
Sommaire
Préambule
La puissance et l'utilité de Symfony ne sont plus à démontrer, c'est devenu un framework reconnu dans le monde professionnel. Il nous permet ainsi de répondre à beaucoup de problématiques techniques plus ou moins facilement, mais de manière assez élégante en général.
Revers de la médaille, on passe souvent plus de temps à écrire des lignes propres au framework. On se concentre beaucoup trop sur la technique.
Vous allez me dire qu'avec Symfony2 on écrit maintenant beaucoup plus de code métier pur php qu'on injecte alors en service. Oui ! Mais souvent ce sont des services "utilitaires" qui viennent là encore répondre à une problématique technique. En fait on écrit trop souvent du code pour notre framework / plateforme technique.
Aujourd'hui si vous devez développer une nouvelle application web, par quoi allez-vous commencer ? Installer Symfony pardi ! Et pourtant vous avez une problématique business à résoudre derrière votre application. Pourquoi alors commencer par la technique ?
Minimiser sa dette technique
On pense souvent qu'utiliser un framework maitrisé est suffisant, que les gardes fous placés dedans nous forcent suffisament à embrasser les bonnes pratiques pour ne pas trop se prendre la tête sur l'architecture de notre code métier.
Et pourtant l'endroit qui acquiert au fil du temps la plus grande dette technique c'est le coeur même de vos applications : la partie métier. Le MVC est un classique, mais on s'aperçoit qu'il ne résoud pas tout. Autant Symfony gère très bien le V et le C autant le M est majoritairement à votre charge.
Est-ce que vous êtes capable d'extraire facilement votre partie métier de votre app Symfony ?
Pourtant votre framework devrait être un détail d'implémentation. Ce n'est pas lui votre application, c'est votre outil, ne le subissez pas !
Le fil rouge en exemple
Afin d'illustrer un peu tout ce que je vais essayer de pointer du doigt, nous allons nous mettre en tête de construire une application qui se concentrera principalement sur des enjeux business/métier.
Imaginons développer une application qui a en charge un établissement de jeu de cartes : Poker, Blackjack, Tarot, ...
L'interface sera minime mais sera réalisée en mode web pour ressembler au plus près à nos applications classiques.
Démonstration
Créons notre premier contrôleur dans notre bundle pour créer nos parties de cartes :
<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Afsy\Bundle\CardBundle\Entity\Game;
class GameController extends Controller
{
public function newAction(Request request)
{
$game = new Game();
$form = $this->createFormBuilder($game)
->add('nbPlayers', 'text')
->add('gameType', 'choice')
->add('create', 'submit')
->getForm()
;
$form->handleRequest($request);
if ($form->isValid()) {
$game = $form->getData();
$em = $this->get('doctrine')->getManager();
$em->persist($game);
$em->flush();
}
return $this->render('AwUnoBundle:Game:new.html.twig', array(
'form' => $form->createView()
));
}
}
Code assez classique que certains doivent avoir dans leurs applications. Qu'est-ce que vous y changeriez ?
"Il faut créer le formulaire dans une classe à part !"
Oui ca serait plus sympa techniquement. Mais en soit en quoi ça va nous aider dans nos problématiques métier ?
"Il faut jamais faire de persist/flush dans notre contrôleur ! Créons un service qui s'en occupe."
Ok c'est un début.
<?php
class GameManager
{
protected $em;
public function __construct($em)
{
$this->em = $em;
}
public function save($game)
{
return $this
->persist($game)
->flush()
;
}
public function persist($game)
{
$this->em->persist($game);
return $this;
}
public function flush()
{
$this->em->flush();
return $this;
}
}
Et modifions notre contrôleur en conséquence :
<?php
/***/
if ($form->isValid()) {
$game = $form->getData();
$manager = $this->get('game.manager');
$manager->save($game);
}
/***/
Ok on abstrait notre couche de persistence par ... une nouvelle couche. Ça se tient, notre bête contrôlleur n'a pas à connaître le moyen de sauvegarder notre donnée. En effet, d'un point de vue technique il n'a pas à le savoir. On a donc résolu une nouvelle problématique technique.
Mais si on ajoute maintenant une problématique business ?
Admettons qu'on veuille au moins 3 participants dans une partie de Blackjack et 4 dans une partie de Poker et Tarot.
Comment répondre à cette problématique ?
Ca ressemble à une règle de validation qui prend en compte 2 attributs. Symfony propose un bon système de validation et un callback validator, ca semble la bonne solution !
# src/Afsy/Bundle/CardBundle/Resources/config/validation.yml
Afsy\Bundle\CardBundle\Entity\Game:
constraints:
- Callback: validateNbPlayers
<?php
use Symfony\Component\Validator\ExecutionContextInterface;
class Game
{
/***/
public function validateNbPlayers(ExecutionContextInterface $context)
{
$message = null;
if ('blackjack' === $this->gameType && 3 > $this->nbPlayers) {
$message = 'Au moins 3 joueurs pour jouer au UNO';
} elseif ('poker' === $this->gameType && 4 > $this->nbPlayers) {
$message = 'Au moins 4 joueurs pour jouer au poker';
} elseif ('tarot' === $this->gameType && 4 > $this->nbPlayers) {
$message = 'Au moins 4 joueurs pour jouer au tarot';
}
if (null !== $message) {
$context->addViolationAt(
'nbPlayers',
$message,
array(),
null
);
}
}
/***/
}
Ok bien vu ! On a réussi à utiliser la technique pour résoudre une problématique métier !… Et c'est là où les ennuis commencent en général car on n'est toujours pas préparé à ce qui va suivre.
Maintenant, j'ai envie qu'entre 20h et 23h, pour ne pas monopoliser des tables de jeux trop à blanc, on ne puisse créer des parties qu'avec au moins 6 participants pour chacun des jeux.
Ah mince ! Va falloir changer notre méthode de validation.
<?php
/***/
$now = new \DateTime;
$start = clone $now;
$start->setTime(20, 0, 0);
$to = clone $now;
$to->setTime(23, 0, 0);
if (($start <= $now && $now <= $to) && 6 > $this->nbPlayers) {
$message = 'Au moins 6 joueurs pour jouer entre 20h et 23h';
} else {
if ('uno' === $this->gameType && 3 > $this->nbPlayers) {
$message = 'Au moins 3 joueurs pour jouer au UNO';
} elseif ('poker' === $this->gameType && 4 > $this->nbPlayers) {
$message = 'Au moins 4 joueurs pour jouer au poker';
} elseif ('tarot' === $this->gameType && 4 > $this->nbPlayers) {
$message = 'Au moins 4 joueurs pour jouer au tarot';
}
}
/***/
?>
Ok, problème résolu. Bon pas simple à tester mais ce n'est pas grave, on verra ça plus tard…
Mais les admins en backoffice aimeraient pouvoir créer des parties de poker à 3 tout le temps en fait.
Utilisons les groupes de validation ?
Mais maintenant, on aimerait que quand il y a au moins 10 participants, un éclairage particulier soit mis en place sur la table. Ok c'est un simple if
en fait. Mais à quel endroit ajouter cette vérification et déclencher l'allumage ?
- Dans notre contrôleur ?
- Dans notre manager ?
- Dans notre validateur ?
STOOOOP !
Et si on commençait à réfléchir plus loin que 2 coups ? Descendre votre code dans une classe appelée depuis votre contrôleur (notre GameManager ici) n'est pas un pattern en fait.
Et si l'on partait du postulat que oui forcément les règles métiers vont très vite évoluer comme dans toute application et qu'on s'armait en conséquence pour pouvoir capitaliser sur notre métier, le rendre le plus explicite et évolutif possible ?
Et surtout le rendre indépendant de tout framework ?
Vous pensez déjà que ça va être compliqué, que compliqué rime avec difficile à maintenir. Et si je vous disais qu'en fait c'est plus simple. Il suffit, comme tout le reste, d'adopter le bon design !
Solutions avec le DDD
Dans notre démonstration on voit bien que notre méthode validateNbPlayers
devient :
- difficile à lire ;
- difficile à tester ;
- difficile à faire évoluer.
On pourrait la sortir dans une classe à part pour l'exploser un peu, Symfony le permet. Mais du coup, le seul lien entre cette validation et notre entité serait Symfony. Est-ce vraiment normal ?
Attention, je ne dis pas que les outils de Symfony ne sont pas adaptés. Je dis juste qu'on ne les utilise toujours pas dans les bonnes conditions.
Rassurez-vous le développement objet a 30 ans, ces problématiques ont déjà été rencontrées par d'autres. La solution est en fait d'appliquer, comme vous le faites pour vos problématiques techniques, des patterns pour votre partie métier. Ces différents concepts sont réunis sous le DDD : Domain Driven Design.
L'objectif est de remettre le métier au coeur de vos préoccupations, l'implémentation technique n'étant alors que secondaire. L'approche est intéressante car en structurant notre partie métier, on en vient souvent à simplifier notre partie technique qui en devient limpide.
Dans notre exemple notre domaine est donc les jeux de cartes. On ne veut pas que l'implémentation que ce soit du stockage, du routing ou de la vue ne viennent changer la façon dont on va concevoir toute la logique business.
Regardons quelques exemples en détail qui pourraient nous aider à organiser et structurer notre partie métier.
Attention, je ne vais pas rentrer dans le détail de chaque pattern présenté. L'idée est plus de sensibiliser et montrer rapidement leur intérêt pour vos applications. Par contre, vous trouverez à la fin la bibliographie sur laquelle je me suis appuyé pour mettre tout ça en place ainsi que l'application fil rouge réalisée pour les besoins de l'article.
Spécification
Ce pattern est le plus simple à mettre en place. Vous l'avez peut-être même déjà utilisé sans vous en rendre compte. Et pourtant il va nous aider à organiser notre métier avec une sémantique qui va guider tous les développeurs de l'application.
<?php
class PokerNbPlayersSpecification
{
public function isSatisfiedBy($game)
{
return ('poker' === $game->gameType && 3 <= $game->nbPlayers);
}
}
Une vraie classe métier ! Une sémantique guidée par le métier, un code indépendant, testable facilement. Que demande le peuple ?
"Comment on s'en sert pardi !"
<?php
/***/
public function validateNbPlayers(ExecutionContextInterface $context)
{
$pokerSpecification = new PokerNbPlayersSpecification();
if (!$pokerSpecification->isSatisfiedBy($this)) {
$message = 'Au moins 3 joueurs pour jouer au Poker';
}
//...
}
/***/
Évidemment, on peut maintenant facilement combiner toutes sortes de spécifications (AND, OR, XOR,...) et surtout utiliser une librairie plutôt que de refaire toujours les mêmes choses tel que Ruler ou Hoa\Ruler.
Mais le concept est là ! Comme Hugo l'expliquait dans son article sur STUPID/SOLID, le nommage est difficile mais indispensable. Et selon le DDD, plus vous l'orientez métier, et plus il vous sera utile.
CQRS
Command and Query Responsibility Segregation.
Un autre grand concept du DDD, qui se résume à séparer vos opérations de lecture et d'écriture avec pour objectif de rendre chacune d'elle plus simple et plus évolutive. Et encore une fois, la sémantique va bien nous y aider.
Dans votre application classique 3-tiers, vous communiquez avec votre base de données de la même façon pour la lire que pour écrire dedans. Au final, on oriente tout en fonction de nos données, de la façon dont on va les stocker... Encore une fois, on privilégie la technique.
Pourtant quand on y pense, pour la grande majorité de nos applications, on passe genre 80% à lire ? 90% ? Et pourtant au fil du temps ce sont nos requêtes de lectures qui deviennent complexes.
En quoi ça nous aide avec nos problématiques métiers ?
Si on reprend notre contrôleur initial, si on enlève le nom de notre action. Comment sait-on de quoi on parle ?
La partie Command
prend ainsi tout son intérêt. Depuis vos contrôleurs vous allez donner des ordres à votre application de la manière la plus précise possible : "Je veux créer une nouvelle partie de Poker avec 4 joueurs".
<?php
// Je veux créer une nouvelle partie
class StartGameCommand
{
// de Blackjack/Poker/Tarot
public $gameType;
// Avec 4/5/6/N joueurs
public $nbPlayers;
}
Tadam ! Et oui toujours rien de compliqué mais descriptif.
On associe souvent un handler à une commande, qui va être un peu le service chargé de savoir quoi en faire.
<?php
class StartGameCommandHandler
{
public function handle($command)
{
$game = new Game();
$game->start($command->gameType, $command->nbPlayers);
}
}
Rien de plus encore une fois. On notera l'introduction de la nouvelle méthode start
sur notre modèle qu'on verra plus bas.
On a maintenant un service capable d'encapsuler le traitement métier lié à la création d'une partie. Si à la création d'une partie, vous avez besoin de créer une autre entité ou autre nécessaire à la création, c'est par ici !
Par convention dans ce pattern, on utilise un Bus pour enregistrer tous nos handlers de commandes et ainsi n'avoir qu'un seul service à utiliser. Je vous en passe l'implémentation qui ne nous intéresse pas. Voici par contre notre contrôleur maintenant :
<?php
class GameController extends Controller
{
public function newAction(Request $request)
{
$startGame = new StartGameCommand();
$form = $this->createFormBuilder($startGame)
->add('nbPlayers', 'text')
->add('gameType', 'choice')
->add('create', 'submit')
->getForm()
;
$form->handleRequest($request);
if ($form->isValid()) {
$startGame = $form->getData();
// Notre bus va se charger d'executer notre handler adéquat en fonction de l'event passé
$this->get('command.bus')->execute($startGame);
}
return $this->render('AwUnoBundle:Game:new.html.twig', array(
'form' => $form->createView()
));
}
}
Pas de révolution, mais 2 grosses améliorations :
- On ne travaille plus directement avec nos entités via notre formulaire. Plus besoin de setter/getter.
Et c'est souvent plus simple car on n'a pas besoin de faire des pirouettes dans notre formulaire. Notre command est notre objet dédié à notre action et pas à notre état !
- On voit maintenant qu'on parle d'une création de partie sans avoir le nom du contrôleur et l'action. Sous entendu que ce code à un autre endroit a tout autant de sens.
Revenons maintenant à notre model et sa nouvelle méthode:
<?php
class Game extends DomainEventProvider
{
protected $id;
protected $gameType;
protected $nbPlayers;
protected $started = false;
public function start($gameType, $nbPlayers)
{
$this->gameType = $gameType;
$this->nbPlayers = $nbPlayers;
$this->raise(new GameStartedEvent($gameType, $nbPlayers));
}
}
class DomainEventProvider
{
private $events;
protected function raise($event)
{
$this->events[] = $event;
}
public function getEventsSubmitted()
{
return $this->events;
}
}
Plutôt que de faire bêtement des set, on utilise une vraie sémantique dans notre méthode qui décrit ce qu'elle fait. On peut faire ça plus facilement car notre modèle ici est dédié à l'écriture !
Et bien sûr le petit détail, l'appel à la méthode raise
qui nous permettra de stocker dans notre modèle les changements d'états. On pourra alors les relire à la fin et les répercuter sur notre modèle de lecture mais aussi sur n'importe quel listener.
Ainsi on résoud par la même occasion notre fameuse problématique d'éclairage ! Il nous suffira de nous brancher sur l'event GameStartedEvent
et de vérifier si on a 10 joueurs ou pas.
EventSourcing
Souvent utilisé en même temps que le CQRS, l'Event Sourcing est basé sur le principe de stocker les flux plutôt que les stocks de vos données. On va ainsi stocker l'action que nous avons créer une partie avec l'id 123 plutôt que de stocker la partie en elle même.
Oui étrange de prime abord. Et pourtant très puissant.
Aujourd'hui si j'enregistre en bdd un joueur du nom de "Joueur 1", que celui-ci change ensuite de nom en "Joueur 2", je perds son nom original. Dommage non quand on sait que les données sont le coeur de tout système informatique !
A l'inverse si je stock l'action de créer un joueur avec le nom "Joueur 1" puis que je stocke l'action du joueur de changer de nom en "Joueur 2", et bien mon état final sera le même et pourtant j'ai stocké quelque part une information supplémentaire sur mon joeur. L'event sourcing c'est ça !
Rien de compliqué techniquement en plus. On pourrait écrire nos actions/events dans un simple fichier texte. On pourrait alors pour cette partie considérer utiliser un stockage bien différent de notre bdd relationnelle classique.
Et pour les afficher ?
Il suffit de garder une "vue" de l'état de nos données synchronées avec notre flux d'actions. Et si demain on veut changer notre vue, rien de plus simple du coup, vu qu'elle n'est plus qu'un état, totalement décorellé de notre stockage !
Et plus encore
Le DDD regroupe beaucoup d'autres concepts dont certains vous sont familiers si vous utilisez Doctrine : Repository par exemple.
Rien n'est figé d'ailleurs, aucune règle n'est universelle et applicable dans tous les cas. C'est aussi notre job de faire la part des choses. Mais quand certains concepts nous facilitent la vie, embrassent totalement la programmation objet, pourquoi s'en priver ?
L'application fil rouge
Pour illustrer la totalité des concepts introduits voici une petite sandbox Symfony que j'ai développé :
https://github.com/tyx/cqrs-php-sandbox
Elle utilise la seule implémentation sérieuse de CQRS/EventSourcing en PHP de Benjamin Eberlei :
https://github.com/beberlei/litecqrs-php
Pour l'instant seul le jeu blackjack est développé. On pourrait certainement discuter sur plusieurs détails d'implémentation. Mais l'objectif n'est pas de donner une réponse universelle, je suis bien loin de détenir la véritié ;)
Mais de montrer qu'aujourd'hui le DDD n'est plus un simple fantasme, il peut être utilisé au coeur de nos applications Symfony.
Bibliographie
Voici la listes des différentes présentations, vidéos et articles de blog qui m'ont servi de support pour écrire cet article. Je me permet de les lister car les ressources sont encore trop rares sur le sujet.
DDD
DDD avec Doctrine2
Spécification
CQRS / Event Sourcing
Exemple d'implémentation de CQRS