Patron Poids-mouche en Python

Le patron Flyweight est un patron de conception structurel qui minimise l'utilisation de la mémoire et améliore les performances en partageant des objets. En termes simples, c'est l'idée de « partager des métadonnées ».

Imaginez que vous empruntiez des livres à une bibliothèque : si chaque livre était acheté séparément par différentes personnes, la bibliothèque devrait stocker des milliers de livres identiques. Mais en réalité, la bibliothèque n'a besoin de stocker qu'un seul exemplaire de « Introduction à Python », et tous ceux qui souhaitent le lire peuvent emprunter le même exemplaire. Le patron Flyweight est une telle « bibliothèque » qui gère des objets partageables.

Idée centrale

Le patron de conception Poids-Mouche divise l'état des objets en deux types :

  • État intrinsèque: la partie immuable et partageable
  • État extrinsèque: la partie variable et non partageable

En partageant l'état intrinsèque, cela évite de créer un grand nombre d'objets similaires, économisant ainsi les ressources système.


Pourquoi avons-nous besoin du patron de conception Poids-Mouche

Scénario problématique

Supposons que nous développions un traitement de texte et que nous devions afficher des caractères dans un document. Si nous créons un objet distinct pour chaque caractère :

Exemple

# Mauvaise implémentation : chaque caractère est un objet indépendant
class Character:
    def __init__(self, char, font, size, color):
        self.char = char      # caractère
        self.font = font      # police
        self.size = size      # taille de police
        self.color = color    # couleur
   
    def render(self, position):
        print(f"Afficher le caractère '{self.char}' à la position {position}")

# exemple d'utilisation
char_a = Character('A', 'SimSun', 12, 'black')
char_b = Character('B', 'SimSun', 12, 'black')
char_a_another = Character('A', 'SimSun', 12, 'black')  # création répétée du même 'A'

Problèmes avec cette implémentation :

  • Gaspillage de mémoire : les mêmes caractères sont créés à plusieurs reprises
  • Mauvaises performances : surcoût lié à la création et à la destruction d'un grand nombre d'objets
  • Difficile à maintenir : le nombre d'objets explose

Solution

En utilisant le motif Flyweight, nous pouvons :

  • Partager les mêmes objets caractère
  • Stocker une seule copie de l'état intrinsèque (le caractère lui-même)
  • Passer l'état extrinsèque (position) lors du rendu

Implémentation du motif Flyweight

Structure de base

Implémentons le motif Flyweight en utilisant l'exemple du traitement de texte :

Exemple

from typing import Dict

# Classe Flyweight - stocke l'état intrinsèque
class CharacterFlyweight:
    def __init__(self, char: str, font: str, size: int, color: str):
        self.char = char      # État intrinsèque : contenu du caractère
        self.font = font      # État intrinsèque : police
        self.size = size      # État intrinsèque : taille de police
        self.color = color    # État intrinsèque : couleur
   
    def render(self, position: tuple):
        """Afficher le caractère ; la position est l'état extrinsèque"""
        x, y = position
        print(f"Afficher à ({x}, {y}) : caractère '{self.char}' "
              f"[police:{self.font}, taille:{self.size}, couleur:{self.color}]")

# Fabrique de poids mouches - gère les objets partagés
class CharacterFactory:
    _characters: Dict[str, CharacterFlyweight] = {}
   
    @classmethod
    def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
        # Clé d'identification unique pour créer des objets
        key = f"{char}_{font}_{size}_{color}"
       
        # Si l'objet n'existe pas, le créer et le mettre en cache
        if key not in cls._characters:
            cls._characters[key] = CharacterFlyweight(char, font, size, color)
            print(f"Création d'un nouvel objet caractère : {key}")
        else:
            print(f"Réutilisation de l'objet caractère existant : {key}")
       
        return cls._characters[key]

# Classe client - utilise des objets flyweight
class TextDocument:
    def __init__(self):
        self.characters = []  # Stocke les informations sur le caractère et la position
   
    def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
        # Obtient l'objet poids-mouche depuis la fabrique
        character = CharacterFactory.get_character(char, font, size, color)
        # Stocke l'objet caractère et l'état extrinsèque (position)
        self.characters.append((character, position))
   
    def render(self):
        print("\n"=== Début du rendu du document ===")
        for character, position in self.characters:
            character.render(position)
        print("=== Rendu du document terminé ==="\n")

Exemple d'utilisation

Exemple

# Créer le document
document = TextDocument()

# Ajouter un caractère au document
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0))  # Réutiliser 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0))    # Couleur différente, créer un nouvel objet
document.add_character('H', 'Arial', 12, 'black', (0, 1))  # Réutiliser 'H'

