Composición y colaboración entre objetos
Una aplicación real rara vez cabe en una sola clase. La clave no es crear una clase enorme, sino repartir responsabilidades y hacer que los objetos colaboren.
Una relación «tiene un»
TasteMatch necesita pedidos asociados a clientes. Un pedido tiene un cliente; no es un tipo de cliente. Esa relación se modela mediante composición: un objeto guarda una referencia a otro.
class Cliente {
private String nombre;
public Cliente(String nombre) {
this.nombre = nombre;
}
public String getNombre() {
return nombre;
}
}
class Pedido {
private int numero;
private Cliente cliente;
public Pedido(int numero, Cliente cliente) {
this.numero = numero;
this.cliente = cliente;
}
public String resumen() {
return "Pedido " + numero + " de " + cliente.getNombre();
}
}
Pedido no copia el nombre: conserva la referencia al Cliente y le pide el dato cuando lo necesita. Ambos objetos colaboran, y cada uno mantiene su responsabilidad.
Un estado con valores limitados
El estado de un pedido solo puede ser uno de unos pocos valores. Un String permitiría errores como "en preparacion" o "EN_PREPARACION ". Un enum expresa el conjunto válido:
enum EstadoPedido {
PENDIENTE,
PREPARANDO,
LISTO,
ENTREGADO
}
Ahora Pedido puede proteger sus transiciones:
class Pedido {
private int numero;
private Cliente cliente;
private EstadoPedido estado;
public Pedido(int numero, Cliente cliente) {
this.numero = numero;
this.cliente = cliente;
this.estado = EstadoPedido.PENDIENTE;
}
public boolean empezarPreparacion() {
if (estado != EstadoPedido.PENDIENTE) {
return false;
}
estado = EstadoPedido.PREPARANDO;
return true;
}
public String resumen() {
return "Pedido " + numero + " de " + cliente.getNombre()
+ " - " + estado;
}
}
El constructor decide el estado inicial y el método decide cuándo puede cambiar. El exterior no puede inventar estados ni saltarse la regla.
Delegar en quien conoce la respuesta
Componer no significa trasladar toda la lógica a Pedido. Si un cliente conoce su descuento, el pedido puede delegar el cálculo:
class Cliente {
private String nombre;
private boolean premium;
public Cliente(String nombre, boolean premium) {
this.nombre = nombre;
this.premium = premium;
}
public double aplicarDescuento(double importe) {
return premium ? importe * 0.90 : importe;
}
}
La pregunta útil es: ¿qué objeto dispone de los datos y las reglas necesarias? La respuesta ayuda a colocar cada comportamiento.
Dependencias explícitas
Si un Pedido no tiene sentido sin cliente, recibirlo en el constructor hace visible esa dependencia. En los ejemplos de Java 2 asumiremos que el colaborador obligatorio recibido es válido y no es null.
Más adelante estudiaremos mecanismos adecuados para rechazar o comunicar entradas inválidas. Por ahora, lo importante es reconocer que la relación forma parte del diseño y evitar constructores que puedan dejar objetos parcialmente inválidos.
Composición no es herencia
Este diseño es incorrecto:
// Incorrecto: un pedido no es un cliente.
class Pedido extends Cliente {
}
extends expresa «es un» y llegará en la próxima unidad. Cuando la frase natural es «tiene un» o «usa un», una referencia suele ser la opción adecuada.
Ejemplo completo
public class PedidosDemo {
public static void main(String[] args) {
Cliente cliente = new Cliente("Nuria");
Pedido pedido = new Pedido(101, cliente);
System.out.println(pedido.resumen());
System.out.println(pedido.empezarPreparacion());
System.out.println(pedido.empezarPreparacion());
System.out.println(pedido.resumen());
}
}
La segunda transición devuelve false: el pedido ya no está pendiente.
Errores habituales
- Guardar datos duplicados en vez de una referencia al objeto que los posee.
- Hacer públicos los atributos para que las clases «se comuniquen».
- Crear un
enumpara valores abiertos, como el nombre de un cliente. - Usar herencia solo para reutilizar código, aunque no exista una relación «es un».
- Concentrar todas las decisiones en una clase y dejar las demás como simples bolsas de datos.
Practica: curso y estudiante
Enunciado
Modela un Curso que tiene un Estudiante. El estudiante conserva su nombre. El curso conserva su título y delega en el estudiante la obtención de su nombre. Añade un método descripcion().
Criterios
- Los atributos son privados.
- La relación se expresa mediante una referencia, no mediante herencia.
- El constructor de
Cursorecibe el estudiante. - La descripción incluye título y nombre.
Solución
class Estudiante {
private String nombre;
public Estudiante(String nombre) {
this.nombre = nombre;
}
public String getNombre() {
return nombre;
}
}
class Curso {
private String titulo;
private Estudiante estudiante;
public Curso(String titulo, Estudiante estudiante) {
this.titulo = titulo;
this.estudiante = estudiante;
}
public String descripcion() {
return titulo + " - estudiante: " + estudiante.getNombre();
}
}
public class CursosDemo {
public static void main(String[] args) {
Estudiante estudiante = new Estudiante("Leo");
Curso curso = new Curso("Java 2", estudiante);
System.out.println(curso.descripcion());
}
}
Curso coordina la descripción, pero no necesita apropiarse del nombre. Esta separación prepara el terreno para comparar composición y herencia con intención.