Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Factory Method ou Abstract Factory ?

Dans cette quête de patterns pour un code totalement découplé on rencontre souvent deux stratégies, la Factory Method et l’Abstract Factory, savoir quand utiliser l’un ou l’autre de ces patrons de conception réclame de bien en cerner les intentions…

Les Factories

Les “fabriques” au sens d’usine. Les deux patterns dont état aujourd’hui fabriquent donc quelque chose, des instances. Elles se trouvent ainsi classées dans la taxonomie des patterns dans les “creational patterns”, c’est à dire les “patrons de conception créationnels”.

Il existe une nuance entre ces deux types de “fabrique”. D’un point de vue théorique :

Le motif Fabrique (Factory Method)

Le problème traité est celui de l’instanciation de classes différentes selon le besoin. L’intention est de régler ce problème par l’utilisation d’une interface et de méthodes retournant les instances concrètes sans que l’appelant n’ait besoin d’en connaitre la véritable nature.

Le schéma UML ci-dessous résume le fonctionnement de ce pattern :

image

 

La Fabrique Abstraite (Abstract Factory)

Le problème traité est sensiblement le même sauf qu’ici il ne s’agit plus d’une seule instance à créer mais de nombreuses instances reliées entre elles. Le schéma UML qui suit montre le principe de ce pattern :

image

Tout de suite cela se complique un peu puisque bien entendu il existe plusieurs classes concrètes à instancier dans une même action.

Mais nous allons voir par des exemples à quoi cela peut ressembler de façon plus parlante.

Factory Method

Ce pattern peut se décliner de différentes façon en C#.

La façon la plus découplée consiste à introduire la notion de Factory sous la forme d’une classe statique ou d’un service ou d’un singleton. Une méthode permettant de retourner une instance concrète sous la forme d’une interface prédéfinie.

La version la plus “molle”, la plus “relâchée” de la Factory Method peut s’implémenter tout simplement sous la forme d’un constructeur nommé. Regardons le code ci-dessous (tous mes exemples sont faits sous LinqPad pour vous faciliter l’expérimentation sans charger VS) :

public class Animal
{
	public string Owner { get; set; }
	public string Name { get; set; }
	public Animal(string owner, string name)
	{
		Owner = owner;
		Name = name;
	}

	public Animal()
	{	}
}

La classe “Animal” possède un nom (Name) et un propriétaire (Owner). Elle expose un constructeur permettant de passer ces deux informations. Mais il existe aussi un constructeur vide. L’utiliser consiste à créer un animal anonyme et orphelin (sans propriétaire).

La première façon de clarifier le code pourrait être dans cette version très “relâchée” de la Factory Method de rendre les intentions lisibles :

public class Animal
{
	public string Owner { get; set; }
	public string Name { get; set; }
	public Animal(string owner, string name)
	{
		Owner = owner;
		Name = name;
	}

	public static Animal Orphan()
	{ return new Animal(); }

	private Animal()
	{	}
}

 

Vous noterez la subtile modification : le constructeur vide est maintenant privé, il ne peut plus être invoqué depuis l’extérieur de la classe. En revanche une méthode statique publique fait son apparition “Orphan”. Son nom précise l’intention, créer un “orphelin” sans maitre (ni nom). Le constructeur “normal” subsiste et il permet de créer une instance pourvue d’un nom et d’un propriétaire.

Critique de l’approche “relâchée”

Dans cette version ultra light de la Factory Method il n’existe pas d’interface et la méthode de fabrique se trouve directement dans la classe concernée, “Animal”.

Si cette façon de faire permet de matérialiser les intentions, ce qui est toujours un excellent choix, cela est loin de régler tous les problèmes adressés par le pattern.

Créer plus de constructeurs n’est pas en soi une voie intéressante car on doit toujours viser la généralisation plus que la spécification. C’est un premier problème de cette approche simpliste.

En outre les appelants auront besoin de connaitre la classe “Animal” et seront donc fortement couplés à celle-ci… Tout le contraire de ce qu’on cherche à obtenir, un code découplé.

De plus le code simplifié ne permettra pas simplement de substituer demain un animal différent sans avoir à modifier tout le code de l’application. On peut imaginer une entreprise gérant des articles avec une classe “Article”. Puis dans le futur il peut s’avérer essentiel de séparer les produits frais des produits secs, la date de conservation étant calculée de façon différente. Avec l’approche simplifiée tout est à revoir… Avec introduction de nouveaux bogues certainement.

La bonne façon de procéder

Pour bien procéder il est préférable de créer une interface et de ne traiter les objets qu’au travers de celle-ci. Pour l’application les instances concrètes n’existent plus, elles sont “dématérialisées” par l’interface.

