Apuntes DAM

Patrón Composite

Trata igual a un objeto suelto y a un grupo de objetos organizados en árbol: carpetas y ficheros, capítulos y partidas, menús y submenús. Cada operación se resuelve con una llamada recursiva.

nivel intermedioTambién: compuesto, árbol de objetos, parte-todo

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Composite

Escribe un árbol de carpetas y ficheros: carpeta(...) contiene elementos separados por comas y fichero:KB es un fichero con su tamaño.

proyecto.tamano()

  • esperando a sus hijos

Paso 1

proyecto/ es una carpeta: empieza con total = 0 y pide el tamaño a cada uno de sus 3 elementos, sin saber si son ficheros o carpetas.

1class Fichero implements Elemento {
2    public long tamano() {
3        return kb;
4    }
5}
6
7class Carpeta implements Elemento {
8    public long tamano() {
9        long total = 0;  // llamadas abiertas = proyecto, total = 0 KB
10        for (Elemento e : hijos)
11            total += e.tamano();
12        return total;
13    }
14}

Variables

llamadas abiertas
proyecto
total
0 KB

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Una carpeta contiene ficheros y otras carpetas, que a su vez contienen más. Para saber cuánto ocupa una carpeta hay que sumar sus ficheros y, para cada subcarpeta, repetir lo mismo. Si el código distingue a cada paso «si es un fichero haz esto, si es una carpeta haz lo otro», se llena de instanceof y de casos.

El patrón Composite da a la hoja (Fichero) y al compuesto (Carpeta) la misma interfaz (Elemento). El compuesto guarda una lista de Elemento y resuelve cada operación preguntando a sus hijos: tamano() de una carpeta es la suma de tamano() de lo que contiene, sin saber si son ficheros o carpetas. La recursión sale sola.

El cliente trata igual a un fichero que a un árbol entero: llama a tamano() o a mostrar() y ya está. Lo verás en los sistemas de ficheros, en el DOM de una página web (un elemento contiene elementos), en las interfaces gráficas (un panel contiene botones y otros paneles), en los menús y en los presupuestos por capítulos.

Cuándo usarlo

  • Los datos forman un árbol de partes y todos (carpetas, menús, organigramas, presupuestos, el DOM).
  • Quieres que el cliente trate igual a un elemento suelto y a un grupo.
  • Las operaciones se calculan combinando el resultado de los hijos (sumar, contar, buscar, pintar).

Cuándo no

  • Si la estructura no es un árbol o solo tiene un nivel: una lista basta.
  • Si las hojas y los compuestos casi no tienen operaciones en común: la interfaz común acaba con métodos que no tienen sentido para unos u otros.

Participantes

  1. Componente. La interfaz común a hojas y compuestos (Elemento) con las operaciones del árbol.
  2. Hoja. El elemento sin hijos (Fichero): resuelve cada operación por sí mismo.
  3. Compuesto. El elemento con hijos (Carpeta): guarda una lista de componentes y resuelve cada operación llamando a sus hijos.
  4. Cliente. Trabaja con la interfaz común, sin distinguir hojas de compuestos.

Diagrama de clases

0..*hijos«interface»Elemento+nombre() : String+tamano() : long+mostrar(sangria : String) : voidFichero-kb : longCarpeta-hijos : List<Elemento>+anadir(e : Elemento) : Carpeta
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface Elemento {
3  +nombre() : String
4  +tamano() : long
5  +mostrar(sangria : String) : void
6}
7class Fichero {
8  -kb : long
9}
10class Carpeta {
11  -hijos : List<Elemento>
12  +anadir(e : Elemento) : Carpeta
13}
14Elemento <|.. Fichero
15Elemento <|.. Carpeta
16Carpeta o-- "0..*" Elemento : hijos
17@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Carpetas y ficheros

tamano() y mostrar() de una carpeta llaman a los de sus hijos: la recursión recorre todo el árbol sin un solo instanceof.

