Design Patterns - Composite

Propósito

El patrón Composite es un patrón de diseño estructural que permite componer objetos en estructuras de árbol para representar jerarquías de parte-todo.

El objetivo es permite que los clientes traten de manera uniforme a los objetos individuales y a los complejos.

Es especialmente útil para modelar y trabajar con estructuras complejas.

Problema

Construir objetos complejos compuestos de partes más simples, y cómo tratar de manera uniforme tanto a las partes individuales como al conjunto.

Solución

La solución que propone el Composite es:

  • Divide los objetos en dos categorías: hojas (objetos individuales sin sub-objetos) y compuestos (objetos que tienen sub-objetos).
  • Interfaz Común: Proporciona una interfaz común para tratar tanto los objetos hoja como los compuestos, lo que simplifica el trabajo con estructuras de árbol.
singleton

Estructura

singleton

Participantes

  • Component: Interfaz común para objetos individuales y sus composiciones.
  • Leaf (Hoja): Representa objetos individuales en la composición (sin hijos).
  • Composite (Compuesto): Define el comportamiento para componentes que tienen hijos.
  • Client: Interactúa con los objetos a través de la interfaz Component.

Cuándo Usarlo

Este patrón es recomendable cuando queremos:

  • representar jerarquías de objetos parte-todo.
  • que los clientes no tengan que diferenciar al objeto individual del compuesto, que pueda tratar a ambos de la misma manera, facilitando el uso.

Ventajas

Simplicidad para los Clientes: Facilita a los clientes el trabajo con estructuras complejas al tratar los objetos simples y compuestos de igual manera.

Flexibilidad en la Estructura: Permite construir estructuras complejas y jerárquicas de objetos.

Fácil Agregado de Nuevos Tipos de Componentes: Permite agregar nuevos tipos de componentes sin cambiar el código existente.

Desventajas

Dificultad para Restringir Tipos de Componentes: Puede ser difícil restringir los tipos de componentes que se pueden agregar a objetos compuestos.

Rendimiento en Estructuras Muy Grandes: El rendimiento puede ser un problema si la estructura del árbol es muy grande y profunda.

Ejemplo: Sistema de gestión de archivos

Debemos desarrollar un módulo de gestión de archivos para un sistema que debe organizar y manipular una gran cantidad de documentos y carpetas.

El sistema debe permitir a los usuarios realizar diversas operaciones como ver, agregar o eliminar archivos y carpetas.

Problema

El desafío principal esta tratar archivos y carpetas de manera uniforme. Los archivos son elementos individuales, mientras que las carpetas pueden contener múltiples archivos y otras carpetas.

Esta estructura jerárquica de parte-todo complica la interacción del usuario con el sistema, ya que las operaciones sobre archivos y carpetas podrían requerir implementaciones muy diferentes. Implementar cada operación para cada tipo de elemento (archivo o carpeta) termina en un código repetitivo y difícil de mantener, sobre todo cuando se agregan nuevas operaciones o tipos de elementos.

Solución planteada

Vamos a usar el patrón Composite que nos permite tratar tanto archivos individuales (elementos ‘Leaf’) como carpetas (elementos ‘Composite’) de manera uniforme.

Al usar una interfaz común para ambos, simplificamos la lógica del cliente en la interacción con archivos y carpetas. Así, las carpetas pueden contener y gestionar varios elementos (ya sean archivos u otras carpetas) de la misma manera que un archivo individual.

Definimos una interfaz FileSystemComponent, que define operaciones comunes, como printName, add y remove.

La clase File representa archivos individuales y maneja operaciones básicas. No permite agregar ni eliminación componentes, ya que es un elemento Hoja.

La clase Directory representa una carpeta, que puede contener tanto archivos como otras carpetas. Implementa métodos para agregar y eliminar componentes.

En el cliente creamos instancias de File y Directory, y las organizamos en una estructura de árbol. Luego invocamos a printName en el directorio principal para imprimir los nombres de todos los archivos y subdirectorios.

singleton

Código Java

Codificamos en Java lo que preparamos en el diagrama.

Definimos la Interfaz de FileSystemComponent (Component):


    interface FileSystemComponent {
        void printName();
        void add(FileSystemComponent component) throws Exception;
        void remove(FileSystemComponent component) throws Exception;
    }
                

Implementamos las clase File (Hoja):


    class File implements FileSystemComponent {
        private String name;
    
        public File(String name) {
            this.name = name;
        }
    
        public void printName() {
            System.out.println("Archivo: " + name);
        }
    
        public void add(FileSystemComponent component) {
            throw new UnsupportedOperationException();
        }
    
        public void remove(FileSystemComponent component) {
            throw new UnsupportedOperationException();
        }
    }
                

Implementamos las clase Directory (Composite):


    class Directory implements FileSystemComponent {
        private String name;
        private List children = new ArrayList<>();
    
        public Directory(String name) {
            this.name = name;
        }
    
        public void printName() {
            System.out.println("Carpeta: " + name);
            for (FileSystemComponent component : children) {
                component.printName();
            }
        }
    
        public void add(FileSystemComponent component) {
            children.add(component);
        }
    
        public void remove(FileSystemComponent component) {
            children.remove(component);
        }
    }
                

El código cliente:


    public class Client {
        public static void main(String[] args) {
            FileSystemComponent file1 = new File("file1.txt");
            FileSystemComponent file2 = new File("file2.txt");
            FileSystemComponent dir1 = new Directory("dir1");
    
            try {
                dir1.add(file1);
                dir1.add(file2);
    
                FileSystemComponent file3 = new File("file3.txt");
                FileSystemComponent mainDir = new Directory("main");
                mainDir.add(dir1);
                mainDir.add(file3);
    
                mainDir.printName();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
                

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Component, Leaf, Composite, Client:

  • FileSystemComponent(Component): Interfaz que define operaciones comunes para archivos y carpetas
  • File(Leaf): Representa archivos individuales. Implementa las operaciones de la interfaz Component.
  • Directory(Composite): Representa carpetas. Implementa las operaciones de la interfaz Component y mantiene una colección de elementos, que pueden ser tanto archivos como otras carpetas.
  • Client (Client): Interactúa con archivos y carpetas a través de la interfaz común Component, lo que facilita operaciones como listar todos los elementos de una carpeta o agregar nuevos elementos, sin necesidad de conocer si está tratando con un archivo o con una carpeta.

Conclusiones

Al usar el patrón Composite, nuestro sistema de gestión de archivos se vuelve más sencillo y escalable. Permite fácilmente agregar nuevas operaciones o tipos de elementos sin alterar la lógica existente. Esto no solo mejora la mantenibilidad del código, sino que también ofrece una experiencia de usuario más coherente y eficiente al tratar con la estructura jerárquica de archivos y carpetas.

Patrones relacionados

  • Decorator

A menudo se usa con Composite para añadir responsabilidades a los componentes individuales.

  • Flyweight

Puede ser combinado con Composite para compartir componentes.