Ejercicios de diseño orientado a objetos
Estos ejercicios no buscan una palabra clave aislada. Antes de programar, explica qué objeto posee cada dato, qué reglas protege y por qué eliges composición, herencia o interfaz.
1. Dos referencias, un producto
Enunciado. Crea un producto, asigna su referencia a dos variables y cambia el precio mediante una de ellas.
Criterios. Un solo new, atributo privado y método que rechaza precios negativos.
class Producto {
private double precio;
public Producto(double precio) { this.precio = precio; }
public boolean cambiarPrecio(double precio) {
if (precio < 0) return false;
this.precio = precio; return true;
}
public double getPrecio() { return precio; }
}
Solución razonada. Producto a = new Producto(10); Producto b = a; a.cambiarPrecio(12); hace que b.getPrecio() devuelva 12. Se copiaron referencias, no objetos.
2. Construir una cuenta válida
Enunciado. Diseña Cuenta con titular obligatorio y saldo inicial no negativo.
Criterios. Estado privado, constructor completo y operaciones; no setter de saldo.
class Cuenta {
private String titular; private double saldo;
public Cuenta(String titular, double saldoInicial) {
this.titular = titular; this.saldo = saldoInicial >= 0 ? saldoInicial : 0;
}
public boolean ingresar(double cantidad) {
if (cantidad <= 0) return false;
saldo += cantidad; return true;
}
public boolean retirar(double cantidad) {
if (cantidad <= 0 || cantidad > saldo) return false;
saldo -= cantidad; return true;
}
public double getSaldo() { return saldo; }
}
Decisión. Los métodos nombran operaciones del dominio y mantienen saldo >= 0.
3. Pedido y cliente
Enunciado. Un pedido pertenece a un cliente y pasa de PENDIENTE a CONFIRMADO.
Criterios. Composición, enum y transición protegida.
enum EstadoPedido { PENDIENTE, CONFIRMADO }
class Cliente {
private String nombre;
public Cliente(String nombre) { this.nombre = nombre; }
public String getNombre() { return nombre; }
}
class Pedido {
private Cliente cliente;
private EstadoPedido estado = EstadoPedido.PENDIENTE;
public Pedido(Cliente cliente) { this.cliente = cliente; }
public boolean confirmar() {
if (estado != EstadoPedido.PENDIENTE) return false;
estado = EstadoPedido.CONFIRMADO; return true;
}
public String resumen() { return cliente.getNombre() + ": " + estado; }
}
Decisión. Pedido extends Cliente sería falso: un pedido tiene un cliente.
4. ¿Composición o herencia?
Enunciado. Corrige class Coche extends Motor.
Criterios. Representar «tiene un» y delegar el arranque.
class Motor { public String arrancar() { return "Arrancado"; } }
class Coche {
private Motor motor;
public Coche(Motor motor) { this.motor = motor; }
public String arrancar() { return motor.arrancar(); }
}
Decisión. Heredar permitiría tratar un coche como motor, algo que el dominio no sostiene.
5. Especializar empleados
Enunciado. Empleado calcula una paga base. EmpleadoVentas añade comisión y sobrescribe el cálculo.
Criterios. Relación «es un», super y @Override.
class Empleado {
private double paga;
public Empleado(double paga) { this.paga = paga; }
public double calcularPaga() { return paga; }
}
class EmpleadoVentas extends Empleado {
private double comision;
public EmpleadoVentas(double paga, double comision) { super(paga); this.comision = comision; }
@Override public double calcularPaga() { return super.calcularPaga() + comision; }
}
Decisión. La especialización conserva la promesa de Empleado.
6. Formas polimórficas
Enunciado. Modela figuras que calculan área. No debe crearse una figura sin fórmula.
Criterios. Clase abstracta, dos hijas y recorrido por el tipo común.
abstract class Forma { public abstract double area(); }
class Cuadrado extends Forma {
private double lado;
public Cuadrado(double lado) { this.lado = lado; }
@Override public double area() { return lado * lado; }
}
class Triangulo extends Forma {
private double base; private double altura;
public Triangulo(double base, double altura) { this.base = base; this.altura = altura; }
@Override public double area() { return base * altura / 2; }
}
Solución razonada. Un Forma[] puede guardar ambos objetos y llamar a area() sin comprobar su clase.
7. Elegir interfaz o clase abstracta
Enunciado. Una impresora y una factura pueden imprimir, pero no comparten estado ni forman una familia.
Criterios. Contrato pequeño y dos implementaciones.
interface Imprimible { String imprimir(); }
class Impresora implements Imprimible {
@Override public String imprimir() { return "Página de prueba"; }
}
class Factura implements Imprimible {
@Override public String imprimir() { return "Factura emitida"; }
}
Decisión. Una interfaz expresa la capacidad. Una clase abstracta añadiría estado compartido innecesario.
8. Diagnóstico y refactorización
Enunciado. Una clase Gestor contiene nombre de cliente, saldo, estado del pedido y métodos para todo. Propón una división.
Criterios. Responsabilidades y relaciones explícitas; reglas junto al estado que protegen.
Solución razonada. Cliente conserva su identidad; Cuenta protege el saldo; Pedido compone un cliente y usa EstadoPedido; un servicio coordina operaciones entre objetos. Separar responsabilidades no exige herencia.
Comprobación integrada
public class DisenoDemo {
public static void main(String[] args) {
Cuenta cuenta = new Cuenta("Ana", 50);
System.out.println(cuenta.retirar(60));
System.out.println(cuenta.retirar(20));
Pedido pedido = new Pedido(new Cliente("Leo"));
System.out.println(pedido.confirmar());
System.out.println(pedido.confirmar());
System.out.println(pedido.resumen());
Forma[] formas = { new Cuadrado(2), new Triangulo(4, 3) };
for (Forma forma : formas) System.out.println(forma.area());
}
}
Los casos límite prueban las decisiones: retirada imposible, transición repetida y dos implementaciones bajo un mismo tipo. Ya puedes reunir estas piezas en una aplicación completa.