Patrón Flyweight en Python
El patrón Flyweight es un patrón de diseño estructural que minimiza el uso de memoria y mejora el rendimiento mediante el uso compartido de objetos. En pocas palabras, es la idea de "compartir metadatos".
Imagina que vas a la biblioteca a tomar prestado un libro: si cada persona comprara su propio ejemplar, la biblioteca tendría que almacenar miles de copias del mismo libro. Pero en realidad, la biblioteca solo necesita almacenar un ejemplar de "Introducción a Python", y todos los que quieran leerlo pueden tomar prestado ese mismo ejemplar. El patrón Flyweight es como esa "biblioteca": gestiona los objetos que se pueden compartir.
Idea central
El patrón Flyweight divide el estado de un objeto en dos tipos:
- Estado interno (Intrinsic State): la parte inmutable y compartible
- Estado externo (Extrinsic State): la parte variable y no compartible
Al compartir el estado interno, se evita crear muchos objetos similares, ahorrando así recursos del sistema.
¿Por qué se necesita el patrón Flyweight?
Escenario del problema
Supongamos que estamos desarrollando un procesador de texto y necesitamos renderizar los caracteres de un documento. Si creamos un objeto independiente para cada carácter:
Ejemplos
class Character:
def __init__(self, char, font, size, color):
self.char = char # Carácter
self.font = font # Fuente
self.size = size # Tamaño de fuente
self.color = color # Color
def render(self, position):
print(f"Renderizando el carácter '{self.char}' en la posición {position}")
# Ejemplo de uso
char_a = Character('A', 'SimSun', 12, 'negro')
char_b = Character('B', 'SimSun', 12, 'negro')
char_a_another = Character('A', 'SimSun', 12, 'negro') # Se crea repetidamente el mismo 'A'
Problemas de esta implementación:
- Desperdicio de memoria: los mismos caracteres se crean repetidamente
- Bajo rendimiento: gran costo de creación y destrucción de objetos
- Difícil de mantener: la cantidad de objetos crece explosivamente
Solución
Usando el patrón Flyweight, podemos:
- Compartir los mismos objetos de caracteres
- Almacenar solo una copia del estado interno (el carácter en sí)
- El estado externo (posición) se pasa al renderizar
Implementación del patrón Flyweight
Estructura básica
Implementemos el patrón Flyweight con el ejemplo del procesador de texto:
Ejemplos
# Clase Flyweight: almacena el estado interno
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # Estado interno: contenido del carácter
self.font = font # Estado interno: fuente
self.size = size # Estado interno: tamaño de fuente
self.color = color # Estado interno: color
def render(self, position: tuple):
"""Renderiza el carácter; position es el estado externo"""
x, y = position
print(f"Renderizando en ({x}, {y}): carácter '{self.char}' "
f"[fuente:{self.font}, tamaño:{self.size}, color:{self.color}]")
# Fábrica Flyweight: gestiona los objetos compartidos
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# Clave única que identifica el objeto a crear
key = f"{char}_{font}_{size}_{color}"
# Si el objeto no existe, se crea y se almacena en caché
if key not in cls._characters:
cls._characters[key] = CharacterFlyweight(char, font, size, color)
print(f"Creando nuevo objeto de carácter: {key}")
else:
print(f"Reutilizando objeto de carácter existente: {key}")
return cls._characters[key]
# Clase cliente: usa objetos Flyweight
class TextDocument:
def __init__(self):
self.characters = [] # Almacena el carácter y la información de posición
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# Obtiene el objeto Flyweight de la fábrica
character = CharacterFactory.get_character(char, font, size, color)
# Almacena el objeto de carácter y el estado externo (posición)
self.characters.append((character, position))
def render(self):
print("\n"=== Comenzando a renderizar el documento ===")
for character, position in self.characters:
character.render(position)
print("=== Renderizado del documento completado ==="\n")
Ejemplo de uso
Ejemplos
document = TextDocument()
# Añadir caracteres al documento
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)) # Reutilizar 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # Color diferente, crear nuevo objeto
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # Reutilizar 'H'
# Renderizar documento
document.render()
# Ver el número de objetos en la fábrica
print(f"En total se crearon {len(CharacterFactory._characters)} objetos de carácter en la fábrica")
Resultado de salida:
创建新字符对象: 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 个字符对象
Componentes principales del patrón Flyweight
1. Flyweight (interfaz o clase abstracta flyweight)
Define la interfaz de los objetos Flyweight, que generalmente incluye métodos para operar con el estado externo.
2. ConcreteFlyweight (clase concreta de Flyweight)
Implementa la interfaz Flyweight y almacena el estado interno. El estado interno debe ser inmutable.
3. FlyweightFactory (fábrica de Flyweight)
Crea y gestiona los objetos Flyweight, asegurando que se compartan correctamente.
4. Client(cliente)
Mantiene el estado externo y solicita objetos Flyweight a la fábrica cuando es necesario.
Ejemplo más complejo: aplicación en el desarrollo de juegos
Veamos un ejemplo real en el desarrollo de juegos: un sistema de renderizado de árboles.
Ejemplos
from dataclasses import dataclass
@dataclass
class TreeType:
"""Clase Flyweight: tipo de árbol (estado interno)"""
name: str # Nombre de la especie de árbol
texture: str # Archivo de textura
color: str # Color base
def render(self, x: int, y: int, height: int):
"""Renderiza el árbol; la posición y la altura son estado externo"""
print(f"Renderizando árbol {self.name} en ({x}, {y}), altura {height} metros "
f"[textura:{self.texture}, color:{self.color}]")
class TreeFactory:
"""Fábrica Flyweight: gestiona los tipos de árbol"""
_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"Creando nuevo tipo de árbol: {name}")
return cls._tree_types[key]
@classmethod
def list_tree_types(cls):
print(f"\n"Actualmente hay {len(cls._tree_types)} tipos de árbol:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""Objeto árbol: contiene la referencia Flyweight y el estado externo"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # Estado externo: coordenada X
self.y = y # Estado externo: coordenada Y
self.height = height # Estado externo: altura
self.tree_type = tree_type # Referencia al objeto Flyweight
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""Bosque: clase 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"=== Comenzando a renderizar el bosque ===")
for tree in self.trees:
tree.render()
print("=== Renderizado del bosque completado ===")
# Ejemplo de uso
forest = Forest()
# Plantar árboles: los árboles del mismo tipo comparten el objeto Flyweight
forest.plant_tree(10, 20, 15, "Pino", "pine_texture.png", "Verde oscuro")
forest.plant_tree(30, 40, 12, "Pino", "pine_texture.png", "Verde oscuro") # Se reutiliza el tipo Pino
forest.plant_tree(50, 60, 18, "Roble", "oak_texture.png", "Verde claro")
forest.plant_tree(70, 80, 20, "Pino", "pine_texture.png", "Verde oscuro") # Se reutiliza de nuevo
forest.plant_tree(90, 100, 16, "Arce", "maple_texture.png", "Rojo")
# Renderizar bosque
forest.render()
# Ver estadísticas de tipos de árbol
TreeFactory.list_tree_types()
Ventajas y desventajas del patrón Flyweight
Ventajas
- Reducción significativa del uso de memoria: al compartir objetos similares, se reduce notablemente la ocupación de memoria
- Mejora del rendimiento: reduce la sobrecarga de creación de objetos y recolección de basura
- Reutilización de código: la lógica de los objetos idénticos solo necesita implementarse una vez
- Fácil de ampliar: añadir nuevos tipos Flyweight no afecta al código existente
Desventajas
- Mayor complejidad: hay que distinguir entre estado interno y estado externo
- Problemas de concurrencia: los objetos compartidos requieren un tratamiento adicional en entornos multiproceso
- Posible introducción de errores: si se modifica por error el estado interno, afectará a todos los usuarios
- No aplicable a todos los escenarios: solo es efectivo cuando los objetos son realmente compartibles
Escenarios de aplicación
Escenarios adecuados para usar el patrón Flyweight
- Gran cantidad de objetos similares: cuando el sistema necesita crear una gran cantidad de objetos similares
- Aplicaciones sensibles a la memoria: entornos con memoria limitada como dispositivos móviles, sistemas embebidos, etc.
- Sistemas de caché: cuando se necesita almacenar en caché y reutilizar objetos
- Desarrollo de juegos: renderizar una gran cantidad de objetos de juego del mismo tipo
- Procesamiento de documentos: procesadores de texto, hojas de cálculo, etc.
Escenarios en los que no es apropiado
- Los objetos difieren mucho entre sí: si cada objeto tiene un estado único
- Estado externo complejo: si la gestión del estado externo es más compleja que la creación de objetos
- Requisitos de rendimiento bajos: en escenarios con memoria suficiente y requisitos de rendimiento bajos
Práctica
Ejercicio 1: Mejorar el sistema de renderizado de caracteres
Intenta mejorar nuestro sistema de renderizado de caracteres anterior, añadiendo soporte para estilos como negrita, cursiva, etc.:
Ejemplos
class AdvancedCharacterFlyweight:
# Añade soporte para negrita, cursiva y subrayado
pass
# Prueba tu implementación
def test_advanced_system():
# Crea documentos con diferentes estilos
pass
Ejercicio 2: Implementar un sistema de gestión de iconos
Diseña un sistema de gestión de iconos donde el mismo icono comparta el mismo objeto cuando se muestra en diferentes posiciones:
Ejemplos
# Almacena estado interno como la ruta del archivo del icono, dimensiones, etc.
pass
class IconFactory:
# Gestiona los objetos Flyweight de iconos
pass
class Application:
# Muestra iconos en diferentes posiciones de la interfaz
pass
Resumen
El patrón Flyweight es una poderosa técnica de optimización que reduce el consumo de recursos mediante la compartición de objetos. Puntos clave:
- Distinguir estados: distinguir claramente entre estado interno (compartible) y estado externo (no compartible)
- Usar una fábrica: gestionar la creación y compartición de objetos Flyweight mediante una clase de fábrica
- Sopesar pros y contras: encontrar un equilibrio entre el ahorro de memoria y la complejidad del código
- Escenarios aplicables: principalmente para escenarios con una gran cantidad de objetos similares