El patrón Composite: ejemplos de soluciones para jerarquías parte-todo

Las estructuras de datos dinámicas requieren una escructura jerárquica clara y concisa. A menudo, llevar tales estructuras a la práctica no resulta nada fácil. No es nada eficiente, por ejemplo, consultar cada vez el tipo de un objeto antes de que los datos se hayan procesado. En particular, cuando muchos objetos simples se encuentran con objetos compuestos, se recomienda usar el patrón de diseño Composite. Al ocultarles sus diferencias, este paradigma de desarrollo de software facilita a los clientes actuar del mismo modo ante los objetos simples y compuestos.

¿Qué es el patrón Composite?

El patrón de diseño Composite (Composite Pattern) es uno de los 23 patrones de diseño “GoF” para el desarrollo de software que fueron publicados en 1994 por Erich Gamma, Richard Helm, Ralph Johnson, y John Vlissides, los llamados “Gang of Four” o la banda de los cuatro. Así como el patrón Facade y el patrón Decorator, se trata de un patrón de diseño que agrupa objetos complejos y clases en estructuras mayores.

El concepto básico del patrón Composite consiste en representar objetos simples y sus containers (o contenedores, también llamados colecciones en algunos lenguajes, o sea: grupos de objetos) en una clase abstracta de manera que puedan ser tratados uniformemente. Este tipo de estructura se conoce como jerarquía parte-todo (en inglés: part-whole hierarchy), en la que un objeto es siempre, o una parte de un todo, o un todo compuesto por varias partes.

¿Qué problemas resuelve un diagrama UML de patrón Composite?

La finalidad del patrón Composite es, como en todos los patrones “GoF”, mejorar la gestión de problemas de diseño recurrentes en la programación orientada a objetos. El resultado deseado es un software flexible, caracterizado por objetos fácilmente implementables, intercambiables, reutilizables y testeables. A tal efecto, el patrón de diseño Composite describe un método según el cual los objetos simples y complejos pueden ser tratados de la misma manera. De este modo, se puede crear una estructura de objetos fácilmente inteligible que permite al cliente un acceso altamente eficiente. Además, también se minimiza la probabilidad de error en el código.

Patrón de diseño Composite: representación gráfica (UML)

Para implementar jerarquías parte-todo, el patrón Composite proporciona una interfaz de componentes unificada para los objetos simples, también llamados objetos leaf (hoja, en inglés), y los objetos complejos. Los objetos leaf simples integran esta interfaz directamente, mientras que los objetos complejos envían peticiones específicas del cliente automáticamente a la interfaz y a sus componentes subordinados. Para el cliente, resulta totalmente indiferente de qué tipo de objeto se trate (parte o todo) ya que solo se comunica con la interfaz.

El siguiente diagrama de clases en formato UML muestra más claramente las conexiones y jerarquías de un software basado en patrón Composite.

Pros y contras del patrón de diseño Composite

El patrón Composite es una constante en el desarrollo de software. Los proyectos con estructuras altamente anidadas tienden a beneficiarse de la práctica metodología de los objetos: ya sean objetos simples o complejos, con dependencias simples o complejas. El alcance del anidamiento es irrelevante en el patrón Composite. Las diferencias entre los tipos de objetos pueden ser ignoradas por el cliente de manera que las consultas no requieren funciones especiales, lo que conlleva la ventaja de que el código del cliente resulta simple y conciso.

Otra ventaja del patrón Composite es la flexibilidad y sencilla escalabilidad que el patrón otorga al software. La interfaz con componentes universales permite la integración de objetos simples y complejos sin modificaciones en el código –ya sea en el cliente o en estructuras de objetos ya existentes.

Pero, a pesar de todas la ventajas, el patrón Composite y su interfaz unificada tienen algunos inconvenientes. La interfaz puede convertirse en un dolor de cabeza para los desarrolladores, ya que la implementación requiere algunas consideraciones nada desdeñables. Por ejemplo, se debe decidir de antemano qué operaciones van a ser definidas en la interfaz y cuáles en las clases Composite. Ajustes posteriores de las propiedades Composite (por ejemplo, restringir qué tipo de elementos child se permiten) resultan generalmente complicados y difíciles de implementar.

