Factory Method : le cordon bleu de la programmation objets.

Cet article fait partie d’une série sur les patrons de conception. Vous pouvez retrouver tous les autres ici (avec une introduction tout en bas).


Fabrique (Factory Method) : patron de conception permettant de prendre en charge et d’uniformiser l’instanciation de tous les objets d’une interface.


Problème

Imaginez que vous dirigez une usine de cookies. Ceux-ci sont cuisinés par une machine, puis passent sur un tapis roulant où ils sont cuits, garnis, emballés, etc. Un job de rêve, car vous n’avez rien à faire (et vous pouvez en chiper un en cas de petit creux).

C’est le même principe que les fonctions (=usines) en programmation orientée objets : des objets (=cookies) sont instanciés au début avec leur constructeur (=machine), puis on leur applique un tas de traitements (=tapis roulant).

Vous vivez votre meilleure vie, quand soudain, votre boss vous appelle pour vous informer qu’il souhaite se diversifier dans le marché du croissant.

L’idée vous met l’eau à la bouche, mais il y a un problème : toute votre usine est optimisée pour faire QUE des cookies, et pas autre chose.

Donc pour faire des croissants, il faudra construire une nouvelle usine à part entière, même si 90% du processus de production est identique.

C’est parce que votre classe dépend beaucoup trop d’une autre, et vous ne pouvez rien changer sans tout casser ou copier-coller. On dit que les deux sont couplées.

J’ai une solution, on peut utiliser le DIP pour faire une usine capable de fabriquer n’importe quelle pâtisserie.

C’est une excellente idée. Le principe d’inversion des dépendances (DIP) dit qu’une classe ne doit pas dépendre d’une autre, mais que les deux doivent dépendre d’une couche d’abstraction.

En l’occurrence, bien que toutes les pâtisseries soient différentes, les traitements individuels sont tous similaires : cuisson, garniture…

On peut donc placer nos croissants et nos cookies sous la même interface. Ça permet de n’avoir qu’un seul tapis roulant pour tous les types de pâtisserie !

Et si je vous disais que ce n’est pas suffisant ?

Car bien que le couplage ait disparu pour les traitements, il est toujours bel et bien présent au niveau de l’instanciation des objets, car même s’ils se cachent derrière une interface, vous devez toujours appeler leur constructeur.

Typiquement, ça donne lieu à un énorme switch au début de chaque fonction pour décider du type de l’objet à instancier.

Ça n’a l’air de rien, mais si jamais vous ajoutez un nouveau type de pâtisseries, vous allez devoir le prendre en charge sur TOUTES vos fonctions, ce qui n’est autre qu’une odieuse violation du principe ouvert fermé (OCP).

Non seulement vous allez passer un temps fou à tout changer, mais en plus, vous risquez de casser votre code en faisant la modification de trop, et vous retrouver avec des chocolatines au lieu d’avoir des pains au chocolat. L’horreur absolue !


Solution

Pour résoudre ce problème, on va externaliser la création des objets dans une fabrique.

Il s’agit d’un ensemble de classes qui servent uniquement à instancier des produits d’une interface (les pâtisseries : Cookie, Croissant…) pour les distribuer aux clients (les usines).

Ainsi, le client est comme un livreur Amazon : on lui donne un colis, il ne sait pas ce qu’il y a dedans et il s’en fout, car tout ce qu’il a à faire, c’est le livrer à la bonne adresse.

Une stratégie plutôt simple, mais qui apporte d’énormes bénéfices, car :

  • On élimine TOTALEMENT le couplage entre le client et les produits : le type réel de l’objet se cache derrière une interface (DIP) et son constructeur se cache derrière la fabrique.
  • Si l’on ajoute un nouveau produit, ou que leur manière d’être instancié change, on a juste à modifier la fabrique sans toucher aux clients (SRP : principe de responsabilité unique).
  • Le code dédié à l’instanciation des objets est encapsulé hors du client. Donc même s’il fait 2000 lignes de code, on s’en fout, car le client n’y voit que du feu !
  • La manière d’instancier le produit est partagée par tous les clients, ce qui limite la duplication de code (DRY).
  • Si un client a besoin d’une nouvelle manière d’instancier ses produits, il suffit d’ajouter une nouvelle fabrique sans modifier quoi que ce soit (OCP).

