Aller au contenu

Design Pattern Factory Method

Réduire les couplages forts dans votre code

Introduction

Factory Method est un design pattern de création. Son objectif est de déléguer la création d'un objet à une méthode spécialisée, afin que le code manipule une abstraction, plutôt que des classes. Les classes dérivées de cette abstraction décident alors du type d'objet à instancier, en redéfinissant cette méthode.

Problématique

Dans une application, il est fréquent de créer différents types d’objets selon un contexte. Une première solution consiste à utiliser directement le mot-clé new suivi du nom de la classe, au sein d’une structure conditionnelle telle que if ou switch. Cette approche est simple à implémenter, mais elle crée progressivement un fort couplage entre le code métier et les classes concrètes utilisées.

Prenons l’exemple d’une application capable d’envoyer différents types de notification. Une notification peut être envoyée soit par e-mail, soit par SMS. Sans ce design pattern, nous pouvons directement instancier la classe appropriée à l’aide d’un switch.

public enum NotificationType
{
    Email = 1,
    Sms = 2
}

public class NotificationService
{
    public void Send(NotificationType type, string message)
    {
        var sender = type switch
        {
            NotificationType.Email => new EmailNotificationSender(),
            NotificationType.Sms => new SmsNotificationSender(),

            _ => throw new ArgumentOutOfRangeException(
                nameof(type),
                type,
                "Type de notification non pris en charge.")
        };

        sender.Send(message);
    }
}

Cette implémentation est simple, mais la classe NotificationService connaît directement toutes les implémentations de notification disponibles (EmailNotificationSender et SmsNotificationSender). Ainsi, chaque ajout d’un nouveau mode de notification impose de modifier cette classe.

Conception

Le diagramme de classes ci-dessous présente les différents éléments du design pattern Factory Method, ainsi que leurs relations :

Design Pattern Factory Method : conception

Dans ce diagramme, chaque classe et interface joue un rôle précis :

  • La classe NotificationService représente le Creator. Elle contient la méthode CreateSender(), qui constitue la Factory Method.

  • EmailNotificationService et SmsNotificationService sont les Concrete Creators qui redéfinissent CreateSender(), afin d'implémenter l'envoi de la notification appropriée.

  • INotificationSender représente le Product, c'est-à-dire le contrat commun utilisé par les Creators. Les classes EmailNotificationSender et SmsNotificationSender sont les Concrete Products.

Implémentation

L’interface INotificationSender représente un contrat que doivent implémenter les classes envoyant des notifications.

public interface INotificationSender
{
    void Send(string message);
}

Création des Concrete Products

La classe EmailNotificationSender permet d’envoyer une notification par e-mail.

public class EmailNotificationSender : INotificationSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Envoi d'un e-mail : {message}");
    }
}  

Et la classe SmsNotificationSender permet d’envoyer une notification par SMS.

public class SmsNotificationSender : INotificationSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Envoi d'un SMS : {message}");
    }
}

Implémentation de la classe abstraite

La classe abstraite NotificationService propose la méthode abstraite CreateSender(), représentant notre Factory Method, chargée de créer l’objet utilisé pour envoyer la notification.

public abstract class NotificationService
{
    protected abstract INotificationSender CreateSender();

    public void SendNotification(string message)
    {
        INotificationSender sender = CreateSender();

        sender.Send(message);
    }
}

Ainsi, la méthode SendNotification ne connaît pas la classe à utiliser pour envoyer la notification. Elle manipule uniquement un objet au travers de l’interface INotificationSender.

Implémentation des classes concrètes

La classe EmailNotificationService détermine l’objet à utiliser pour envoyer une notification par e-mail.

public class EmailNotificationService : NotificationService
{
    protected override INotificationSender CreateSender()
        => new EmailNotificationSender();
}

Et la classe SmsNotificationService crée quant à elle un objet de type SmsNotificationSender.

public class SmsNotificationService : NotificationService
{
    protected override INotificationSender CreateSender()
        => new SmsNotificationSender();
}