Ventajas Desventajas
Facilita la representación de estructuras altamente anidadas Implementación de interfaz de componentes complicada
Código simple y conciso Ajustes posteriores de las propiedades Composite complicados
Gran escalabilidad  

Campos de aplicación del patrón Composite

Utilizar el patrón Composite compensa cuando existe la necesidad de realizar operaciones en estructuras de datos dinámicas con jerarquías complejas (en términos de profundidad y anchura). Esto también se conoce como escructura de árbol binaria, que resulta especialmente interesante para una amplia variedad de programas. Algunos ejemplos típicos:

Sistemas de datos: los sistemas de datos figuran entre los componentes más importantes de un software de dispositivo. Estos se pueden representar perfectamente usando el patrón Composite: los ficheros simples se representan como objetos leaf y las carpetas (que pueden contener asimismo ficheros u otras carpetas) como objetos complejos.

Menús de software: los menús de programa representan una aplicación típica de una estructura de árbol binaria de acuerdo al patrón de diseño Composite. La barra de menú contiene una o más entradas principales (objetos complejos) como, por ejemplo, “Archivo”. Estas entradas principales proporcionan acceso a varios objetos del menú que pueden seleccionarse directamente (objetos leaf) o que contienen otros submenús (objetos complejos).

Interfaces gráficas de usuario (GUI): las estructuras de árbol y el patrón Composite también juegan un papel importante en el diseño de interfaces gráficas de usuario. Aparte de simples objetos leaf como botones, campos de texto o casillas de selección, los containers, como paneles o marcos, permiten una estructura limpia y una mayor claridad.

Ejemplo de código: patrón Composite

El patrón Composite está más ampliamente afianzado en el lenguaje de programación Java. El patrón, entre otras cosas, es la base del AWT (Abstract Window Toolkit). El AWT es una práctica y extendida API con unas 50 clases de Java listas para utilizar para el desarrollo de interfaces Java multiplataforma. En el siguiente código de ejemplo, extraído del artículo Patrón de diseño Composite en Java en baeldung.com, nos centramos en este popular lenguaje de programación.

En nuestro ejemplo, se muestra una estructura jerárquica de departamentos en una empresa. Como primer paso, se define la interfaz del objeto Department:

public interface Department {
	void printDepartmentName();
}

A continuación, se definen dos simples clases leaf: FinancialDepartment (para el departamento fiscal) y SalesDepartment (para el departamento de ventas). Ambas implementan el método printDepartmentName() de la interfaz del objeto, pero no contienen otros objetos Department.

public class FinancialDepartment implements Department {
	private Integer id;
	private String name;
	public void printDepartmentName() {
		System.out.println(getClass().getSimpleName());
	}
	// standard constructor, getters, setters
}
public class SalesDepartment implements Department {
	private Integer id;
	private String name;
	public void printDepartmentName() {
		System.out.println(getClass().getSimpleName());
	}
	// standard constructor, getters, setters
}

Una clase composite que encaja con la jerarquía se define como “HeadDepartment”. Esta consiste en varios objetos Department y, además del método printDepartmentName(), contiene también otros métodos adicionales para añadir objetos (addDepartment) o eliminar objetos ya existentes (removeDepartment):

public class HeadDepartment implements Department {
	private Integer id;
	private String name;
	private List<department> childDepartments;</department>
	public HeadDepartment(Integer id, String name) {
		this.id = id;
		this.name = name;
		this.childDepartments = new ArrayList<>();
	}
	public void printDepartmentName() {
		childDepartments.forEach(Department::printDepartmentName);
	}
	public void addDepartment(Department department) {
		childDepartments.add(department);
	}
	public void removeDepartment(Department department) {
		childDepartments.remove(department);
	}
}