Patrones de Diseño (IX): Patrones Estructurales - Composite

Este patrón proporciona una simplificación al planteamiento de programas que traten con objetos (relacionados) que pueden agruparse en grupos heterogéneos y dichos grupos a su vez siguen considerándose como elementos propios (que a su vez pueden repetir esta misma estructura).

Por ejemplo, es útil en problemas como el dibujado de figuras dentro de otras figuras, la composición por layouts, etc

Como es costumbre, vamos a revisar la plantilla del patrón Composite:

Composite


Nombre: Composite

Problema y Contexto:

Se nos proporciona un conjunto de clases relacionadas y un contenedor para todas esas clases. Nuestro objeto final tiene que contener una serie de elementos que son, o bien contenedores o bién objetos de las clases relacionadas.

A su vez, los contenedores pueden estar compuestos, igualmente, de objetos de las clases relacionadas o nuevos contenedores.

Se aplica cuando:

  • El usuario ignora la diferencia entre composiciones de objetos y objetos individuales.
  • Si se detecta que se estan utilizando varios objetos de la misma manera, y con frecuencia tienen código casi idéntico para manejarlos.


Solución y Estructura:

Lo que necesitamos hacer, básicamente, es definir una serie de comportamientos comunes tanto para las clases como para los contenedores. De esta manera podemos tratar tanto a contenedores como a elementos de manera homogénea mediante el uso de la recursión.

La estructura es la siguiente:

https://upload.wikimedia.org/wikipedia/commons/5/5a/Composite_UML_class_diagram_%28fixed%29.svg

Donde:

Client: Es la entidad que hará uso del objeto compuesto.

Component: Clase abstracta o interfaz que deben extender o implentar tanto los objetos simples como las agrupaciones de los mismos.

Leaf: Representación de un objeto simple. En jerga de árboles, representan las hoajas de los mismos.

Composite: Representa un componente compuesto o agrupación, es decir, que está a su vez compuesto de otros compotentes ya sean simples o compuestos.

Consecuencias

  • POSITIVAS:
    • Estructura nuestro programa de tal manera que clarifica las jerarquías de clases implicadas.
    • Hace el cliente simple.
    • Permite tratar la estructura y los objetos individuales
    • uniformemente.
    • Permite añadir nuevas clases de componentes de manera sencilla.
  • NEGATIVAS:
    • La composición, la mayoría de las veces, suele ser recursiva.
    • Puede hacer que el diseño sea demasiado general.
    • Hace más difícil restringir los componentes de un composite.
    • Para restringir un composite al uso de ciertos componentes, las comprobaciones se deben hacer en tiempo de ejecución.


Patrones Relacionados: Chain of responsibility, Decorator, Flyweight, Iterator y Visitor.

Ejemplo:

Vamos a ver un ejemplo para dejar todo más claro. Tenemos que diseñar un sistema que permita dibujar planos de una zona concreta. Las zonas pueden ser una ciudad, un barrio, una calle, una avenida, una plaza o una travesía. En primer lugar definiremos una interfaz que represente la zona a dibujar, que hará de Component:

interface Zona {
     public void generarPlano();
     public String obtenerTipo();
     public void agregarZona(Zona z);
     public Zona subzona(int i);
}

El siguiente paso es definir las zonas que pueden estar compuestas por otras zonas. Por ejemplo, una ciudad tiene varios barrios que a su vez están compuestos por domicilios. Lo haremos mediante la implementación de la interfaz zona:

public class Ciudad implements Zona{
     private String nombre;
     private ArrayList subzonas;

     public Ciudad(String nombre){
          this.nombre = nombre;
          subzonas = new ArrayList();
     }

     public void generarPlano(){
          System.out.println("CIUDAD: "+this.nombre);
          for(int i=0; i<subzonas.size();i++){
               subzonas.get(i).generarPlano();
          }
     }

     public void obtenerTipo(){
          return "ciudad";
     }

     public void agregarZona(Zona z){
          // Las ciudades se subdividen en barrios
          if(z.getTipo().equals("barrio")){
               subzonas.add(z);
          } else {
               System.out.println("No se puede añadir "+z.getTipo()+" directamente a una ciudad.");
          } 
     }

     public Zona subzona(int i){
          return subzonas.get(i);
     }
}

public class Barrio implements Zona{
     private String nombre;
     private ArrayList subzonas;
     
