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
- Componente. La interfaz común a hojas y compuestos (
Elemento) con las operaciones del árbol. - Hoja. El elemento sin hijos (
Fichero): resuelve cada operación por sí mismo. - Compuesto. El elemento con hijos (
Carpeta): guarda una lista de componentes y resuelve cada operación llamando a sus hijos. - Cliente. Trabaja con la interfaz común, sin distinguir hojas de compuestos.
Diagrama de clases
Ver el diagrama en PlantUML
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@endumlPuedes 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.
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}from abc import ABC, abstractmethod
class Elemento(ABC):
"""El componente: lo que tienen en común un fichero y una carpeta."""
@abstractmethod
def tamano(self): ... # en KB
@abstractmethod
def mostrar(self, sangria): ...
class Fichero(Elemento):
"""Una hoja: no contiene nada."""
def __init__(self, nombre, kb):
self.nombre, self.kb = nombre, kb
def tamano(self):
return self.kb
def mostrar(self, sangria):
print(f"{sangria}{self.nombre} ({self.kb} KB)")
class Carpeta(Elemento):
"""El compuesto: contiene elementos, que pueden ser ficheros u otras carpetas."""
def __init__(self, nombre):
self.nombre = nombre
self.hijos = []
def anadir(self, e):
self.hijos.append(e)
return self
def tamano(self):
return sum(e.tamano() for e in self.hijos) # la misma llamada para ficheros y carpetas
def mostrar(self, sangria):
print(f"{sangria}{self.nombre}/ ({self.tamano()} KB)")
for e in self.hijos:
e.mostrar(sangria + " ")
proyecto = (Carpeta("proyecto")
.anadir(Fichero("README.md", 4))
.anadir(Carpeta("src")
.anadir(Fichero("Main.java", 12))
.anadir(Carpeta("modelo")
.anadir(Fichero("Alumno.java", 6))
.anadir(Fichero("Curso.java", 5))))
.anadir(Carpeta("docs")))
proyecto.mostrar("")
suelto = Fichero("notas.txt", 2)
print(f"Un fichero suelto también es un Elemento: {suelto.tamano()} KB")/** Una hoja: no contiene nada. */
class Fichero {
constructor(nombre, kb) { this.nombre = nombre; this.kb = kb; }
tamano() { return this.kb; }
mostrar(sangria) { console.log(`${sangria}${this.nombre} (${this.kb} KB)`); }
}
/** El compuesto: contiene elementos, que pueden ser ficheros u otras carpetas. */
class Carpeta {
constructor(nombre) { this.nombre = nombre; this.hijos = []; }
anadir(e) { this.hijos.push(e); return this; }
tamano() {
return this.hijos.reduce((total, e) => total + e.tamano(), 0); // la misma llamada para todos
}
mostrar(sangria) {
console.log(`${sangria}${this.nombre}/ (${this.tamano()} KB)`);
for (const e of this.hijos) e.mostrar(sangria + " ");
}
}
const proyecto = new Carpeta("proyecto")
.anadir(new Fichero("README.md", 4))
.anadir(new Carpeta("src")
.anadir(new Fichero("Main.java", 12))
.anadir(new Carpeta("modelo")
.anadir(new Fichero("Alumno.java", 6))
.anadir(new Fichero("Curso.java", 5))))
.anadir(new Carpeta("docs"));
proyecto.mostrar("");
const suelto = new Fichero("notas.txt", 2);
console.log(`Un fichero suelto también es un Elemento: ${suelto.tamano()} KB`);using System;
using System.Collections.Generic;
using System.Linq;
// El componente: lo que tienen en común un fichero y una carpeta.
interface IElemento {
long Tamano(); // en KB
void Mostrar(string sangria);
}
// Una hoja: no contiene nada.
class Fichero : IElemento {
private readonly string nombre;
private readonly long kb;
public Fichero(string nombre, long kb) { this.nombre = nombre; this.kb = kb; }
public long Tamano() => kb;
public void Mostrar(string sangria) => Console.WriteLine(quot;{sangria}{nombre} ({kb} KB)");
}
// El compuesto: contiene elementos, que pueden ser ficheros u otras carpetas.
class Carpeta : IElemento {
private readonly string nombre;
private readonly List<IElemento> hijos = new();
public Carpeta(string nombre) => this.nombre = nombre;
public Carpeta Anadir(IElemento e) { hijos.Add(e); return this; }
public long Tamano() => hijos.Sum(e => e.Tamano()); // la misma llamada para ficheros y carpetas
public void Mostrar(string sangria) {
Console.WriteLine(quot;{sangria}{nombre}/ ({Tamano()} KB)");
foreach (var e in hijos) e.Mostrar(sangria + " ");
}
}
class Program {
static void Main() {
var proyecto = new Carpeta("proyecto")
.Anadir(new Fichero("README.md", 4))
.Anadir(new Carpeta("src")
.Anadir(new Fichero("Main.java", 12))
.Anadir(new Carpeta("modelo")
.Anadir(new Fichero("Alumno.java", 6))
.Anadir(new Fichero("Curso.java", 5))))
.Anadir(new Carpeta("docs"));
proyecto.Mostrar("");
IElemento suelto = new Fichero("notas.txt", 2);
Console.WriteLine(quot;Un fichero suelto también es un Elemento: {suelto.Tamano()} KB");
}
}<?php
/** El componente: lo que tienen en común un fichero y una carpeta. */
interface Elemento {
public function tamano(): int; // en KB
public function mostrar(string $sangria): void;
}
/** Una hoja: no contiene nada. */
class Fichero implements Elemento {
public function __construct(private string $nombre, private int $kb) {}
public function tamano(): int { return $this->kb; }
public function mostrar(string $sangria): void { echo "$sangria{$this->nombre} ({$this->kb} KB)\n"; }
}
/** El compuesto: contiene elementos, que pueden ser ficheros u otras carpetas. */
class Carpeta implements Elemento {
private array $hijos = [];
public function __construct(private string $nombre) {}
public function anadir(Elemento $e): self { $this->hijos[] = $e; return $this; }
public function tamano(): int {
return array_sum(array_map(fn (Elemento $e) => $e->tamano(), $this->hijos)); // la misma llamada para todos
}
public function mostrar(string $sangria): void {
echo "$sangria{$this->nombre}/ ({$this->tamano()} KB)\n";
foreach ($this->hijos as $e) $e->mostrar($sangria . " ");
}
}
$proyecto = (new Carpeta("proyecto"))
->anadir(new Fichero("README.md", 4))
->anadir((new Carpeta("src"))
->anadir(new Fichero("Main.java", 12))
->anadir((new Carpeta("modelo"))
->anadir(new Fichero("Alumno.java", 6))
->anadir(new Fichero("Curso.java", 5))))
->anadir(new Carpeta("docs"));
$proyecto->mostrar("");
$suelto = new Fichero("notas.txt", 2);
echo "Un fichero suelto también es un Elemento: {$suelto->tamano()} KB\n";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 KBUna 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.
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 hojasimport tkinter as tk # En tkinter, un Frame es un componente que contiene componentes ventana = tk.Tk() formulario = tk.Frame(ventana) tk.Label(formulario, text="Nombre").grid(row=0, column=0) tk.Entry(formulario).grid(row=0, column=1) botones = tk.Frame(ventana) tk.Button(botones, text="Aceptar").pack(side="left") tk.Button(botones, text="Cancelar").pack(side="left") formulario.pack() botones.pack() # un marco dentro de la ventana, como un botón más ventana.mainloop()
// El DOM es un Composite: un elemento contiene nodos, que pueden ser texto u otros elementos
const formulario = document.createElement("form");
const campo = document.createElement("input");
const botones = document.createElement("div");
botones.append(Object.assign(document.createElement("button"), { textContent: "Aceptar" }));
formulario.append(campo, botones); // un div con botones dentro, como un campo más
document.body.append(formulario);
// Las operaciones funcionan igual sobre una hoja o sobre un subárbol entero
console.log(formulario.textContent); // junta el texto de todos sus descendientes
formulario.remove(); // y quita el subárbol completo// En Windows Forms, un Panel es un control que contiene controles
var formulario = new TableLayoutPanel { ColumnCount = 2 };
formulario.Controls.Add(new Label { Text = "Nombre" });
formulario.Controls.Add(new TextBox());
var botones = new FlowLayoutPanel();
botones.Controls.Add(new Button { Text = "Aceptar" });
botones.Controls.Add(new Button { Text = "Cancelar" });
var ventana = new Form { Text = "Alta de alumno" };
ventana.Controls.Add(formulario);
ventana.Controls.Add(botones); // un panel dentro de la ventana, como un botón más<?php
// DOMDocument: un elemento contiene nodos, que pueden ser texto u otros elementos
$doc = new DOMDocument();
$form = $doc->createElement("form");
$botones = $doc->createElement("div");
$botones->appendChild($doc->createElement("button", "Aceptar"));
$form->appendChild($doc->createElement("input"));
$form->appendChild($botones); // un div con botones dentro, como un campo más
$doc->appendChild($form);
echo $doc->saveHTML();En la práctica
- El DOM: cada elemento HTML contiene nodos (texto u otros elementos) y
textContentoremove()funcionan igual en una hoja que en un subárbol. - Swing, JavaFX y Android:
JPanel,PaneyViewGroupson 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) * xes un árbol).
Errores típicos
- Recorrer el árbol desde fuera con
instanceofen 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(comoCocinaoCocina/Muebles),partida ruta nombre cantidad precio,total ruta(escribeTotal de ruta: X €) ymostrar(todo el presupuesto desdeObra). mostrar: cada capítulo comonombre: total €y, dos espacios más adentro, cada cosa que contiene en el orden en que se añadió; una partida esnombre: cantidad × precio € = total €.
Ejemplo
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
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 €Ver la solución explicada
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 rutayaccion ruta(crean un menú o una acción, comoArchivo/Recientes),contar(Acciones activas: n de m),buscar texto(la rutaMenú > Submenú > Acciónde cada acción activa que contiene el texto, sin distinguir mayúsculas, en orden),desactivar rutaymostrar. - Desactivar un menú desactiva todo lo que contiene.
mostrarescribe cada menú comonombre >y cada acción como- nombre, dos espacios más adentro por nivel, con(desactivado)o(desactivada)cuando toca.
Ejemplo
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
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
- PegarVer la solución explicada
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 aciertosElige 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.¿Qué tienen en común la hoja y el compuesto en el patrón Composite?
2.¿Cómo calcula el compuesto una operación como tamano()?
3.¿Cuál de estos es un ejemplo de Composite?
4.¿Qué debe devolver tamano() de una carpeta vacía?
5.¿Qué error hace que la recursión no termine?