Continuons l’exemple de l’entreprise qui doit gérer des produits frais et des produits secs ayant des caractéristiques différentes mais possédant un noyau commun. Tous ces articles peuvent être manipulés par une interface “IArticle”, et toute l’application ne verra que cela. Car pour obtenir une désignation, un stock, voire modifier ce dernier, il n’y aucune différence, sauf éventuellement au niveau de chaque classe mais cela est transparent pour l’application et doit le rester. On ajoutera pour rendre cela palpable une date de péremption calculée par chaque classe d’article en fonction de sa nature.

Cela va donner un code plus complexe forcément (car plus complet et utilisant plus de classes) :

void Main()
{
	var produits = new List<IArticle>();
	produits.Add(ProduitFactory.ProduitFrais("Lait frais"));
	produits.Add(ProduitFactory.ProduitSec("Haricot sec"));
	produits.Add(ProduitFactory.ProduitSansType("Lot de miel chinois"));

	var i = 1;
	foreach (var p in produits)	{ p.StockOpération(10*i++); }

	foreach (var p in produits)
	{ Console.WriteLine(p.ToString()); }
	
	
}

public interface IArticle
{
	string Désignation { get; set; }
	DateTime Péremption { get; }
	string Filière { get; }
	int Stock { get;}
	void StockOpération(int value);
}

public static class ProduitFactory
{
	public static IArticle ProduitSansType(string désignation)
	{ 
		return new Produit { Désignation = désignation}; 
	}

	public static IArticle ProduitFrais(string désignation)
	{
		return new ProduitFrais { Désignation = désignation };
	}

	public static IArticle ProduitSec(string désignation)
	{
		return new ProduitSec { Désignation = désignation };
	}
}

public class Produit : IArticle
{
	internal Produit() 
	{  	créationLe = DateTime.Now; 	}
	
	protected DateTime créationLe { get; private set;}
	protected int stock;
	public string Désignation { get; set; }
	public virtual string Filière { get { return "SANS TYPE"; } }
	public virtual DateTime Péremption { get { return DateTime.MaxValue; } }
	public int Stock { get {return stock; } }

	public void StockOpération(int value)
	{
		stock += value;
	}

	public override string ToString()
	{
		return $"{Désignation} {Filière} {Péremption} {Stock}";
	}

	
}

public class ProduitFrais : Produit
{
	public override DateTime Péremption
	{
		get { return créationLe.AddDays(8); }
	}

	public override string Filière
	{
		get { return "FRAIS"; }
	}
}

public class ProduitSec : Produit
{
	public override DateTime Péremption
	{
		get { return créationLe.AddYears(1); }
	}

	public override string Filière
	{
		get { return "SEC"; }
	}
}

 

La sortie de ce code sera la suivante :

Lait frais FRAIS 22/03/2016 16:46:10 10
Haricot sec SEC 14/03/2017 16:46:10 20
Lot de miel chinois SANS TYPE 31/12/9999 23:59:59 30

La désignation est suivie du type ainsi que de la date de péremption calculée automatiquement par chaque classe en fonction de sa nature. En revanche toutes ces variantes de Produit sont manipulables de façon unique via “IArticle”. Qu’il s’agisse d’obtenir des informations (comme celles inscrites ici) comme d’effectuer des opérations (celles manipulant le stock dans le code principal de Main).

 

La structure générale est simple : une interface, “IArticle” supportée par une classe de base Produit donnant naissance à une hiérarchie, ici deux classes ProduitFrais et ProduitSec.

Le jeu d’héritage ne nous intéresse pas vraiment ici sauf qu’il transmet le support de “IArticle” à tous les produits.

Si on regarde le code de “Main”, celui de “l’application”, on remarque :

  • Que les produits différents sont créés de façon claire via une méthode portant un nom non ambigu
  • Qu’il existe un point central pour créer tous les types d’article (ProduitFactory)
  • Que les articles sont manipulés ensuite de façon totalement transparente quel que soit leur type et ce via uniquement l’interface IArticle

La première chose qu’on pourrait se demander c’est si la classe ProduitFactory est indispensable. Il est vrai que les méthodes statiques qu’elle contient pourraient très bien être placées dans la classe Produit. Mais en faisant ainsi le code serait couplé à la classe Produit. Si demain on veut substituer toute une nouvelle hiérarchie de classes qui remplacera Produit et ses enfants le code de l’application sera impacté. En créant une indirection via ProduitFactory on s’assure que seul ce point unique dans l’application devra être modifié. Car bien entendu la nouvelle hiérarchie réutilisera l’interface “IArticle” ce qui sera donc transparent pour tout le code existant. Ces nouveaux produits pourront offrir une nouvelle interface “IArticle2” qui ajoutera des comportements. C’est la bonne façon au passage de gérer les évolutions des interfaces. Une interface est IMMUABLE une fois créée. De fait “l’ancien” code marchera toujours, via “IArticle”, et le nouveau code aussi qui lui utilisera “IArticle2”. Mais cette nouvelle interface n’est pas même nécessaire, peut-être que seule la hiérarchie de classe sera entièrement revue sans pour autant ajouter de nouveaux comportements.

