El patrón de diseño Composite se enmarca dentro de los patrones estructurales y nos proporciona una manera de crear estructuras jerárquicas de objetos, donde los objetos individuales y las composiciones de objetos se tratan de la misma manera.
Este patrón nos permite crear objetos complejos a partir de objetos más simples y representar cualquier nivel de jerarquía. Es decir, podemos tratar un objeto individual y una composición de objetos de la misma manera, haciendo que el uso de los mismos sea muy sencillo e intuitivo.
La idea principal detrás del patrón Composite es que los objetos que componen una estructura jerárquica deben ser tratados de la misma manera, sin importar si se trata de un objeto único o de una composición de objetos. Así, podemos tratar un objeto individual y una composición de objetos de la misma manera, haciendo que el uso de los mismos sea muy sencillo e intuitivo.
La estructura del patrón Composite se compone de dos elementos: el Component y el Composite. El Component es la interfaz que define los métodos que todos los objetos en la estructura deben implementar. Por otro lado, el Composite es la clase que implementa el Component y que puede contener otros objetos del mismo tipo, permitiendo así construir estructuras jerárquicas.
Diagrama
El patrón de diseño Composite se puede representar del siguiente modo:
Component
+
|
+----------------+-------------+
| |
| |
| |
Leaf Composite
+ +
| |
| |
| |
(no tiene hijos) +--------+--------+
| |
| |
| |
Leaf Composite
+ +
| |
| |
| |
(no tiene hijos) +--------+--------+
| |
| |
| |
Leaf Leaf
+ +
| |
| |
| |
(no tiene hijos) (no tiene hijos)
En este diagrama, la clase Component
es la clase base que define la interfaz para todos los objetos en la estructura, ya sean hojas o compuestos. La clase Leaf
es una implementación concreta de Component
que representa una hoja en la estructura. La clase Composite
es otra implementación concreta de Component
que representa un objeto compuesto que contiene hojas o subcompuestos.
La flecha que va desde Component
a Leaf
indica que Leaf
es un tipo de Component
. La flecha que va desde Component
a Composite
indica lo mismo para Composite
. La flecha que va desde Composite
a Component
indica que un objeto compuesto puede contener uno o más componentes, ya sean hojas o subcompuestos.
Ejemplos
Java
Supongamos que necesitamos representar una estructura de una empresa, donde se tienen tanto empleados individuales como departamentos, y cada uno de ellos tiene un nombre y una descripción. Además, queremos poder calcular el salario total de toda la empresa sumando el salario de cada empleado individual y de cada departamento. Para modelar esta estructura y permitir este cálculo, podemos usar el patrón Composite.
En primer lugar, creamos una interfaz llamada Component
que define la operación getSalary()
que se encargará de calcular el salario total de la empresa y que será implementada por todas las clases que forman la estructura, tanto empleados individuales como departamentos:
public interface Component {
public double getSalary();
}
Luego, creamos la clase Employee
que representa a un empleado individual y que implementa la interfaz Component
:
public class Employee implements Component {
private String name;
private String description;
private double salary;
public Employee(String name, String description, double salary) {
this.name = name;
this.description = description;
this.salary = salary;
}
@Override
public double getSalary() {
return this.salary;
}
// Getters y setters
}
La clase Employee
tiene tres atributos: name
(nombre del empleado), description
(descripción del empleado) y salary
(salario del empleado). Además, implementa el método getSalary()
que simplemente devuelve el salario del empleado.
A continuación, creamos la clase Department
que representa a un departamento y que también implementa la interfaz Component
:
public class Department implements Component {
private String name;
private String description;
private List<Component> components;
public Department(String name, String description) {
this.name = name;
this.description = description;
this.components = new ArrayList<Component>();
}
@Override
public double getSalary() {
double totalSalary = 0;
for (Component component : components) {
totalSalary += component.getSalary();
}
return totalSalary;
}
public void add(Component component) {
this.components.add(component);
}
public void remove(Component component) {
this.components.remove(component);
}
// Getters y setters
}
La clase Department
tiene tres atributos: name
(nombre del departamento), description
(descripción del departamento) y components
(una lista de componentes que forman el departamento, que pueden ser tanto empleados individuales como otros departamentos). La clase implementa el método getSalary()
que suma el salario de todos los componentes que forman el departamento. Además, la clase tiene dos métodos add()
y remove()
que permiten agregar y eliminar componentes de la lista components
.
Por último, podemos crear una clase Client
que use la estructura creada y realice el cálculo del salario total de la empresa:
public class Client {
public static void main(String[] args) {
Component john = new Employee("John", "Manager", 5000.0);
Component mary = new Employee("Mary", "Developer", 4000.0);
Component bob = new Employee("Bob", "Developer", 4000.0);
Department development = new Department("Development", "Software development department");
development.add(mary);
development.add(bob);
Department company = new Department("Company", "Overall company");
company.add(john);
company.add(development);
System.out.println(company.getDetails());
}
}
En este ejemplo, se ha creado una estructura jerárquica de objetos Component
que representa una empresa, donde un Component
puede ser un Employee
o un Department
. Un Department
, a su vez, puede contener otros Department
y/o Employee
.
La clase Component
es la clase abstracta base de la jerarquía, y tiene los métodos abstractos getDetails()
y add(Component c)
. El método getDetails()
es el encargado de devolver una cadena con los detalles del objeto, mientras que el método add()
es el encargado de agregar un Component
a la jerarquía.
Las clases Employee
y Department
son las hojas y los nodos del árbol, respectivamente. La clase Employee
representa un empleado, y la clase Department
representa un departamento. Ambas heredan de la clase Component
y deben implementar sus métodos abstractos.
La clase Client
es la clase encargada de crear la estructura jerárquica y mostrar los detalles del objeto raíz. En este caso, se ha creado una empresa y se han añadido un Manager
, un departamento de desarrollo y dos desarrolladores. Finalmente, se muestra la información de la empresa llamando al método getDetails()
del objeto raíz.
Python
from abc import ABC, abstractmethod
class Component(ABC):
"""
La clase base Componente declara operaciones comunes tanto para simples como para complejos
objetos de la estructura.
"""
@abstractmethod
def operation(self):
pass
class Leaf(Component):
"""
Las hojas representan objetos finales de la estructura. Una hoja no puede tener subobjetos.
"""
def operation(self):
# El código de la operación de una hoja debe ser el más simple, ya que
# no tiene subobjetos que puedan hacer parte del trabajo por ella.
return "Leaf"
class Composite(Component):
"""
La clase Composite representa a los objetos complejos que pueden tener
subobjetos. Los compuestos suelen delegar el trabajo real a sus subobjetos
y luego "resumir" el resultado.
"""
def __init__(self):
self._children = []
def add(self, component: Component) -> None:
"""
Un objeto de componente complejo puede agregar o eliminar otros componentes
(simples o complejos) como sus hijos.
"""
self._children.append(component)
def remove(self, component: Component) -> None:
self._children.remove(component)
def is_composite(self) -> bool:
return True
def operation(self):
"""
El Composite lleva a cabo su trabajo principal delegando a sus hijos
"""
results = []
for child in self._children:
results.append(child.operation())
return f"Branch({'+'.join(results)})"
if __name__ == "__main__":
# El cliente trabaja con todos los componentes a través de la interfaz base.
def client_code(component: Component) -> None:
print(f"RESULTADO: {component.operation()}", end="")
# Crea una estructura de objetos simples y complejos
tree = Composite()
tree.add(Leaf())
tree.add(Leaf())
branch1 = Composite()
branch1.add(Leaf())
branch1.add(Leaf())
tree.add(branch1)
branch2 = Composite()
branch2.add(Leaf())
tree.add(branch2)
# Ejecuta la operación en toda la estructura
print("Cliente: Ejecuto la operacion en la estructura completa:")
client_code(tree)
print("\n\n")
# Ejecuta la operación en una parte de la estructura
print("Cliente: Ejecuto la operacion en una rama:")
client_code(branch1)
Este ejemplo implementa un árbol compuesto que se compone de dos tipos de objetos: hojas y ramas. Las hojas representan objetos finales de la estructura, mientras que las ramas pueden tener otros objetos como hijos. La interfaz Componente
define la operación operation()
que es común a todos los componentes. La clase Leaf
implementa esta operación de la manera más simple posible, mientras que la clase Composite
delega el trabajo a sus hijos. El ejemplo también muestra cómo un cliente puede trabajar con una estructura de componentes complejos a través de la interfaz Componente genérica, y cómo se puede ejecutar la operación en toda la estructura o en una parte de la estructura.
Go
package main
import "fmt"
// Component interface
type Component interface {
ShowDetails()
}
// Employee struct represents a leaf node
type Employee struct {
name string
position string
salary float64
}
// ShowDetails method for Employee
func (e *Employee) ShowDetails() {
fmt.Printf("%s - %s: %.2f\n", e.name, e.position, e.salary)
}
// Department struct represents a composite node
type Department struct {
name string
description string
components []Component
}
// Add method for Department
func (d *Department) Add(c Component) {
d.components = append(d.components, c)
}
// Remove method for Department
func (d *Department) Remove(c Component) {
for i, component := range d.components {
if component == c {
d.components = append(d.components[:i], d.components[i+1:]...)
break
}
}
}
// ShowDetails method for Department
func (d *Department) ShowDetails() {
fmt.Printf("%s - %s\n", d.name, d.description)
for _, component := range d.components {
component.ShowDetails()
}
}
func main() {
john := &Employee{name: "John", position: "Manager", salary: 5000.0}
mary := &Employee{name: "Mary", position: "Developer", salary: 4000.0}
bob := &Employee{name: "Bob", position: "Developer", salary: 4000.0}
development := &Department{name: "Development", description: "Software development department"}
development.Add(mary)
development.Add(bob)
company := &Department{name: "Company", description: "All employees"}
company.Add(john)
company.Add(development)
company.ShowDetails()
}
Como puedes ver, el ejemplo es muy similar al de Java y Python, con algunas diferencias en la sintaxis debido a las particularidades del lenguaje Go. En este caso, se utilizan structs en lugar de clases, y los métodos de la interfaz Component se implementan de manera explícita en cada struct. Además, se utilizan slices en lugar de listas enlazadas para almacenar los componentes de cada Department. Sin embargo, la estructura general del patrón composite se mantiene igual.
Conclusiones
En resumen, el patrón Composite es una técnica de diseño que nos permite construir estructuras jerárquicas de objetos de manera flexible y fácilmente extensible. Es útil cuando tenemos que tratar objetos individuales y composiciones de objetos de la misma manera, y cuando necesitamos trabajar con estructuras complejas que puedan ser construidas a partir de objetos más simples. Este patrón se utiliza en la mayoría de los entornos de programación y es muy útil en la construcción de interfaces gráficas de usuario, árboles de navegación, estructuras de datos, entre otras aplicaciones.
Comentarios