# Rendre le document
document.render()

# Voir le nombre d'objets dans la fabrique
print(f"La fabrique a créé {len(CharacterFactory._characters)} objets caractère au total")

Résultat de sortie :

创建新字符对象: H_Arial_12_black
创建新字符对象: e_Arial_12_black
创建新字符对象: l_Arial_12_black
重用现有字符对象: l_Arial_12_black
创建新字符对象: o_Arial_12_black
创建新字符对象: !_Arial_12_red
重用现有字符对象: H_Arial_12_black

=== 开始渲染文档 ===
在位置(0, 0)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black]
在位置(1, 0)渲染: 字符'e' [字体:Arial, 大小:12, 颜色:black]
在位置(2, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black]
在位置(3, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black]
在位置(4, 0)渲染: 字符'o' [字体:Arial, 大小:12, 颜色:black]
在位置(5, 0)渲染: 字符'!' [字体:Arial, 大小:12, 颜色:red]
在位置(0, 1)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black]
=== 文档渲染完成 ===

工厂中总共创建了 6 个字符对象

Composants principaux du patron Poids-mouche

1. Poids-mouche (interface Poids-mouche ou classe abstraite)

Définit l'interface pour les objets poids-mouche, contenant généralement des méthodes pour opérer sur l'état extrinsèque.

2. ConcreteFlyweight (classe de poids-mouche concrète)

Implémente l'interface de poids-mouche et stocke l'état intrinsèque. L'état intrinsèque doit être immuable.

3. FlyweightFactory (fabrique de poids-mouche)

Crée et gère les objets poids-mouche, en garantissant que les objets poids-mouche sont partagés correctement.

4. Client

Maintient l'état extrinsèque et demande des objets poids-mouche à la fabrique de poids-mouche lorsque nécessaire.


Un exemple plus complexe : application au développement de jeux

Regardons un exemple pratique du développement de jeux - un système de rendu d'arbres :

Exemple

from typing import Dict, List
from dataclasses import dataclass

@dataclass
class TreeType:
    """Classe de poids-mouche - type d'arbre (état intrinsèque)"""
    name: str          # Nom de l'espèce d'arbre
    texture: str       # Fichier de texture
    color: str         # Couleur de base
   
    def render(self, x: int, y: int, height: int):
        """Dessine l'arbre ; la position et la hauteur sont un état extrinsèque"""
        print(f"Affiche l'arbre {self.name} à ({x}, {y}), hauteur {height} mètres "
              f"[texture:{self.texture}, couleur:{self.color}]")

