Aller au contenu

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 un très grand nombre d’objets contenant une quantité importante de données identiques.

Problématique

Nous devons développer une application qui affiche 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 caractérisent leur type. Chaque arbre conserve séparément des références vers ces mêmes informations. Si notre forêt contient plusieurs centaines de milliers de chênes, ces références seront répétées dans un très grand nombre d’objets. Dans une application réelle, certaines données volumineuses pourraient également être chargées ou créées plusieurs fois. Le pattern Flyweight permet de rendre leur partage explicite et de centraliser leur création.

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 :

Design Pattern Flyweight : conception problématique 1

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 sa position et une référence vers un objet de type TreeType. Plusieurs instances de la classe Tree peuvent donc référencer (et partager) 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 Name, string Color, string TextureId), TreeType>
        _treeTypes = [];

    public TreeType GetTreeType(
        string name,
        string color,
        string textureId,
        byte[] texture)
    {
        var key = (name, color, textureId);

        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", "oak", 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", "oak", oakTexture);
var pineType = factory.GetTreeType("Pin", "Vert foncé", "pine", 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. Chaque arbre possède une référence vers l’une de ces instances. Tous les arbres d’un même type partagent ainsi les mêmes informations intrinsèques. Celles-ci devraient idéalement être immuables, car toute modification serait visible par l’ensemble des arbres qui les utilisent. Bien que la propriété Texture soit en lecture seule, le contenu du tableau qu’elle référence reste modifiable. Dans une implémentation réelle, il conviendrait de protéger également ce contenu afin de rendre l’état intrinsèque immuable.

Avantages

Le principal avantage du design pattern Flyweight est de réduire la consommation de mémoire en évitant la duplication d’informations identiques dans un grand nombre d’objets. Les données intrinsèques, qui peuvent être volumineuses, sont regroupées dans des objets partagés. Chaque objet conserve uniquement son état extrinsèque et une référence vers le flyweight correspondant.

La fabrique centralise la création des flyweights et favorise la réutilisation des instances existantes. Toutefois, le gain obtenu dépend du nombre d’objets manipulés et de la quantité de données pouvant être partagées.