Patrón State (máquina de estados)
Cada estado de un objeto es una clase que decide qué hacer con cada acción y a qué estado pasar: un pedido, un semáforo o el enemigo de un juego, sin un switch gigante que crece con cada estado.
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.
State
Escribe las acciones que recibe el pedido, en orden: pagar, enviar, entregar, cancelar o devolver.
Paso 1
El pedido empieza en el estado «pendiente». Cada estado es una clase que sabe qué hacer con cada acción.
1class Pedido {
2 private EstadoPedido estado = new Pendiente(); // estado = pendiente
3
4 void accion(String evento) {
5 estado.manejar(evento, this);
6 }
7
8 void cambiar(EstadoPedido nuevo) {
9 estado = nuevo;
10 }
11}
12
13abstract class EstadoBase implements EstadoPedido {
14 public void manejar(String evento, Pedido p) {
15 System.out.println("No se puede " + evento);
16 }
17}Variables
- estado
- pendiente
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Muchos objetos se comportan distinto según su estado: un pedido pendiente se puede pagar, pero uno enviado no; un reproductor en pausa reanuda con play y uno parado vuelve a empezar. Eso es una máquina de estados: un conjunto de estados, unos eventos (acciones) y unas transiciones que dicen, para cada estado y cada evento, qué pasa y a qué estado se va.
La forma ingenua de programarla es un atributo con el estado y un switch en cada método. Funciona con pocos estados, pero cada estado nuevo obliga a tocar todos los métodos, y la lógica de un mismo estado queda repartida por toda la clase.
El patrón State le da la vuelta: cada estado es una clase que implementa una interfaz con un método por evento. El objeto (el contexto) guarda una referencia a su estado actual y le delega cada acción; el estado hace lo que toque y, si hay transición, cambia el estado del contexto por otro. Todo lo que hace un estado está junto, y añadir uno es añadir una clase.
Antes de programar, conviene dibujar la máquina: un diagrama de estados o una tabla con un estado por fila y un evento por columna. Ahí se ven enseguida las transiciones que faltan y los estados sin salida.
Cuándo usarlo
- El comportamiento de un objeto depende de su estado y cambia en tiempo de ejecución: pedidos, reservas, documentos con flujo de aprobación, reproductores, conexiones, personajes de un juego.
- Hay
switchsobre el mismo atributo de estado repetidos en varios métodos. - Los estados tienen bastante lógica propia (validaciones, efectos, tiempos).
Cuándo no
- Con dos o tres estados y casi sin lógica: un
enumcon unswitches más sencillo y se lee en una pantalla. - Si las transiciones son solo datos (una tabla de qué evento lleva a qué estado), una tabla o un
enumcon un mapa basta.
Participantes
- Estado. La interfaz con un método por cada evento que puede recibir el contexto (
pagar,enviar…). - Estados concretos. Una clase por estado que implementa los eventos que admite y rechaza los demás. Una clase base que rechaza todo por defecto ahorra repetir código.
- Contexto. El objeto con estado (
Pedido): guarda el estado actual, le delega cada acción y ofrece un método para cambiarlo. - Transiciones. Las decide cada estado: al tratar un evento, pone en el contexto el estado siguiente.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface EstadoPedido {
3 +pagar(p : Pedido) : void
4 +enviar(p : Pedido) : void
5 +entregar(p : Pedido) : void
6 +cancelar(p : Pedido) : void
7 +nombre() : String
8}
9class Pedido {
10 -estado : EstadoPedido
11 +setEstado(e : EstadoPedido) : void
12 +pagar() : void
13 +enviar() : void
14}
15class Pendiente
16class Pagado
17class Enviado
18class Entregado
19EstadoPedido <|.. Pendiente
20EstadoPedido <|.. Pagado
21EstadoPedido <|.. Enviado
22EstadoPedido <|.. Entregado
23Pedido o-- "1" EstadoPedido : estado
24@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Un semáforo de peatones
Aquí cada evento devuelve el estado siguiente (o el mismo, si no cambia) y el contexto lo guarda.
1/** Cada estado sabe qué hacer con cada acción y a qué estado pasar. */
2interface Estado {
3 Estado pulsar(); // el botón del semáforo de peatones
4 Estado tiempo(); // ha pasado el tiempo del estado
5 String nombre();
6}
7
8class Verde implements Estado {
9 public Estado pulsar() { return new Ambar(); } // un peatón quiere cruzar
10 public Estado tiempo() { return this; } // en verde se queda hasta que alguien pulse
11 public String nombre() { return "verde"; }
12}
13
14class Ambar implements Estado {
15 public Estado pulsar() { return this; } // ya está cambiando
16 public Estado tiempo() { return new Rojo(); }
17 public String nombre() { return "ámbar"; }
18}
19
20class Rojo implements Estado {
21 public Estado pulsar() { return this; }
22 public Estado tiempo() { return new Verde(); }
23 public String nombre() { return "rojo (cruzan los peatones)"; }
24}
25
26/** El contexto: guarda el estado actual y le delega cada acción. */
27class Semaforo {
28 private Estado estado = new Verde();
29 void pulsar() { estado = estado.pulsar(); System.out.println("pulsar → " + estado.nombre()); }
30 void tiempo() { estado = estado.tiempo(); System.out.println("tiempo → " + estado.nombre()); }
31}
32
33public class Main {
34 public static void main(String[] args) {
35 Semaforo s = new Semaforo();
36 s.tiempo();
37 s.pulsar();
38 s.pulsar();
39 s.tiempo();
40 s.tiempo();
41 }
42}class Verde:
"""Cada estado sabe qué hacer con cada acción y a qué estado pasar."""
nombre = "verde"
def pulsar(self): return Ambar() # un peatón quiere cruzar
def tiempo(self): return self # en verde se queda hasta que alguien pulse
class Ambar:
nombre = "ámbar"
def pulsar(self): return self # ya está cambiando
def tiempo(self): return Rojo()
class Rojo:
nombre = "rojo (cruzan los peatones)"
def pulsar(self): return self
def tiempo(self): return Verde()
class Semaforo:
"""El contexto: guarda el estado actual y le delega cada acción."""
def __init__(self):
self.estado = Verde()
def pulsar(self):
self.estado = self.estado.pulsar()
print("pulsar →", self.estado.nombre)
def tiempo(self):
self.estado = self.estado.tiempo()
print("tiempo →", self.estado.nombre)
s = Semaforo()
s.tiempo()
s.pulsar()
s.pulsar()
s.tiempo()
s.tiempo()/** Cada estado sabe qué hacer con cada acción y a qué estado pasar. */
class Verde {
nombre = "verde";
pulsar() { return new Ambar(); } // un peatón quiere cruzar
tiempo() { return this; } // en verde se queda hasta que alguien pulse
}
class Ambar {
nombre = "ámbar";
pulsar() { return this; } // ya está cambiando
tiempo() { return new Rojo(); }
}
class Rojo {
nombre = "rojo (cruzan los peatones)";
pulsar() { return this; }
tiempo() { return new Verde(); }
}
/** El contexto: guarda el estado actual y le delega cada acción. */
class Semaforo {
estado = new Verde();
pulsar() { this.estado = this.estado.pulsar(); console.log("pulsar → " + this.estado.nombre); }
tiempo() { this.estado = this.estado.tiempo(); console.log("tiempo → " + this.estado.nombre); }
}
const s = new Semaforo();
s.tiempo();
s.pulsar();
s.pulsar();
s.tiempo();
s.tiempo();using System;
// Cada estado sabe qué hacer con cada acción y a qué estado pasar.
interface IEstado {
IEstado Pulsar(); // el botón del semáforo de peatones
IEstado Tiempo(); // ha pasado el tiempo del estado
string Nombre { get; }
}
class Verde : IEstado {
public IEstado Pulsar() => new Ambar(); // un peatón quiere cruzar
public IEstado Tiempo() => this; // en verde se queda hasta que alguien pulse
public string Nombre => "verde";
}
class Ambar : IEstado {
public IEstado Pulsar() => this; // ya está cambiando
public IEstado Tiempo() => new Rojo();
public string Nombre => "ámbar";
}
class Rojo : IEstado {
public IEstado Pulsar() => this;
public IEstado Tiempo() => new Verde();
public string Nombre => "rojo (cruzan los peatones)";
}
// El contexto: guarda el estado actual y le delega cada acción.
class Semaforo {
private IEstado estado = new Verde();
public void Pulsar() { estado = estado.Pulsar(); Console.WriteLine("pulsar → " + estado.Nombre); }
public void Tiempo() { estado = estado.Tiempo(); Console.WriteLine("tiempo → " + estado.Nombre); }
}
class Program {
static void Main() {
var s = new Semaforo();
s.Tiempo();
s.Pulsar();
s.Pulsar();
s.Tiempo();
s.Tiempo();
}
}<?php
/** Cada estado sabe qué hacer con cada acción y a qué estado pasar. */
interface Estado {
public function pulsar(): Estado; // el botón del semáforo de peatones
public function tiempo(): Estado; // ha pasado el tiempo del estado
public function nombre(): string;
}
class Verde implements Estado {
public function pulsar(): Estado { return new Ambar(); } // un peatón quiere cruzar
public function tiempo(): Estado { return $this; } // en verde se queda hasta que alguien pulse
public function nombre(): string { return "verde"; }
}
class Ambar implements Estado {
public function pulsar(): Estado { return $this; } // ya está cambiando
public function tiempo(): Estado { return new Rojo(); }
public function nombre(): string { return "ámbar"; }
}
class Rojo implements Estado {
public function pulsar(): Estado { return $this; }
public function tiempo(): Estado { return new Verde(); }
public function nombre(): string { return "rojo (cruzan los peatones)"; }
}
/** El contexto: guarda el estado actual y le delega cada acción. */
class Semaforo {
private Estado $estado;
public function __construct() { $this->estado = new Verde(); }
public function pulsar(): void { $this->estado = $this->estado->pulsar(); echo "pulsar → " . $this->estado->nombre() . "\n"; }
public function tiempo(): void { $this->estado = $this->estado->tiempo(); echo "tiempo → " . $this->estado->nombre() . "\n"; }
}
$s = new Semaforo();
$s->tiempo();
$s->pulsar();
$s->pulsar();
$s->tiempo();
$s->tiempo();Salida al ejecutarlo (la misma en los 5 lenguajes)
tiempo → verde pulsar → ámbar pulsar → ámbar tiempo → rojo (cruzan los peatones) tiempo → verde
La alternativa con switch
Para comparar: así queda un solo evento sin el patrón.
1// La alternativa sin patrón: un switch en cada acción. Con 4 estados y 4 acciones son 16 casos
2// repartidos por la clase, y cada estado nuevo obliga a tocar todos los métodos.
3void enviar() {
4 switch (estado) {
5 case PENDIENTE -> System.out.println("No se puede enviar un pedido sin pagar");
6 case PAGADO -> estado = Estado.ENVIADO;
7 case ENVIADO, ENTREGADO -> System.out.println("Ya se envió");
8 case CANCELADO -> System.out.println("El pedido está cancelado");
9 }
10}# La alternativa sin patrón: un match en cada acción. Con 4 estados y 4 acciones son 16 casos
# repartidos por la clase, y cada estado nuevo obliga a tocar todos los métodos.
def enviar(self):
match self.estado:
case Estado.PENDIENTE:
print("No se puede enviar un pedido sin pagar")
case Estado.PAGADO:
self.estado = Estado.ENVIADO
case Estado.ENVIADO | Estado.ENTREGADO:
print("Ya se envió")
case Estado.CANCELADO:
print("El pedido está cancelado")// La alternativa sin patrón: un switch en cada acción. Con 4 estados y 4 acciones son 16 casos
// repartidos por la clase, y cada estado nuevo obliga a tocar todos los métodos.
enviar() {
switch (this.estado) {
case "pendiente": console.log("No se puede enviar un pedido sin pagar"); break;
case "pagado": this.estado = "enviado"; break;
case "enviado":
case "entregado": console.log("Ya se envió"); break;
case "cancelado": console.log("El pedido está cancelado"); break;
}
}// La alternativa sin patrón: un switch en cada acción. Con 4 estados y 4 acciones son 16 casos
// repartidos por la clase, y cada estado nuevo obliga a tocar todos los métodos.
void Enviar() {
switch (estado) {
case Estado.Pendiente: Console.WriteLine("No se puede enviar un pedido sin pagar"); break;
case Estado.Pagado: estado = Estado.Enviado; break;
case Estado.Enviado or Estado.Entregado: Console.WriteLine("Ya se envió"); break;
case Estado.Cancelado: Console.WriteLine("El pedido está cancelado"); break;
}
}// La alternativa sin patrón: un match en cada acción. Con 4 estados y 4 acciones son 16 casos
// repartidos por la clase, y cada estado nuevo obliga a tocar todos los métodos.
public function enviar(): void {
match ($this->estado) {
Estado::Pendiente => print("No se puede enviar un pedido sin pagar\n"),
Estado::Pagado => $this->estado = Estado::Enviado,
Estado::Enviado, Estado::Entregado => print("Ya se envió\n"),
Estado::Cancelado => print("El pedido está cancelado\n"),
};
}Traza: transiciones del pedido
| Estado \ evento | pagar | enviar | entregar | cancelar | devolver |
|---|---|---|---|---|---|
| pendiente | → pagado | ✗ | ✗ | → cancelado | ✗ |
| pagado | ya está pagado | → enviado | ✗ | → cancelado (reembolso) | ✗ |
| enviado | ✗ | ✗ | → entregado | en camino: ✗ | ✗ |
| entregado | ✗ | ✗ | ✗ | ✗ | → devuelto (14 días) |
| cancelado, devuelto | ✗ | ✗ | ✗ | ✗ | ✗ |
La tabla de transiciones es la especificación: cada fila es una clase de estado y cada celda, un método. Las ✗ las resuelve la clase base rechazando la acción.
En la práctica
- El ciclo de vida de una Activity de Android y la máquina de estados de
MediaPlayerson máquinas de estados (hay un ejercicio largo de cada una). - Los protocolos de red: TCP pasa por LISTEN, SYN_SENT, ESTABLISHED, CLOSE_WAIT…
- La IA de los enemigos de un juego (patrulla, persigue, ataca, huye) y los analizadores léxicos (autómatas finitos).
- Los flujos de trabajo de una empresa (una factura borrador, emitida, cobrada, anulada) y librerías como Spring State Machine.
Errores típicos
- Dejar que el contexto decida las transiciones con
ifsobre el tipo del estado: se vuelve al switch con más clases. - Olvidar una combinación de estado y evento: con la clase base que rechaza todo, un evento no previsto da un mensaje claro en lugar de un comportamiento raro.
- Estados que guardan datos que deberían estar en el contexto (el importe, la fecha): al cambiar de estado se pierden.
- Crear un objeto de estado nuevo en cada transición cuando el estado no tiene datos propios: se puede reutilizar una sola instancia (o usar un
enum). - No dibujar la máquina antes: aparecen estados sin salida o eventos que no llevan a ninguna parte.
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. El pedido de una tienda online
Completa los estados del pedido con el patrón State. La interfaz, la clase base que rechaza cualquier acción (No se puede enviar un pedido pendiente) y el estado Pendiente ya están escritos, igual que el contexto Pedido, que escribe cada transición. Redefine en cada estado las acciones que admite según la tabla de transiciones de la ficha.
- Órdenes:
pagar,enviar,entregar,cancelar,devolveryesperar N(pasan N días). Cada transición escribependiente → pagado(lo hacePedido.cambiar). - Mensajes propios: pagar un pedido pagado,
Ya está pagado; cancelar uno pagado cambia a cancelado y escribe despuésSe devuelve el importe al cliente; cancelar uno enviado,Ya está en camino: espera a la entrega y devuélvelo; devolver uno entregado hace más de 14 días,Han pasado más de 14 días: ya no se puede devolver. - Al final,
Historial: pendiente → pagado → ….
Ejemplo
enviar pagar pagar enviar cancelar entregar esperar 3 devolver pagar
No se puede enviar un pedido pendiente pendiente → pagado Ya está pagado pagado → enviado Ya está en camino: espera a la entrega y devuélvelo enviado → entregado entregado → devuelto No se puede pagar un pedido devuelto Historial: pendiente → pagado → enviado → entregado → devuelto
Ver la solución explicada
1import java.util.*;
2
3/** Las acciones del pedido. Cada estado decide qué pasa y a qué estado se va. */
4interface EstadoPedido {
5 void pagar(Pedido p);
6 void enviar(Pedido p);
7 void entregar(Pedido p);
8 void cancelar(Pedido p);
9 void devolver(Pedido p);
10 String nombre();
11}
12
13/** Por defecto, ninguna acción está permitida: cada estado redefine las suyas. */
14abstract class EstadoBase implements EstadoPedido {
15 private void rechazar(String accion) {
16 System.out.println("No se puede " + accion + " un pedido " + nombre());
17 }
18 public void pagar(Pedido p) { rechazar("pagar"); }
19 public void enviar(Pedido p) { rechazar("enviar"); }
20 public void entregar(Pedido p) { rechazar("entregar"); }
21 public void cancelar(Pedido p) { rechazar("cancelar"); }
22 public void devolver(Pedido p) { rechazar("devolver"); }
23}
24
25class Pendiente extends EstadoBase {
26 public String nombre() { return "pendiente"; }
27 public void pagar(Pedido p) { p.cambiar(new Pagado()); }
28 public void cancelar(Pedido p) { p.cambiar(new Cancelado()); }
29}
30
31class Pagado extends EstadoBase {
32 public String nombre() { return "pagado"; }
33 public void pagar(Pedido p) { System.out.println("Ya está pagado"); }
34 public void enviar(Pedido p) { p.cambiar(new Enviado()); }
35 public void cancelar(Pedido p) {
36 p.cambiar(new Cancelado());
37 System.out.println("Se devuelve el importe al cliente");
38 }
39}
40
41class Enviado extends EstadoBase {
42 public String nombre() { return "enviado"; }
43 public void entregar(Pedido p) { p.cambiar(new Entregado()); }
44 public void cancelar(Pedido p) { System.out.println("Ya está en camino: espera a la entrega y devuélvelo"); }
45}
46
47class Entregado extends EstadoBase {
48 public String nombre() { return "entregado"; }
49 public void devolver(Pedido p) {
50 if (p.diasDesdeEntrega() > 14) System.out.println("Han pasado más de 14 días: ya no se puede devolver");
51 else p.cambiar(new Devuelto());
52 }
53}
54
55class Cancelado extends EstadoBase {
56 public String nombre() { return "cancelado"; }
57}
58
59class Devuelto extends EstadoBase {
60 public String nombre() { return "devuelto"; }
61}
62
63/** El contexto: delega cada acción en su estado actual. */
64class Pedido {
65 private EstadoPedido estado = new Pendiente();
66 private final List<String> historial = new ArrayList<>(List.of("pendiente"));
67 private int dias = 0, diaEntrega = -1;
68
69 void cambiar(EstadoPedido nuevo) {
70 System.out.println(estado.nombre() + " → " + nuevo.nombre());
71 estado = nuevo;
72 historial.add(nuevo.nombre());
73 if (nuevo.nombre().equals("entregado")) diaEntrega = dias;
74 }
75
76 int diasDesdeEntrega() { return dias - diaEntrega; }
77 void pasarDias(int n) { dias += n; }
78 String historial() { return String.join(" → ", historial); }
79 EstadoPedido estado() { return estado; }
80}
81
82public class Main {
83 public static void main(String[] args) {
84 Scanner sc = new Scanner(System.in);
85 Pedido p = new Pedido();
86 while (sc.hasNextLine()) {
87 String linea = sc.nextLine().trim();
88 if (linea.isEmpty()) continue;
89 String[] w = linea.split("\\s+");
90 switch (w[0]) {
91 case "pagar" -> p.estado().pagar(p);
92 case "enviar" -> p.estado().enviar(p);
93 case "entregar" -> p.estado().entregar(p);
94 case "cancelar" -> p.estado().cancelar(p);
95 case "devolver" -> p.estado().devolver(p);
96 case "esperar" -> {
97 try {
98 p.pasarDias(Integer.parseInt(w[1]));
99 } catch (RuntimeException e) {
100 System.out.println("Orden no válida: " + linea);
101 }
102 }
103 default -> System.out.println("Orden no válida: " + linea);
104 }
105 }
106 System.out.println("Historial: " + p.historial());
107 }
108}Cada clase contiene exactamente una fila de la tabla de transiciones: lo que se puede hacer en ese estado. Las combinaciones no permitidas las resuelve la clase base con un mensaje genérico, así que no hay casos olvidados.
Añadir un estado nuevo (por ejemplo, «en almacén» entre pagado y enviado) es escribir una clase y cambiar una transición, sin tocar el resto.
2. Una máquina expendedora
Una máquina expendedora tiene tres estados: en reposo (sin dinero, ya escrito), con saldo y sin productos (todo agotado). Programa los dos que faltan. Con saldo: las monedas suman; al elegir un producto, si existe, quedan unidades y el saldo llega, sale el producto, se devuelve el cambio y se vuelve a reposo (o a sin productos si era la última unidad de todo); cancelar devuelve el saldo. Sin productos: no acepta monedas (las devuelve) ni ventas. El dinero va en céntimos y el contexto ya sabe devolver el cambio con las monedas más grandes.
- Órdenes:
moneda 0.50(monedas de 2, 1, 0,50, 0,20, 0,10 y 0,05 €),elegir A1,cancelaryreponer A1 3. Productos: A1 agua (1,00 €, 2 uds.), A2 refresco (1,50 €, 1 ud.) y B1 patatas (1,20 €, 1 ud.). - Con saldo:
Saldo: 1,50 €al añadir;Código desconocido: X,refresco: agotado,Faltan 0,30 € para patatasoSale aguaseguido del cambio (Devuelve 0,50 €: 1 × 0,50 €); cancelar devuelve todo el saldo así. - Sin productos:
Fuera de servicio: se devuelve la moneda de 1,00 €,Fuera de servicioal elegir yNo hay nada que devolveral cancelar. Cada cambio de estado escribe[en reposo → con saldo].
Ejemplo
elegir A1 moneda 2 elegir B1 moneda 0.5 moneda 0.2 elegir A2 cancelar
Introduce 1,00 € para agua [en reposo → con saldo] Saldo: 2,00 € Sale patatas Devuelve 0,80 €: 1 × 0,50 € + 1 × 0,20 € + 1 × 0,10 € [con saldo → en reposo] [en reposo → con saldo] Saldo: 0,50 € Saldo: 0,70 € Faltan 0,80 € para refresco Devuelve 0,70 €: 1 × 0,50 € + 1 × 0,20 € [con saldo → en reposo] Estado final: en reposo
Ver la solución explicada
1import java.util.*;
2
3interface EstadoMaquina {
4 void moneda(Maquina m, int centimos);
5 void elegir(Maquina m, String codigo);
6 void cancelar(Maquina m);
7 String nombre();
8}
9
10/** Sin dinero dentro. */
11class Reposo implements EstadoMaquina {
12 public void moneda(Maquina m, int c) {
13 m.saldo = c;
14 m.cambiar(new ConSaldo());
15 System.out.println("Saldo: " + Maquina.euros(m.saldo));
16 }
17 public void elegir(Maquina m, String codigo) {
18 Producto p = m.productos.get(codigo);
19 System.out.println(p == null ? "Código desconocido: " + codigo : "Introduce " + Maquina.euros(p.precio) + " para " + p.nombre);
20 }
21 public void cancelar(Maquina m) { System.out.println("No hay nada que devolver"); }
22 public String nombre() { return "en reposo"; }
23}
24
25/** Con dinero dentro, esperando a que se elija un producto. */
26class ConSaldo implements EstadoMaquina {
27 public void moneda(Maquina m, int c) {
28 m.saldo += c;
29 System.out.println("Saldo: " + Maquina.euros(m.saldo));
30 }
31
32 public void elegir(Maquina m, String codigo) {
33 Producto p = m.productos.get(codigo);
34 if (p == null) System.out.println("Código desconocido: " + codigo);
35 else if (p.stock == 0) System.out.println(p.nombre + ": agotado");
36 else if (m.saldo < p.precio) System.out.println("Faltan " + Maquina.euros(p.precio - m.saldo) + " para " + p.nombre);
37 else {
38 p.stock--;
39 System.out.println("Sale " + p.nombre);
40 m.devolverCambio(m.saldo - p.precio);
41 m.saldo = 0;
42 m.cambiar(m.quedanProductos() ? new Reposo() : new SinProductos());
43 }
44 }
45
46 public void cancelar(Maquina m) {
47 m.devolverCambio(m.saldo);
48 m.saldo = 0;
49 m.cambiar(new Reposo());
50 }
51
52 public String nombre() { return "con saldo"; }
53}
54
55/** Todo agotado: no acepta dinero hasta que se reponga. */
56class SinProductos implements EstadoMaquina {
57 public void moneda(Maquina m, int c) { System.out.println("Fuera de servicio: se devuelve la moneda de " + Maquina.euros(c)); }
58 public void elegir(Maquina m, String codigo) { System.out.println("Fuera de servicio"); }
59 public void cancelar(Maquina m) { System.out.println("No hay nada que devolver"); }
60 public String nombre() { return "sin productos"; }
61}
62
63class Producto {
64 final String nombre;
65 final int precio;
66 int stock;
67 Producto(String nombre, int precio, int stock) { this.nombre = nombre; this.precio = precio; this.stock = stock; }
68}
69
70/** El contexto. El dinero va en céntimos: con double, 0,10 + 0,20 no da 0,30 exacto. */
71class Maquina {
72 static final int[] MONEDAS = {200, 100, 50, 20, 10, 5};
73 final Map<String, Producto> productos = new LinkedHashMap<>();
74 EstadoMaquina estado = new Reposo();
75 int saldo;
76
77 static String euros(int c) {
78 return c / 100 + "," + String.format("%02d", c % 100) + " €";
79 }
80
81 void cambiar(EstadoMaquina nuevo) {
82 System.out.println("[" + estado.nombre() + " → " + nuevo.nombre() + "]");
83 estado = nuevo;
84 }
85
86 boolean quedanProductos() {
87 return productos.values().stream().anyMatch(p -> p.stock > 0);
88 }
89
90 /** Devuelve el importe con las monedas más grandes posibles. */
91 void devolverCambio(int c) {
92 if (c == 0) return;
93 List<String> monedas = new ArrayList<>();
94 int resto = c;
95 for (int m : MONEDAS) {
96 int n = resto / m;
97 if (n > 0) monedas.add(n + " × " + euros(m));
98 resto %= m;
99 }
100 System.out.println("Devuelve " + euros(c) + ": " + String.join(" + ", monedas));
101 }
102
103 void reponer(String codigo, int unidades) {
104 Producto p = productos.get(codigo);
105 if (p == null || unidades <= 0) {
106 System.out.println("No se puede reponer " + codigo);
107 return;
108 }
109 if (estado instanceof ConSaldo) {
110 System.out.println("No se puede reponer con dinero dentro");
111 return;
112 }
113 p.stock += unidades;
114 System.out.println("Repuesto " + p.nombre + ": " + p.stock + " unidades");
115 if (estado instanceof SinProductos) cambiar(new Reposo());
116 }
117}
118
119public class Main {
120 public static void main(String[] args) {
121 Scanner sc = new Scanner(System.in);
122 Maquina m = new Maquina();
123 m.productos.put("A1", new Producto("agua", 100, 2));
124 m.productos.put("A2", new Producto("refresco", 150, 1));
125 m.productos.put("B1", new Producto("patatas", 120, 1));
126 while (sc.hasNextLine()) {
127 String linea = sc.nextLine().trim();
128 if (linea.isEmpty()) continue;
129 String[] w = linea.split("\\s+");
130 try {
131 switch (w[0]) {
132 case "moneda" -> {
133 int c = (int) Math.round(Double.parseDouble(w[1].replace(',', '.')) * 100);
134 if (Arrays.stream(Maquina.MONEDAS).noneMatch(x -> x == c)) System.out.println("Moneda no admitida: " + w[1]);
135 else m.estado.moneda(m, c);
136 }
137 case "elegir" -> m.estado.elegir(m, w[1]);
138 case "cancelar" -> m.estado.cancelar(m);
139 case "reponer" -> m.reponer(w[1], Integer.parseInt(w[2]));
140 default -> throw new IllegalArgumentException();
141 }
142 } catch (RuntimeException e) {
143 System.out.println("Orden no válida: " + linea);
144 }
145 }
146 System.out.println("Estado final: " + m.estado.nombre() + (m.saldo > 0 ? " (saldo " + Maquina.euros(m.saldo) + ")" : ""));
147 }
148}Los tres estados responden a las mismas tres acciones de formas distintas, y ninguno necesita preguntar en qué estado está: lo sabe por ser la clase que es.
Las reglas que no dependen del estado (reponer, devolver el cambio, el formato del dinero) se quedan en el contexto; las que sí dependen, en los estados. Es la frontera que hay que decidir en cada máquina.
Test
Test: State (máquina de estados)
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.En el patrón State, ¿quién decide a qué estado se pasa tras un evento?
2.¿Qué ventaja tiene sobre un switch en cada método?
3.¿Para qué sirve una clase base que rechaza todas las acciones por defecto?
4.¿Qué patrón tiene casi la misma estructura de clases que State?
5.Con tres estados sin apenas lógica, ¿qué suele ser más sencillo?