Python Patrón Composite

El patrón Composite es un patrón de diseño estructural que te permite combinar objetos en estructuras de árbol para representar jerarquías de "parte-todo". El patrón Composite permite al cliente manejar objetos individuales y composiciones de objetos de manera uniforme, sin necesidad de preocuparse si está tratando con un objeto individual o con toda la estructura compuesta.

Analogía de la vida cotidiana

Imagina la estructura organizativa de un sistema de archivos:

  • Archivo individual: Es la unidad más básica
  • Carpeta: Puede contener múltiples archivos u otras carpetas
  • Ya sea que operes con un archivo o una carpeta, puedes realizar acciones como "abrir", "eliminar", "renombrar", etc.

Esta relación jerárquica de "parte-todo" es el escenario de aplicación típico del patrón Composite.


Componentes centrales del patrón Composite

1. Interfaz de componente (Component)

Este es el núcleo del patrón Composite; define la interfaz común para todos los objetos (incluyendo nodos hoja y nodos compuestos).

Ejemplo

from abc import ABC, abstractmethod
from typing import List

class FileSystemComponent(ABC):
    """Clase base abstracta del componente del sistema de archivos"""
   
    def __init__(self, name: str):
        self.name = name
        self.parent = None
   
    @abstractmethod
    def display(self, indent: int = 0) -> None:
        """Muestra la información del componente"""
        pass
   
    @abstractmethod
    def get_size(self) -> int:
        """Obtiene el tamaño del componente"""
        pass
   
    def get_path(self) -> str:
        """Obtiene la ruta completa"""
        if self.parent:
            return f"{self.parent.get_path()}/{self.name}"
        return self.name

2. Nodo hoja (Leaf)

Representa los objetos hoja en la composición; los nodos hoja no tienen hijos.

Ejemplo

class File(FileSystemComponent):
    """Clase de archivo - Nodo hoja"""
   
    def __init__(self, name: str, size: int):
        super().__init__(name)
        self._size = size
   
    def display(self, indent: int = 0) -> None:
        """Muestra la información del archivo"""
        spaces = "  " * indent
        print(f"{spaces}📄 {self.name} ({self._size} bytes)")
   
    def get_size(self) -> int:
        """Devuelve el tamaño del archivo"""
        return self._size

3. Nodo compuesto (Composite)

Representa objetos compuestos que pueden contener subcomponentes; define métodos para almacenar subcomponentes.

Ejemplo

class Directory(FileSystemComponent):
    """Clase de directorio - Nodo compuesto"""
   
    def __init__(self, name: str):
        super().__init__(name)
        self._children: List[FileSystemComponent] = []
   
    def add(self, component: FileSystemComponent) -> None:
        """Añadir subcomponente"""
        component.parent = self
        self._children.append(component)
   
    def remove(self, component: FileSystemComponent) -> None:
        """Eliminar subcomponente"""
        self._children.remove(component)
        component.parent = None
   
    def display(self, indent: int = 0) -> None:
        """Muestra el directorio y todos sus subcomponentes"""
        spaces = "  " * indent
        print(f"{spaces}📁 {self.name}/")
       
        # Mostrar recursivamente todos los subcomponentes
        for child in self._children:
            child.display(indent + 1)
   
    def get_size(self) -> int:
        """Calcula el tamaño total del directorio (incluyendo todos los subcomponentes)"""
        total_size = 0
        for child in self._children:
            total_size += child.get_size()
        return total_size
   
    def find_component(self, name: str) -> FileSystemComponent:
        """Busca un componente por nombre especificado"""
        for child in self._children:
            if child.name == name:
                return child
            if isinstance(child, Directory):
                found = child.find_component(name)
                if found:
                    return found
        return None

Ejemplo completo: Simulación de sistema de archivos

Veamos un ejemplo completo para demostrar la aplicación práctica del patrón Composite:

Ejemplo