Java
1import java.util.*;
2
3/** El componente: lo que tienen en común un fichero y una carpeta. */
4interface Elemento {
5    String nombre();
6    long tamano();                                     // en KB
7    void mostrar(String sangria);
8}
9
10/** Una hoja: no contiene nada. */
11class Fichero implements Elemento {
12    private final String nombre;
13    private final long kb;
14    Fichero(String nombre, long kb) { this.nombre = nombre; this.kb = kb; }
15    public String nombre() { return nombre; }
16    public long tamano() { return kb; }
17    public void mostrar(String sangria) { System.out.println(sangria + nombre + " (" + kb + " KB)"); }
18}
19
20/** El compuesto: contiene elementos, que pueden ser ficheros u otras carpetas. */
21class Carpeta implements Elemento {
22    private final String nombre;
23    private final List<Elemento> hijos = new ArrayList<>();
24
25    Carpeta(String nombre) { this.nombre = nombre; }
26
27    Carpeta anadir(Elemento e) { hijos.add(e); return this; }
28
29    public String nombre() { return nombre; }
30
31    public long tamano() {
32        long total = 0;
33        for (Elemento e : hijos) total += e.tamano();  // la misma llamada para ficheros y carpetas
34        return total;
35    }
36
37    public void mostrar(String sangria) {
38        System.out.println(sangria + nombre + "/ (" + tamano() + " KB)");
39        for (Elemento e : hijos) e.mostrar(sangria + "  ");
40    }
41}
42
43public class Main {
44    public static void main(String[] args) {
45        Carpeta proyecto = new Carpeta("proyecto")
46                .anadir(new Fichero("README.md", 4))
47                .anadir(new Carpeta("src")
48                        .anadir(new Fichero("Main.java", 12))
49                        .anadir(new Carpeta("modelo")
50                                .anadir(new Fichero("Alumno.java", 6))
51                                .anadir(new Fichero("Curso.java", 5))))
52                .anadir(new Carpeta("docs"));
53        proyecto.mostrar("");
54        Elemento suelto = new Fichero("notas.txt", 2);
55        System.out.println("Un fichero suelto también es un Elemento: " + suelto.tamano() + " KB");
56    }
57}

Salida al ejecutarlo (la misma en los 5 lenguajes)

proyecto/ (27 KB)
  README.md (4 KB)
  src/ (23 KB)
    Main.java (12 KB)
    modelo/ (11 KB)
      Alumno.java (6 KB)
      Curso.java (5 KB)
  docs/ (0 KB)
Un fichero suelto también es un Elemento: 2 KB

Una interfaz gráfica es un Composite

Un panel es un componente que contiene componentes: se añade a la ventana como un botón más.

Java
1// Las interfaces gráficas son un Composite: un panel es un componente que contiene componentes
2JPanel formulario = new JPanel(new GridLayout(0, 2));
3formulario.add(new JLabel("Nombre"));
4formulario.add(new JTextField(20));
5
6JPanel botones = new JPanel();
7botones.add(new JButton("Aceptar"));
8botones.add(new JButton("Cancelar"));
9
10JFrame ventana = new JFrame("Alta de alumno");
11ventana.add(formulario, BorderLayout.CENTER);
12ventana.add(botones, BorderLayout.SOUTH);        // un panel dentro de la ventana, como un botón más
13ventana.pack();                                   // calcula el tamaño de todo el árbol
14ventana.setVisible(true);                         // y lo pinta, de la raíz a las hojas

En la práctica

  • El DOM: cada elemento HTML contiene nodos (texto u otros elementos) y textContent o remove() funcionan igual en una hoja que en un subárbol.
  • Swing, JavaFX y Android: JPanel, Pane y ViewGroup son componentes que contienen componentes.
  • Los sistemas de ficheros (java.io.File, Path) y los menús con submenús de cualquier aplicación.
  • Los presupuestos por capítulos, los organigramas, las categorías de una tienda y las expresiones matemáticas ((2 + 3) * x es un árbol).

Errores típicos

  • Recorrer el árbol desde fuera con instanceof en lugar de dejar que cada nodo resuelva la operación.
  • Olvidar el caso del compuesto vacío: una carpeta sin nada debe dar 0, no un error.
  • Crear ciclos (meter una carpeta dentro de sí misma o de un descendiente): la recursión no termina.
  • Poner en la interfaz común métodos de compuesto (anadir) y hacer que las hojas lancen excepciones: mejor dejarlos solo en el compuesto.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Presupuesto de una obra

