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
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
# 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
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 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
- Réduire considérablement l'utilisation de la mémoire: réduire considérablement la consommation de mémoire en partageant des objets similaires
- Améliorer les performances: réduire la surcharge de création d'objets et de collecte des déchets
- Réutilisation du code: la même logique d'objet ne doit être implémentée qu'une seule fois
- Facile à étendre: l'ajout de nouveaux types de flyweight n'affecte pas le code existant
Inconvénients
- Augmente la complexité: il faut distinguer entre l'état intrinsèque et extrinsèque
- Problèmes de sécurité des threads: les objets partagés exigent une gestion supplémentaire dans les environnements multithreadés
- Peut introduire des bogues: si l'état intrinsèque est modifié par erreur, cela affectera tous les utilisateurs
- 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
- Grand nombre d'objets similaires: Lorsque le système doit créer un grand nombre d'objets similaires
- Applications sensibles à la mémoire: Environnements à mémoire limitée tels que les appareils mobiles et les systèmes embarqués
- Systèmes de cache: Lorsque les objets doivent être mis en cache et réutilisés
- Développement de jeux: Rendu d'un grand nombre d'objets de jeu du même type
- Traitement de documents: Traitements de texte, tableurs, etc.
Scénarios inadaptés à l'utilisation
- Objets très différents: Si chaque objet possède un état unique
- État externe complexe: Si la gestion de l'état externe est plus complexe que la création d'objets
- 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
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
# 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 :
- Distinguer les états: Distinguer clairement l'état interne (partageable) de l'état externe (non partageable)
- Utiliser une fabrique: Gérer la création et le partage des objets poids-mouche via une classe de fabrique
- Peser le pour et le contre: Trouver un équilibre entre les économies de mémoire et la complexité du code
- Scénarios d'application: Principalement utilisé dans les scénarios comportant un grand nombre d'objets similaires