Chaque classe concrète redéfinit la Factory Method afin de fournir l’implémentation appropriée.

Utilisation

Le service peut ensuite être utilisé de la manière suivante :

NotificationService emailService = new EmailNotificationService();

emailService.SendNotification("Votre commande a été expédiée.");

NotificationService smsService = new SmsNotificationService();

smsService.SendNotification("Votre code de validation est 123456.");

Nous commençons par instancier notre Creator (NotificationService) en fonction du type de notification à envoyer. Puis nous appelons la méthode SendNotification pour envoyer la notification. Seule la création de l’objet exposé au travers de l'interface INotificationSender varie selon la classe concrète utilisée.

Ajout d'un nouveau type de notification

Supposons que nous devons prendre en charge un nouveau type de notification : les notifications Push. Il suffit alors d'ajouter un nouveau Concrete Product (nouvelle implémentation de INotificationSender), ainsi qu'un nouveau Concrete Creator (service dérivant de NotificationService) :

public class PushNotificationSender : INotificationSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Envoi d'une notification Push : {message}");
    }
}

public class PushNotificationService : NotificationService
{
    protected override INotificationSender CreateSender()
        => new PushNotificationSender();
}

Aucune modification n'a été nécessaire dans la classe NotificationService. Le traitement commun défini par cette classe est réutilisé tel quel, tandis que seule la création de l'objet est adaptée.

Le code est ainsi ouvert à l'extension et fermé à la modification, conformément au principe Open/Closed (OCP) des principes SOLID. Cette approche facilite la maintenabilité et l'évolutivité de l'application.

Utilisation des classes au sein d'une structure switch

Dans l'exemple précédent, le type de service était choisi directement lors de son instanciation. Dans une application, ce choix dépend souvent d'une valeur reçue dynamiquement, par exemple depuis une interface utilisateur, un paramètre de configuration, etc. Dans ce cas, il est tout à fait possible d'utiliser les classes du design pattern Factory Method, au sein d'une structure switch afin de sélectionner le type de notifications approprié :

public enum NotificationType
{
    Email = 1,
    Sms = 2,
    Push = 3
}

public class NotificationDispatcher
{
    public void Send(
        NotificationType notificationType,
        string message)
    {
        NotificationService notificationService =
            notificationType switch
            {
                NotificationType.Email
                    => new EmailNotificationService(),

                NotificationType.Sms
                    => new SmsNotificationService(),

                NotificationType.Push
                    => new PushNotificationService(),

                _ => throw new ArgumentOutOfRangeException(
                    nameof(notificationType),
                    notificationType,
                    "Type de notification non pris en charge.")
            };

        notificationService.SendNotification(message);
    }
}

Cet exemple montre que le design pattern Factory Method n'a pas vocation à remplacer les structures conditionnelles comme le switch. Son objectif est de limiter les couplages forts entre les classes.

Factory Method et injection de dépendances

Dans les applications utilisant l'injection de dépendances, le conteneur d'injections de dépendance (IoC) prend généralement en charge la création des objets. Ce design pattern reste néanmoins utile lorsqu'une classe de base définit un traitement commun, tout en laissant les classes dérivées déterminer l'implémentation concrète à utiliser.

Il est également utile lorsque la création d'un objet nécessite une logique spécifique qui ne peut pas être entièrement déléguée au conteneur d'injection de dépendances.

Avantages

Le design pattern Factory Method apporte plusieurs avantages :

  • Il réduit le couplage entre le code métier et les classes concrètes. Le traitement principal dépend d’une abstraction plutôt que des classes qu’il instancie.

  • Il centralise également la création des objets dans des méthodes ou des classes dédiées.

  • L’ajout d’un nouveau produit nécessite généralement la création d’une nouvelle implémentation et d’un nouveau créateur, sans modification du traitement commun.

  • Enfin, ce pattern facilite les tests unitaires, car les responsabilités de création et d’utilisation des objets sont mieux séparées.