Un presupuesto se organiza en capítulos que contienen partidas y otros capítulos. Las partidas ya calculan su total y se muestran; el main ya crea el árbol. Completa total() y mostrar() de Capitulo para que trabajen con todo lo que contienen.

  • Órdenes: capitulo ruta (como Cocina o Cocina/Muebles), partida ruta nombre cantidad precio, total ruta (escribe Total de ruta: X €) y mostrar (todo el presupuesto desde Obra).
  • mostrar: cada capítulo como nombre: total € y, dos espacios más adentro, cada cosa que contiene en el orden en que se añadió; una partida es nombre: cantidad × precio € = total €.
JavaPresupuesto de una obraFácil

Ejemplo

Entrada (lo que se escribe por teclado)
capitulo Cocina
capitulo Cocina/Muebles
partida Cocina/Muebles Encimera 3 120.50
partida Cocina/Muebles Armario 2 89
partida Cocina Pintura 25 8
capitulo Bano
partida Bano Plato 1 340
total Cocina
total Cocina/Muebles
mostrar
Salida esperada
Total de Cocina: 739,50 €
Total de Cocina/Muebles: 539,50 €
Obra: 1079,50 €
  Cocina: 739,50 €
    Muebles: 539,50 €
      Encimera: 3 × 120,50 € = 361,50 €
      Armario: 2 × 89,00 € = 178,00 €
    Pintura: 25 × 8,00 € = 200,00 €
  Bano: 340,00 €
    Plato: 1 × 340,00 € = 340,00 €
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3/** Lo que tienen en común una partida y un capítulo del presupuesto. */
4interface Concepto {
5    String nombre();
6    double total();
7    void mostrar(String sangria);
8}
9
10/** Una hoja: una partida con su cantidad y su precio por unidad. */
11class Partida implements Concepto {
12    private final String nombre;
13    private final double cantidad, precio;
14    Partida(String nombre, double cantidad, double precio) { this.nombre = nombre; this.cantidad = cantidad; this.precio = precio; }
15    public String nombre() { return nombre; }
16    public double total() { return cantidad * precio; }
17    public void mostrar(String sangria) {
18        System.out.println(sangria + nombre + ": " + Main.num(cantidad) + " × " + Main.euros(precio) + " = " + Main.euros(total()));
19    }
20}
21
22/** El compuesto: un capítulo contiene partidas y otros capítulos. */
23class Capitulo implements Concepto {
24    private final String nombre;
25    private final List<Concepto> hijos = new ArrayList<>();
26    Capitulo(String nombre) { this.nombre = nombre; }
27    public String nombre() { return nombre; }
28    void anadir(Concepto c) { hijos.add(c); }
29
30    /** El hijo con ese nombre, o null. */
31    Concepto hijo(String nombre) {
32        for (Concepto c : hijos) if (c.nombre().equals(nombre)) return c;
33        return null;
34    }
35
36    public double total() {
37        double suma = 0;
38        for (Concepto c : hijos) suma += c.total();      // partidas y capítulos, igual
39        return suma;
40    }
41
42    public void mostrar(String sangria) {
43        System.out.println(sangria + nombre + ": " + Main.euros(total()));
44        for (Concepto c : hijos) c.mostrar(sangria + "  ");
45    }
46}
47
48public class Main {
49    static String euros(double x) {
50        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
51    }
52
53    static String num(double x) {
54        return x == Math.rint(x) ? String.valueOf((long) x) : String.valueOf(x).replace('.', ',');
55    }
56
57    /** El capítulo de una ruta («Cocina/Muebles»), o null si no existe. */
58    static Capitulo buscar(Capitulo raiz, String ruta) {
59        Capitulo actual = raiz;
60        if (ruta.equals("Obra")) return raiz;
61        for (String parte : ruta.split("/")) {
62            if (!(actual.hijo(parte) instanceof Capitulo c)) return null;
63            actual = c;
64        }
65        return actual;
66    }
67
68    public static void main(String[] args) {
69        Capitulo obra = new Capitulo("Obra");
70        Scanner sc = new Scanner(System.in);
71        while (sc.hasNextLine()) {
72            String linea = sc.nextLine().trim();
73            if (linea.isEmpty()) continue;
74            String[] p = linea.split("\\s+");
75            try {
76                switch (p[0] + "/" + (p.length - 1)) {
77                    case "capitulo/1" -> {
78                        int barra = p[1].lastIndexOf('/');
79                        Capitulo padre = barra < 0 ? obra : buscar(obra, p[1].substring(0, barra));
80                        String nombre = p[1].substring(barra + 1);
81                        if (padre == null) System.out.println("No existe el capítulo: " + p[1].substring(0, barra));
82                        else if (padre.hijo(nombre) != null) System.out.println("Ya existe: " + p[1]);
83                        else padre.anadir(new Capitulo(nombre));
84                    }
85                    case "partida/4" -> {
86                        Capitulo c = buscar(obra, p[1]);
87                        if (c == null) System.out.println("No existe el capítulo: " + p[1]);
88                        else c.anadir(new Partida(p[2], Double.parseDouble(p[3]), Double.parseDouble(p[4])));
89                    }
90                    case "total/1" -> {
91                        Capitulo c = buscar(obra, p[1]);
92                        System.out.println(c == null ? "No existe el capítulo: " + p[1] : "Total de " + p[1] + ": " + euros(c.total()));
93                    }
94                    case "mostrar/0" -> obra.mostrar("");
95                    default -> System.out.println("Orden no válida: " + linea);
96                }
97            } catch (NumberFormatException e) {
98                System.out.println("Orden no válida: " + linea);
99            }
100        }
101    }
102}