Facile, n’est-ce pas ? Voyons un peu comment ça se présente.


Explication

Inversion des dépendances

Avant de penser aux fabriques, il faut d’abord commencer par appliquer le DIP, sinon, ça n’a absolument aucun intérêt. Tout est expliqué dans mon article sur le sujet.

N’oubliez pas que les clients n’utiliseront QUE les membres de cette interface, donc assurez-vous qu’elle fasse tout ce dont vous avez besoin.


Création d’une fabrique

La première étape est de créer une fabrique de base. Il s’agit d’une classe abstraite contenant une méthode abstraite nommée Create(). Le type de retour doit obligatoirement être le type de l’interface regroupant tous les produits.

Ensuite, on va créer des fabriques concrètes. Il s’agit de classes héritant de la fabrique de base qui vont implémenter la méthode Create().

Et voilà, nos fabriques sont sur pied. Pour récupérer un produit dans le client, il suffit :

  1. D’instancier la bonne fabrique concrète.
  2. D’appeler la méthode Create().
  3. De récupérer le résultat dans un objet.

Et le tour est joué.

Attends, c’est quoi tout ce bazar, pourquoi ne pas juste avoir fait quelques fonctions d’instanciation dans un coin ?

On aurait pu, mais franchement, ça reviendrait à mettre la poussière sous le tapis.

Cette structure a en réalité beaucoup d’avantages :

  • Évolution : si jamais un client demande une nouvelle manière d’instancier un produit (par exemple, en demandant tout sauf des cookies), il suffit de créer une nouvelle fabrique concrète sans modifier quoi que ce soit.
  • Uniformisation : on s’assure que toutes les fabriques ont la même signature (Pastry Create()), peu importe leur implémentation. C’est une sorte de contrat.
  • Encapsulation : on peut mettre autant de code que l’on veut dans les classes sans polluer le reste.
  • DRY : on peut ajouter du code dans la fabrique de base qui sera partagé par toutes les fabriques concrètes, notamment grâce à des patrons de méthodes.

Passage de paramètres

D’accord, on déplace les constructeurs des produits dans une autre méthode, j’ai pigé. Mais du coup, je fais comment pour lui passer mes paramètres maintenant ?

Rien de plus facile, il suffit d’utiliser le constructeur de la fabrique :

  1. Définissez le constructeur de la fabrique concrète avec les paramètres que vous souhaitez passer à la méthode Create().
  2. Dans le constructeur, sauvegardez ces paramètres dans des champs privés.
  3. Utilisez ses champs privés dans la méthode Create().

Application

On va commencer par créer les différents produits et les placer sous une interface commune (DIP).

public interface Pastry 
{
    void Bake(int temperatureInCelcius, int timeInSeconds);
}

public class Cookie : Pastry
{
    public Cookie() { }
    public void Bake(int temperatureInCelcius, int timeInSeconds) { }
}

public class Croissant : Pastry
{
    public Croissant() { }
    public void Bake(int temperatureInCelcius, int timeInSeconds) { }
}

public class Doughnut : Pastry
{
    public Doughnut() { }
    public void Bake(int temperatureInCelcius, int timeInSeconds) { }
}

Ensuite, on va créer une fabrique de base qui contiendra deux méthodes :

  • La méthode abstraite Create(), bien entendu.
  • Une méthode GetBakedPastry(int, int) qui retournera la même chose que Create(), mais avec un petit traitement en plus.

On pourrait en mettre d’autres, tout ce qui compte, c’est que toutes les méthodes publiques retournent une instance de Pastry, car une fabrique ne sert qu’à ça.