Bref on l’a compris, utiliser ProduitFactory permet de se protéger de toutes les évolutions de ce type même si dans l’instant elle n’est pas absolument indispensable.

La seconde chose essentielle à noter est bien sûr l’utilisation du pattern Factory Method, c’est à dire que pour créer des instances de Produits on utilise uniquement des méthodes spécialisées dont les noms sont sans ambigüité.

Abstract Factory

Le motif de Fabrique Abstraite concerne comme je l’ai expliqué en début d’article des grappes de classes et d’instances reliées entre elles.

La Factory Method ne concerne qu’une classe et ses variantes, toutes étant manipulables via la même interface.

Mais supposons que les instances à créer dépendent d’un choix et qu’il faille puiser dans différentes hiérarchies de classe pour créer l’objet principal ?

C’est là que l’Abstract Factory entre en jeu.

Les exemples se compliquent très nettement puisque le nombre de classes en jeu augmente pour démontrer l’intérêt du principe… Celui donné par Wikipédia est finalement assez court et donne une bonne vision de la chose même si ce n’est qu’une possibilité parmi d’autres. Ici le pattern est suivi via des classes abstraites plutôt que des interfaces, c’est un choix. De même l’exemple est réducteur puisqu’un seul objet est finalement visé (un bouton d’IHM dans différents OS) mais on comprend bien que cela est extensible à l’infini. Cet exemple imparfait – comme tout exemple d’ailleurs – à aussi l’avantage de faire comprendre comment peut fonctionner une librairie de code cross-plateforme un peu comme Xamarin.Forms… Une pierre deux coups c’est toujours bon à prendre (sauf dans la figure !).

Voici ce code légèrement adapté par mes soins (toujours sous LinqPad) :

void Main()
{
	GUIFactory aFactory = GUIFactory.getFactory(OsType.Windows);
	Button aButton = aFactory.createButton();
	aButton.caption = "Play";
	aButton.paint();

	aFactory = GUIFactory.getFactory(OsType.OsX);
	aButton = aFactory.createButton();
	aButton.caption = "Play";
	aButton.paint();
}

public enum OsType { Windows, OsX }

abstract class GUIFactory
{

	public static GUIFactory getFactory(OsType osType)
	{
		if (osType == 0) return (new WinFactory());
		else return (new OSXFactory());
	}
	public abstract Button createButton();
}

class WinFactory : GUIFactory
{
	public override Button createButton()
	{
		return (new WinButton());
	}
}

class OSXFactory : GUIFactory
{
	public override Button createButton()
	{
		return (new OSXButton());
	}
}

abstract class Button
{
	public string caption;
	public abstract void paint();
}

class WinButton : Button
{
	public override void paint()
	{
		Console.WriteLine("I'm a WinButton: " + caption);
	}
}

class OSXButton : Button
{
	public override void paint()
	{
		Console.WriteLine("I'm a OSXButton: " + caption);
	}
}

Et la sortie sera …

I'm a WinButton: Play
I'm a OSXButton: Play

Le principe mis ici en lumière est le suivant :

En fonction d’un choix, ici celui de l’OS cible, une Factory “générale” via une méthode (getFactory) retourne une Factory spécialisée. Et c’est cette dernière qui permet de créer enfin de créer une instance de “Button”, un élément d’IHM utilisé ensuite par l’application.

De fait l’application n’a aucun contact avec l’implémentation du bouton, ni même du TextBlock ou autres éléments qu’une Factory réelle supporterait, comme les Xamarin.Forms donc.

Si le choix dépend d’un paramètre lu dans un fichier de configuration (celui de l’OS cible) l’application sera totalement ignorante de l’OS utilisé ! Un simple changement dans le fichier de configuration et c’est tout le reste qui suit …

Conclusion

Abstract Factory et Factory Method sont deux puissants moyens de structurer le code dédié à la création d’instances quel qu’en soit la complexité et ce en séparant clairement les responsabilité et surtout en isolant totalement l’application des implémentations concrètes, base d’un code fortement découplé.

Il y a une sorte de gymnastique de l’esprit dont il faut prendre le rythme mais cela n’est pas que de la stylistique, le découplage fort est un absolu pour du code maintenable et évolutif !

Trouver des exemples est toujours délicat car ces mécanismes réclament de nombreuses classes et un peu de “mise en scène”. Le résultat est souvent très réducteur et on a parfois du mal à voir comment cela peut s’appliquer à son propre code. La réponse est simple : il n’existe pas d’exemples parfaits et seule la pratique permet de véritablement comprendre. Il n’y a pas de “vérité révélée”, coder c’est agir et c’est dans l’action que la compréhension vient, non dans une révélation mystique…

Bon code et…

Stay Tuned !

Faites des heureux, PARTAGEZ l'article !