def demonstrate_composite_pattern():
    """Demuestra el uso del patrón Composite"""
   
    # Crear directorio raíz
    root = Directory("root")
   
    # Crear subdirectorios
    documents = Directory("documents")
    pictures = Directory("pictures")
    music = Directory("music")
   
    # Crear archivos
    readme = File("README.txt", 1024)
    notes = File("notes.md", 2048)
    photo1 = File("vacation.jpg", 1536000)
    photo2 = File("family.png", 2048000)
    song1 = File("song1.mp3", 4096000)
    song2 = File("song2.mp3", 5120000)
   
    # Construir la estructura de directorios
    root.add(readme)
    root.add(documents)
    root.add(pictures)
    root.add(music)
   
    documents.add(notes)
   
    pictures.add(photo1)
    pictures.add(photo2)
   
    music.add(song1)
    music.add(song2)
   
    # Mostrar toda la estructura del sistema de archivos
    print("=== Estructura del sistema de archivos ===")
    root.display()
   
    print("\n"=== Estadísticas de tamaño ===")
    print(f"Tamaño total del directorio raíz: {root.get_size()} bytes")
    print(f"Tamaño del directorio de imágenes: {pictures.get_size()} bytes")
    print(f"Tamaño del directorio de música: {music.get_size()} bytes")
   
    print("\n"=== Información de ruta ===")
    print(f"Ruta del archivo: {photo1.get_path()}")
    print(f"Ruta del directorio: {pictures.get_path()}")
   
    print("\n"=== Buscar componente ===")
    found = root.find_component("song1.mp3")
    if found:
        print(f"Archivo encontrado: {found.get_path()}")

# Ejecutar demostración
if __name__ == "__main__":
    demonstrate_composite_pattern()

Al ejecutar el código anterior, verás la siguiente salida:


Diagrama de clases UML del patrón Composite


Ventajas y desventajas del patrón Composite

Ventajas

  1. Interfaz unificada: El cliente puede usar objetos individuales y objetos compuestos de manera consistente
  2. Principio de abierto/cerrado: Es fácil añadir nuevos tipos de componentes sin modificar el código existente
  3. Simplifica el código del cliente: El cliente no necesita determinar si está tratando con un objeto individual o una composición
  4. Estructura jerárquica flexible: Puede construir estructuras de árbol complejas

Desventajas

  1. Complejidad de diseño: Un diseño demasiado generalizado puede hacer que el sistema sea complejo
  2. Problemas de seguridad de tipos: En algunos casos, puede ser necesaria la verificación de tipos en tiempo de ejecución
  3. Consideraciones de rendimiento: Para estructuras jerárquicas grandes, las operaciones recursivas pueden afectar el rendimiento

Escenarios de aplicación práctica

1. Sistema de componentes GUI

Ejemplo

class UIComponent:
    """Clase base de componente UI"""
    def render(self):
        pass
    def add(self, component):
        pass

class Button(UIComponent):
    """Botón - Nodo hoja"""
    def render(self):
        print("Renderizar botón")

class Panel(UIComponent):
    """Panel - Nodo compuesto"""
    def __init__(self):
        self.children = []
   
    def add(self, component):
        self.children.append(component)
   
    def render(self):
        print("Comenzar a renderizar el panel")
        for child in self.children:
            child.render()
        print("Finalizar renderizado del panel")

2. Gestión de estructura organizativa

Ejemplo

class Employee:
    """Clase base de empleado"""
    def get_salary(self):
        pass

class Developer(Employee):
    """Desarrollador - Nodo hoja"""
    def __init__(self, salary):
        self.salary = salary
   
    def get_salary(self):
        return self.salary

class Department(Employee):
    """Departamento - Nodo compuesto"""
    def __init__(self):
        self.employees = []
   
    def add(self, employee):
        self.employees.append(employee)
   
    def get_salary(self):
        return sum(emp.get_salary() for emp in self.employees)

Ejercicios prácticos

Ejercicio 1: Extender el sistema de archivos

Añade las siguientes funcionalidades al sistema de archivos:

  1. Implementar la función de copia de archivos
  2. Añadir búsqueda con filtro por tipo de archivo
  3. Implementar visualización con límite de profundidad de directorio

Ejercicio 2: Diseño de sistema de menús

Diseña un sistema de menú de restaurante, con los siguientes requisitos:

  • El menú puede contener platos o submenús
  • Poder calcular el precio total
  • Soportar el filtrado de platos por categoría

Ejercicio 3: Organigrama

Crea un sistema de estructura organizativa de una empresa:

  • Los empleados pueden ser individuos o departamentos
  • Un departamento puede contener otros empleados o subdepartamentos
  • Poder calcular el número total de personas y el salario total del departamento

Resumen

El patrón Composite maneja objetos individuales y composiciones de objetos mediante una interfaz unificada, lo que hace que el código del cliente sea más conciso y flexible. Este patrón es especialmente adecuado para escenarios con estructuras jerárquicas, como sistemas de archivos, componentes GUI, estructuras organizativas, etc.

Puntos clave:

  • La interfaz de componente define las operaciones comunes de todos los objetos
  • Los nodos hoja implementan funcionalidades básicas
  • Los nodos compuestos gestionan subcomponentes y proporcionan operaciones agregadas
  • El cliente no necesita distinguir entre objetos individuales y composiciones de objetos
Otras extensiones