Si elle possède des méthodes publiques qui font autre chose, ça reviendrait à enfreindre le SRP.

public abstract class PastryFactory
{
    public abstract Pastry Create();

    public Pastry GetBakedPastry(int temperatureInCelcius, int timeInSeconds)
    {
        Pastry result = Create();
        result.Bake(temperatureInCelcius, timeInSeconds);
        return result;
    }
}
Certaines personnes argumentent que la méthode Create() devrait être protégée, et utilisable uniquement au travers d’une autre méthode. Même si je comprends l’idée, je préfère la laisser publique pour cet exemple.

On va ensuite créer deux fabriques concrètes qui vont implémenter Create() de deux manières différentes :

  • La fabrique RandomPastryFactory qui instancie un produit au hasard.
  • La fabrique FromStringPastryFactory qui instancie un produit en fonction d’une chaine de caractères.
public class PastryFromStringPastryFactory : PastryFactory
{
    private string PastryName { get; set; } 

    public PastryFromStringPastryFactory(string pastryName)
    {
        PastryName = pastryName;
    }

    public override Pastry Create()
    {
        return PastryName.ToUpper() switch
        {
            "CROISSANT" => new Croissant(),
            "COOKIE" => new Cookie(),
            "DOUGHNUT" => new Doughnut(),
            "DONUT" => new Doughnut(),
            _ => throw new InvalidOperationException()
        };
    }
}

public class RandomPastryFactory : PastryFactory
{
    public override Pastry Create()
    {
        int randomNumber = new Random().Next(1, 4);

        return randomNumber switch
        {
            1 => new Croissant(),
            2 => new Cookie(),
            _ => new Doughnut()
        };
    }
}

C’est bon pour les fabriques. À présent, il ne reste plus qu’à les utiliser dans le client. Voici quelques exemples :

static void Main(string[] args)
{
    PastryFactory randomPastryFactory = new RandomPastryFactory();
    Pastry randomPastry = randomPastryFactory.Create();
    Pastry randomBakedPastry = randomPastryFactory.GetBakedPastry(180, 300);

    PastryFactory fromStringPastryFactory = new FromStringPastryFactory("cookie");
    Pastry pastryFromString = fromStringPastryFactory.Create();
    
    //Si on utilise la fabrique qu'une seule fois, on peut utiliser cette syntaxe
    Pastry anotherPastryFromString = new FromStringPastryFactory("donut").Create();

    Console.WriteLine(pastryFromString.GetType().Name); // Cookie
    Console.WriteLine(anotherPastryFromString.GetType().Name); // Doughnut
    Console.WriteLine(randomPastry.GetType().Name); // Impossible à prédire :/
}
Vous remarquerez que toutes les fabriques sont du type de la fabrique de base. C’est une bonne pratique pour limiter les ennuis si un jour vous avez besoin de changer son type.

Et voilà le travail ! Si vous voulez voir ce que ça donne, vous pouvez visionner et télécharger le code source ici.


Conditions d’utilisation

Les conditions d’utilisation des fabriques sont sensiblement les mêmes que celles du DIP. Il faut les utiliser quand :

  • On ne veut pas qu’un objet dépende trop fortement d’un autre, au risque de tout casser le jour où celui-ci change.
  • On ne connait pas le type d’un objet à l’avance.
  • Un même objet jongle entre plusieurs types.

Si le type d’un objet doit être connu, ou qu’il ne risque jamais de changer, les fabriques n’ont aucun intérêt, de même que le DIP.


Soutenez le blog en partageant cet article autour de vous (merci 💙) et suivez-moi sur Twitter @ITExpertFr pour m’entendre déblatérer des inepties choses très intéressantes.

Et pour les amateurs d’architecture logicielle et de patrons de conception, voici une paire d’articles à vous mettre sous la dent :

Quant à moi, je vous laisse, je vais aller faire coucou à mon pote Perseverance sur Mars.

À bientôt pour un nouvel article sur le blog des développeurs ultra-efficaces. Tchao !