Accueil /  Blog / Design Patterns / Le pattern Chaîne de responsabilité avec Symfony

Le pattern Chaîne de responsabilité avec Symfony

Publié le mercredi 20 juillet 2022

Apprenez à rendre votre code plus léger et maintenable avec le design pattern Chaîne de responsabilité avec Symfony

Le pattern Chaîne de Responsabilité

Ce pattern comportemental a pour vocation de créer un ensemble de classes traitant une requête de différentes manières sans avoir conscience de l'objet effectivement responsable du traitement.

De façon plus concrète l'idée est d'avoir un contrat (classe abstraite ou interface) capable de passer une requête a chacun des objets implémentant ce contrat.

Voici un schéma décrivant le processus:

Pattern Chaîne de responsabilité

En plus de rendre votre application maintenable plus facilement, la structure et le découpage qu'impose ce pattern apporte plus de souplesse et de simplicité à votre code.

Exemple concret

Imaginons que votre application doive pouvoir envoyer des notifications à des utilisateurs.

class MessageService
{
    public function sendNotification(Message $message): void
    {
        //...
    }
}

Maintenant on vous demande d'ajouter de nouveaux comportements similaires: - Envoyer le message par mail - Envoyer le message par sms - Envoyer le message vers via une api tierce

Alors bien évidemment, nous pouvons complexifier notre service afin de le rendre capable d'effectuer toutes ces actions possibles, mais cela risque d'être difficile à maintenir, surtout si d'autres systèmes de communication viennent se greffer petit à petit.
Et puis n'oublions pas le principe de responsabilité unique qui n'est plus du tout respecté.

Mise en oeuvre avec Symfony

Avant de modifier notre service et de créer nos handlers, identifions les avec une interface. Cette interface a pour but d'imposer 2 méthodes: - La méthode handle() qui appliquera le code fonctionnel métier correspondant. - La méthode getPriority() qui nous permettra d'ordonner nos handlers

Voici à quoi cela pourrait ressembler.

namespace App\Notification;

interface MessageSenderInterface 
{
     public function handle(Message $message): void;
}
namespace App\Notification;

interface MessageHandlerInterface extends MessagesSenderInterface
{
     public static function getPriority(): int;
}

Créer le sender

Mettons en place le sender, cette classe aura pour métier d'exécuter nos handlers. Il suffit de modifier quelque peu votre service précédemment créé.

namespace App\Service;

class MessageService implements MessageSenderInterface
{
    public function __construct(
        /**
         * @var iterable<MessageHandlerInterface>
         */
        private iterable $handlers,
    ) {
    }

    public function sendNotification(Message $message): void
    {
        foreach ($this->handlers as $handler) {
            $handler->handle($message);
        }
    }
}

Créer des handlers

Ensuite créer quelques handlers.

namespace App\Notification;

class NotificationHandler implements MessageHandlerInterface
{
     public function handle(Message $message): void
     {
          //todo envoyer une notification
     }

     public static function getPriority(): int
     {
          return 1;
     }
}
namespace App\Notification;

class SmsNotificationHandler implements MessageHandlerInterface
{
     public function handle(Message $message): void
     {
          //todo envoyer un SMS
     }

     public static function getPriority(): int
     {
          return 2;
     }
}
namespace App\Notification;

class MailNotificationHandler implements MessageHandlerInterface
{
     public function handle(Message $message): void
     {
          //todo envoyer un email
     }

     public static function getPriority(): int
     {
          return 3;
     }
}

Utilisation dans un controller

Nous pouvons maintenant utiliser notre service dans un controller.

namespace App\Notification;

class FooController
{
     public function action(MessageSenderInterface $sender): void
     {
          $sender->send($message);
     }
}

Configurer Symfony

Nous savons qu'à l'aide de symfony nous pouvons injecter une collection de service. De plus nous pouvons leur donner un ordre de priorité ce qui peut rendre la conception du code plus flexible.

services:
    _instanceof:
        App\Notification\MessageHandlerInterface:
            tags:
                - { name: 'notification.handler' }

    App\Service\MessageSenderInterface: '@App\Service\MessageService'
    App\Service\MessageService:
        arguments:
             $handlers: !tagged_iterator { tag: 'notification.handler', default_priority_method: getPriority }

Grâce à cette configuration, notre service recevra une collection d'objets ordonnés via le retour de chaque méthode getPriority().

Conclusion

Ce pattern est donc intéressant pour restructurer une classe ayant pour métier de réaliser différentes actions, afin de séparer le code de chaque action en une classe dédiée à chacune d'entre elle.

Symfony vous aidera à la mise en place de ce pattern. De plus, vous gagnerez en clarté et en légèreté du code, mais aussi en maintenabilité et flexibilité, ce qui permettra une évolution plus rapide.

Suivez notre actualité en avant première. Pas plus d’une newsletter par mois.