Cada capítulo solo conoce a sus hijos directos; la recursión baja sola hasta las partidas.

El main pide el total de un capítulo o de la obra entera con la misma llamada.

2. Menús con submenús

La barra de menús de una aplicación es un árbol: menús que contienen acciones y otros menús. Ya está la hoja, Accion. Completa Menu para que cuente, busque, se desactive y se muestre con todo lo que contiene.

  • Órdenes: menu ruta y accion ruta (crean un menú o una acción, como Archivo/Recientes), contar (Acciones activas: n de m), buscar texto (la ruta Menú > Submenú > Acción de cada acción activa que contiene el texto, sin distinguir mayúsculas, en orden), desactivar ruta y mostrar.
  • Desactivar un menú desactiva todo lo que contiene. mostrar escribe cada menú como nombre > y cada acción como - nombre, dos espacios más adentro por nivel, con (desactivado) o (desactivada) cuando toca.
JavaMenús con submenúsMedio

Ejemplo

Entrada (lo que se escribe por teclado)
menu Archivo
accion Archivo/Nuevo
accion Archivo/Abrir
menu Archivo/Recientes
accion Archivo/Recientes/notas.txt
accion Archivo/Recientes/apuntes.pdf
menu Editar
accion Editar/Copiar
accion Editar/Pegar
contar
buscar a
mostrar
Salida esperada
Acciones activas: 6 de 6
Archivo > Abrir
Archivo > Recientes > notas.txt
Archivo > Recientes > apuntes.pdf
Editar > Copiar
Editar > Pegar
Barra >
  Archivo >
    - Nuevo
    - Abrir
    Recientes >
      - notas.txt
      - apuntes.pdf
  Editar >
    - Copiar
    - Pegar
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3/** Lo que tienen en común una acción y un menú. */
4interface Opcion {
5    String nombre();
6    /** Cuántas acciones hay dentro (una acción cuenta 1); con soloActivas, solo las que están activas. */
7    int contar(boolean soloActivas);
8    /** Añade a la lista la ruta de cada acción activa cuyo nombre contiene el texto. */
9    void buscar(String texto, String ruta, List<String> encontradas);
10    void desactivar();
11    void mostrar(String sangria);
12}
13
14class Accion implements Opcion {
15    private final String nombre;
16    private boolean activa = true;
17    Accion(String nombre) { this.nombre = nombre; }
18    public String nombre() { return nombre; }
19    public int contar(boolean soloActivas) { return !soloActivas || activa ? 1 : 0; }
20    public void buscar(String texto, String ruta, List<String> encontradas) {
21        if (activa && nombre.toLowerCase().contains(texto.toLowerCase())) encontradas.add(ruta + nombre);
22    }
23    public void desactivar() { activa = false; }
24    public void mostrar(String sangria) { System.out.println(sangria + "- " + nombre + (activa ? "" : " (desactivada)")); }
25}
26
27class Menu implements Opcion {
28    private final String nombre;
29    private final List<Opcion> hijos = new ArrayList<>();
30    private boolean activo = true;
31    Menu(String nombre) { this.nombre = nombre; }
32    public String nombre() { return nombre; }
33    void anadir(Opcion o) { hijos.add(o); }
34    Opcion hijo(String nombre) {
35        for (Opcion o : hijos) if (o.nombre().equals(nombre)) return o;
36        return null;
37    }
38
39    public int contar(boolean soloActivas) {
40        int n = 0;
41        for (Opcion o : hijos) n += o.contar(soloActivas);
42        return n;
43    }
44
45    public void buscar(String texto, String ruta, List<String> encontradas) {
46        if (!activo) return;                                  // un menú desactivado no muestra nada
47        for (Opcion o : hijos) o.buscar(texto, ruta + nombre + " > ", encontradas);
48    }
49
50    public void desactivar() {
51        activo = false;
52        for (Opcion o : hijos) o.desactivar();                // desactivar un menú desactiva todo lo de dentro
53    }
54
55    public void mostrar(String sangria) {
56        System.out.println(sangria + nombre + " >" + (activo ? "" : " (desactivado)"));
57        for (Opcion o : hijos) o.mostrar(sangria + "  ");
58    }
59}
60
61public class Main {
62    /** El menú o la acción de una ruta («Archivo/Recientes»), o null. */
63    static Opcion buscar(Menu raiz, String ruta) {
64        Opcion actual = raiz;
65        for (String parte : ruta.split("/")) {
66            if (!(actual instanceof Menu m)) return null;
67            actual = m.hijo(parte);
68            if (actual == null) return null;
69        }
70        return actual;
71    }
72
73    public static void main(String[] args) {
74        Menu barra = new Menu("Barra");
75        Scanner sc = new Scanner(System.in);
76        while (sc.hasNextLine()) {
77            String linea = sc.nextLine().trim();
78            if (linea.isEmpty()) continue;
79            String[] p = linea.split("\\s+", 2);
80            String arg = p.length > 1 ? p[1].trim() : "";
81            switch (p[0]) {
82                case "menu", "accion" -> {
83                    int barraPos = arg.lastIndexOf('/');
84                    Opcion padre = barraPos < 0 ? barra : buscar(barra, arg.substring(0, barraPos));
85                    String nombre = arg.substring(barraPos + 1);
86                    if (nombre.isEmpty()) System.out.println("Orden no válida: " + linea);
87                    else if (!(padre instanceof Menu m)) System.out.println("No existe el menú: " + arg.substring(0, Math.max(barraPos, 0)));
88                    else if (m.hijo(nombre) != null) System.out.println("Ya existe: " + arg);
89                    else m.anadir(p[0].equals("menu") ? new Menu(nombre) : new Accion(nombre));
90                }
91                case "contar" -> System.out.println("Acciones activas: " + barra.contar(true) + " de " + barra.contar(false));
92                case "buscar" -> {
93                    List<String> r = new ArrayList<>();
94                    barra.buscar(arg, "", r);
95                    System.out.println(r.isEmpty() ? "Nada con «" + arg + "»" : String.join("\n", r).replace("Barra > ", ""));
96                }
97                case "desactivar" -> {
98                    Opcion o = buscar(barra, arg);
99                    if (o == null) System.out.println("No existe: " + arg);
100                    else o.desactivar();
101                }
102                case "mostrar" -> barra.mostrar("");
103                default -> System.out.println("Orden no válida: " + linea);
104            }
105        }
106    }
107}

Las cuatro operaciones tienen la misma forma: el menú reparte la llamada entre sus hijos y combina lo que devuelven (sumar, juntar resultados, propagar la desactivación).

El main nunca pregunta si algo es un menú o una acción para contar, buscar o mostrar.

Test

Test: Composite

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué tienen en común la hoja y el compuesto en el patrón Composite?

  2. 2.¿Cómo calcula el compuesto una operación como tamano()?

  3. 3.¿Cuál de estos es un ejemplo de Composite?

  4. 4.¿Qué debe devolver tamano() de una carpeta vacía?

  5. 5.¿Qué error hace que la recursión no termine?

Relacionado