class TreeFactory:
    """Fabrique de poids-mouches - gère les types d'arbres"""
    _tree_types: Dict[str, TreeType] = {}
   
    @classmethod
    def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
        key = f"{name}_{texture}_{color}"
        if key not in cls._tree_types:
            cls._tree_types[key] = TreeType(name, texture, color)
            print(f"Création d'un nouveau type d'arbre : {name}")
        return cls._tree_types[key]
   
    @classmethod
    def list_tree_types(cls):
        print(f"\n"Il y a actuellement {len(cls._tree_types)} types d'arbres :")
        for tree_type in cls._tree_types.values():
            print(f" - {tree_type.name}")

class Tree:
    """Objet Tree - contient une référence de poids-mouche et un état extrinsèque"""
    def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
        self.x = x              # État extrinsèque : coordonnée X
        self.y = y              # État extrinsèque : coordonnée Y
        self.height = height    # État extrinsèque : height
        self.tree_type = tree_type  # Référence à l'objet poids-mouche
   
    def render(self):
        self.tree_type.render(self.x, self.y, self.height)

class Forest:
    """Forest - classe cliente"""
    def __init__(self):
        self.trees: List[Tree] = []
   
    def plant_tree(self, x: int, y: int, height: int,
                   name: str, texture: str, color: str):
        tree_type = TreeFactory.get_tree_type(name, texture, color)
        tree = Tree(x, y, height, tree_type)
        self.trees.append(tree)
   
    def render(self):
        print("\n"=== Début du rendu de la forêt ===")
        for tree in self.trees:
            tree.render()
        print("=== Rendu de la forêt terminé ===")

# Exemple d'utilisation
forest = Forest()

# Planter des arbres - les arbres du même type partagent des objets poids-mouche
forest.plant_tree(10, 20, 15, "Pin", "pine_texture.png", "Vert foncé")
forest.plant_tree(30, 40, 12, "Pin", "pine_texture.png", "Vert foncé")  # Réutiliser le type de pin
forest.plant_tree(50, 60, 18, "Chêne", "oak_texture.png", "Vert clair")
forest.plant_tree(70, 80, 20, "Pin", "pine_texture.png", "Vert foncé")  # Réutiliser à nouveau
forest.plant_tree(90, 100, 16, "Maple", "maple_texture.png", "Red")

# Rendu de la forêt
forest.render()

# Afficher les statistiques des types d'arbres
TreeFactory.list_tree_types()

Avantages et inconvénients du patron Flyweight

Avantages

  1. Réduire considérablement l'utilisation de la mémoire: réduire considérablement la consommation de mémoire en partageant des objets similaires
  2. Améliorer les performances: réduire la surcharge de création d'objets et de collecte des déchets
  3. Réutilisation du code: la même logique d'objet ne doit être implémentée qu'une seule fois
  4. Facile à étendre: l'ajout de nouveaux types de flyweight n'affecte pas le code existant

Inconvénients

  1. Augmente la complexité: il faut distinguer entre l'état intrinsèque et extrinsèque
  2. Problèmes de sécurité des threads: les objets partagés exigent une gestion supplémentaire dans les environnements multithreadés
  3. Peut introduire des bogues: si l'état intrinsèque est modifié par erreur, cela affectera tous les utilisateurs
  4. Non applicable à tous les scénarios: Efficace uniquement lorsque l'objet est réellement partageable

Scénarios applicables

Scénarios adaptés au motif Poids-Mouche

  1. Grand nombre d'objets similaires: Lorsque le système doit créer un grand nombre d'objets similaires
  2. Applications sensibles à la mémoire: Environnements à mémoire limitée tels que les appareils mobiles et les systèmes embarqués
  3. Systèmes de cache: Lorsque les objets doivent être mis en cache et réutilisés
  4. Développement de jeux: Rendu d'un grand nombre d'objets de jeu du même type
  5. Traitement de documents: Traitements de texte, tableurs, etc.

Scénarios inadaptés à l'utilisation

  1. Objets très différents: Si chaque objet possède un état unique
  2. État externe complexe: Si la gestion de l'état externe est plus complexe que la création d'objets
  3. Faibles exigences de performance: Dans les scénarios avec suffisamment de mémoire et de faibles exigences de performance

Exercices pratiques

Exercice 1 : Améliorer le système de rendu des caractères

Essayez d'améliorer notre précédent système de rendu des caractères en ajoutant la prise en charge de styles tels que le gras et l'italique :

Exemple

# Votre code amélioré va ici
class AdvancedCharacterFlyweight:
    # Ajouter la prise en charge du gras, de l'italique et du soulignement
    pass

# Testez votre implémentation
def test_advanced_system():
    # Créez un document contenant différents styles
    pass

Exercice 2 : Implémenter un système de gestion d'icônes

Concevez un système de gestion d'icônes où la même icône partage le même objet lorsqu'elle est affichée à différentes positions :

Exemple

class IconFlyweight:
    # Stocker l'état interne tel que le chemin du fichier d'icône et les dimensions
    pass

class IconFactory:
    # Gérer les objets poids-mouche d'icône
    pass

class Application:
    # Afficher les icônes à différentes positions sur l'interface
    pass

Résumé

Le patron Poids-mouche est une puissante technique d'optimisation qui réduit la consommation de ressources en partageant des objets. Points clés :

  1. Distinguer les états: Distinguer clairement l'état interne (partageable) de l'état externe (non partageable)
  2. Utiliser une fabrique: Gérer la création et le partage des objets poids-mouche via une classe de fabrique
  3. Peser le pour et le contre: Trouver un équilibre entre les économies de mémoire et la complexité du code
  4. Scénarios d'application: Principalement utilisé dans les scénarios comportant un grand nombre d'objets similaires
Autres extensions