     public Barrio(String nombre){
          this.nombre = nombre;
          subzonas = new ArrayList();
     }

     public void generarPlano(){
          System.out.println("BARRIO: "+this.nombre);
          for(int i=0; i<subzonas.size();i++){
               subzonas.get(i).generarPlano();
          }
     }

     public void obtenerTipo(){
          return "barrio";
     }

     public void agregarZona(Zona z){
          // Un barrio no puede tener ni barrios ni ciudades dentro de el.
          if(!(z.getTipo().equals("ciudad") ||
               (z.getTipo().equals("barrio")){
               subzonas.add(z);
          } else {
               System.out.println("No se puede añadir "+z.getTipo()+" directamente a un barrio.");
          } 
     }

     public Zona subzona(int i){
          return subzonas.get(i);
          }
     }

Nos queda por implementar el resto de zonas, las que no son composiciones (zonas finales u hojas) o son composiciones sin hijos:
public class Calle implements Zona{
     private String nombre;
     
     public Calle(String nombre){
          this.nombre = nombre;
     }

     public void generarPlano(){
          System.out.println("CALLE: "+this.nombre);
     }

     public void obtenerTipo(){
          return "calle";
     }

     public void agregarZona(Zona z){
          System.out.println("No se pueden agregar zonas a "+z.getTipo());
     }
     
     public Zona subzona(int i){
          System.out.println("Este elemento no contiene subzonas");
     }
}

public class Avenida implements Zona{
     private String nombre;
     
     public Avenida(String nombre){
          this.nombre = nombre;
     }

     public void generarPlano(){
          System.out.println("AVENIDA: "+this.nombre);
     }

     public void obtenerTipo(){
          return "avenida";
     }

     public void agregarZona(Zona z){
          System.out.println("No se pueden agregar zonas a "+z.getTipo());
     }

     public Zona subzona(int i){
          System.out.println("Este elemento no contiene subzonas");
     }
}

public class Plaza implements Zona{
     private String nombre;

     public Plaza(String nombre){
          this.nombre = nombre;
     }

     public void generarPlano(){
          System.out.println("PLAZA: "+this.nombre);
     }

     public void obtenerTipo(){
          return "plaza";
     }

     public void agregarZona(Zona z){
          System.out.println("No se pueden agregar zonas a "+z.getTipo());
     }

     public Zona subzona(int i){
          System.out.println("Este elemento no contiene subzonas");
     }
}

public class Travesia implements Zona{
     private String nombre;

     public Travesia(String nombre){
          this.nombre = nombre;
     }

     public void generarPlano(){
          System.out.println("TRAVESIA: "+this.nombre);
     }

     public void obtenerTipo(){
          return "travesia";
     }

     public void agregarZona(Zona z){
          System.out.println("No se pueden agregar zonas a "+z.getTipo());
     }

     public Zona subzona(int i){
          System.out.println("Este elemento no contiene subzonas");
     }
}

Bien, ya tenemos todos los tipos de zonas definidos, ahora toca crear un cliente para probar la funcionalidad:
public static void main(String[] args){
     // Creamos una lista de zonas
     ArrayList zonas = new ArrayList();
      
     Zona madrid = new Ciudad("Madrid");

     Zona vallecas = new Barrio("Vallecas");
     Zona chamberi = new Barrio("Chamberí");
     Zona hortaleza = new Barrio("Hortaleza");

     vallecas.agregarZona(new Calle("Alameda del Valle"));
     vallecas.agregarZona(new Travesia("Gavia"));

     chamberi.agregarZona(new Avenida("Reina Victoria"));
     chamberi.agregarZona(new Plaza("Colón"));

     hortaleza.agregarZona(new Travesia("Biosca"));
     hortaleza.agregarZona(new Avenida("América"));

     madrid.agregarZona(vallecas);
     madrid.agregarZona(chamberi);
     madrid.agregarZona(hortaleza);
     
     zonas.add(madrid);
     zonas.add(new Ciudad("Barcelona"));
     zonas.add(new Barrio("Georgetown"));
     zonas.add(new Calle("Aleatoria"));

     // Como veis, tenemos zonas de varios tipos y varios niveles
     // Generaremos todos los planos independientemente del nivel
     for(int i=0; i<zonas.size();i++){
          zonas.get(i).generarPlano();
     }
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP