Flyweight
Design Pattern Flyweight
Réduire l'empreinte mémoire en partageant les objets
Introduction
Le design pattern Flyweight est un pattern de conception structurel dont l’objectif est de réduire la consommation de la mémoire, lorsqu’un grand nombre d’objets similaires doivent être manipulés. Plutôt que de stocker les mêmes informations dans chaque instance, le design pattern Flyweight consiste à identifier les données communes à plusieurs objets et à les partager entre eux. Les informations d’un objet sont alors séparées en deux états distincts :
-
L’état intrinsèque, commun à plusieurs objets et pouvant être partagé.
-
L’état extrinsèque, propre au contexte d’utilisation de chaque objet.
Le design pattern Flyweight est particulièrement utile lorsqu’une application manipule des milliers d’objets ou davantage, contenant une quantité importante de données identiques.
Problématique
Nous devons développer une application qui doit afficher une forêt contenant plusieurs milliers d’arbres. Chacun de ces arbres possède des coordonnées (une abscisse (X) et une ordonnée (Y)), un type d’arbre, un nom, une couleur et une texture. Soit la classe Tree permettant de créer chacun de ces arbres :
public class Tree
{
public int X { get; }
public int Y { get; }
public string Name { get; }
public string Color { get; }
public byte[] Texture { get; }
public Tree(int x, int y, string name, string color, byte[] texture)
{
X = x;
Y = y;
Name = name;
Color = color;
Texture = texture;
}
}
Nous pouvons instancier cette classe pour créer différents arbres :
var tree1 = new Tree(10,20, "Chêne", "Vert", oakTexture);
var tree2 = new Tree(30, 40, "Chêne", "Vert", oakTexture);
var tree3 = new Tree(50, 60, "Chêne", "Vert", oakTexture);
Les positions (X et Y) sont propres à chaque arbre. En revanche, le nom, la couleur et la texture sont identiques pour tous les arbres appartenant au même type. Si notre forêt contient plusieurs centaines de milliers de chênes, ces informations seront donc inutilement dupliquées dans un très grand nombre d’objets. Le coût devient particulièrement important lorsque certaines données, comme une texture ou une représentation graphique, occupent beaucoup de mémoire.
Principe du design pattern Flyweight
Le design pattern Flyweight propose de sortir les données communes des objets et de les regrouper dans un objet qui sera partagé entre tous ces objets. Dans notre exemple, nous pouvons distinguer deux catégories de données.
-
L’état intrinsèque qui est indépendant du contexte d’utilisation de l’objet. Pour un arbre, il peut s’agir des propriétés Name, Color et Texture. Ces informations peuvent être partagées entre tous les arbres du même type.
-
L'état extrinsèque qui est propre à chaque objet. Pour un arbre, il est constitué des coordonnées (propriétés X et Y), car chaque arbre possède sa propre position dans la forêt.
Nous pouvons donc représenter la séparation suivante :
La classe TreeType est un Flyweight.
Implémentation
Et voici l'implémentation des classes :
public class TreeType
{
public string Name { get; }
public string Color { get; }
public byte[] Texture { get; }
public TreeType(string name, string color, byte[] texture)
{
Name = name;
Color = color;
Texture = texture;
}
}
public class Tree
{
public int X { get; }
public int Y { get; }
public TreeType Type { get; }
public Tree(int x, int y, TreeType type)
{
X = x;
Y = y;
Type = type;
}
public void Draw()
{
Console.WriteLine($"Affichage d'un {Type.Name} en ({X}, {Y})");
}
}
Chaque arbre stocke désormais uniquement que sa position et un TreeType. Plusieurs instances de la classe Tree peuvent donc référencer exactement la même instance de TreeType.
Implémentation d'une fabrique de Flyweights
Pour que le pattern soit davantage efficace, il faut également éviter de recréer plusieurs objets de type TreeType représentant exactement le même type d’arbre. Une fabrique peut être utilisée pour centraliser leur création et faciliter leur réutilisation :
public class TreeTypeFactory
{
private readonly Dictionary<string, TreeType> _treeTypes = [];
public TreeType GetTreeType(string name, string color, byte[] texture)
{
string key = $"{name}_{color}";
if (!_treeTypes.TryGetValue(key, out TreeType? treeType))
{
treeType = new TreeType(name, color, texture);
_treeTypes.Add(key, treeType);
}
return treeType;
}
}
Lorsqu’un type d’arbre est demandé, la fabrique vérifie s’il a déjà été créé. Si c'est le cas, l’instance précédemment créée est retournée. Dans le cas contraire, une nouvelle instance est créée puis ajoutée à la collection _treeTypes. Ainsi, la fabrique garantit que les objets représentant les mêmes informations intrinsèques peuvent être partagés.
Utilisation
Nous pouvons maintenant créer notre forêt composée d'arbres :
var factory = new TreeTypeFactory();
var oakType = factory.GetTreeType("Chêne", "Vert", oakTexture);
var tree1 = new Tree(10, 20, oakType);
var tree2 = new Tree(30, 40, oakType);
var tree3 = new Tree(50, 60, oakType);
Les trois arbres sont bien des objets différents. Ils possèdent notamment chacun leurs propres coordonnées. En revanche, ils utilisent tous la même instance oakType, car ces arbres sont du même type.
Exemple avec plusieurs types d'arbres
La fabrique devient particulièrement intéressante lorsque plusieurs types d'objets coexistent :
var oakType = factory.GetTreeType("Chêne", "Vert", oakTexture);
var pineType = factory.GetTreeType("Pin", "Vert foncé", pineTexture);
var trees = new List<Tree>
{
new(10, 20, oakType),
new(30, 40, oakType),
new(50, 60, pineType),
new(70, 80, oakType),
new(90, 100, pineType)
};
Nous créons cinq arbres, mais seulement deux instances de la classe TreeType (oakType et pineType). Les différents arbres ont une référence vers l'un de ces instances. Ainsi, toute modification d'une information sur un type d'arbre s'applique sur tous les arbres de ce type.
Avantages
Le principal avantage du design pattern Flyweight est la réduction de la consommation mémoire en évitant la duplication d’informations identiques et partageant des objets potentiellement volumineux, grâce à la création centralisée des objets partagés avec à la fabrique.
