Ejercicios Prácticos: POO Avanzada en Java
Narrativa del Proyecto: ¡Bienvenido al equipo de desarrollo de Innovatec Solutions! Nuestra empresa está en plena expansión y nuestro sistema de gestión de recursos humanos, basado en hojas de cálculo, se ha quedado obsoleto. Tu misión es liderar el diseño y la implementación del núcleo de nuestro nuevo sistema, "Innovatec HR Core", utilizando los principios más sólidos de la Programación Orientada a Objetos en Java.
Este proyecto no solo gestionará empleados, sino que deberá ser lo suficientemente flexible para adaptarse a nuevos roles, normativas y funcionalidades en el futuro. ¡Es hora de demostrar tus habilidades como arquitecto de software!
Ejercicios de Consolidación
1) La Jerarquía de Innovatec: Creando la Base de Empleados | Nivel: Fácil
Objetivo:
Aplicar el concepto de Herencia para modelar la relación fundamental entre una persona y un empleado en el sistema.
Tarea a realizar:
- Crea una clase
Personacon los atributosprotectednombreynif. Incluye un constructor para inicializarlos y un métodotoString()que devuelva una representación básica de la persona. - Crea una clase
Empleadoque herede dePersona. Empleadodebe añadir un atributoprivatellamadoidEmpleado(unString).- El constructor de
Empleadodebe recibir elnombre,nify elidEmpleado. Asegúrate de llamar correctamente al constructor de la clase padre (Persona) usandosuper(). - Sobrescribe el método
toString()enEmpleadopara que, además de la información de la persona, muestre el ID del empleado. Utilizasuper.toString()para reutilizar la lógica del padre. - Crea una clase
Mainpara probar tu código: instancia un objetoEmpleadoy muestra su información por consola.
Aplicación en el Mundo Real:
Establecer jerarquías de clases es fundamental en cualquier aplicación empresarial. Permite definir entidades generales (como una Persona) y luego especializarlas (Empleado, Cliente, Proveedor) reutilizando código y manteniendo una estructura lógica y ordenada.
2) Organizando la Empresa: Departamentos y sus Miembros | Nivel: Fácil
Objetivo:
Aplicar el concepto de Composición para modelar la relación "tiene un/a" entre un Departamento y los Empleados que lo componen.
Tarea a realizar:
- Crea una clase
Departamento. Esta clase tendrá un atributoprivatenombre(String) y un atributoprivatemiembrosque será unaList<Empleado>. - En el constructor de
Departamento, inicializa el nombre y la lista de miembros (como unArrayListvacío). - Crea un método
agregarMiembro(Empleado empleado)que añada un empleado a la lista. - Crea un método
listarMiembros()que imprima por consola los detalles de todos los empleados del departamento. - En tu clase
Main, crea una instancia deDepartamento(ej: "Ingeniería de Software") y varias instancias deEmpleado. Agrega los empleados al departamento y luego llama alistarMiembros().
Aplicación en el Mundo Real:
La composición es una de las técnicas de diseño más importantes. En lugar de que un Departamento sea un Empleado (lo cual no tiene sentido), un Departamento tiene Empleados. Este modelo es flexible y representa con precisión las relaciones del mundo real en la mayoría de los casos.
3) Identidad Única: El Contrato equals y hashCode | Nivel: Fácil
Objetivo:
Comprender y aplicar correctamente la sobrescritura de los métodos equals(), hashCode() y toString() de la clase Object.
Tarea a realizar:
- Partiendo de la clase
Empleadodel ejercicio 1, modifica su lógica de igualdad. En Innovatec, dos empleados se consideran la misma persona a efectos contractuales si sunifes idéntico. - Sobrescribe el método
equals(Object obj)en la claseEmpleadopara que compare dos empleados basándose únicamente en sunif. - Cumple con el contrato
equals-hashCode: sobrescribe también el métodohashCode()para que genere un código hash basado en elnif. - En tu clase
Main, crea dos objetosEmpleadocon el mismonifpero diferenteidEmpleado(por ejemplo, un recontrato). Comprueba quep1.equals(p2)devuelvetruey quep1 == p2devuelvefalse.
Aplicación en el Mundo Real:
Sobrescribir equals y hashCode es absolutamente crucial cuando se trabaja con colecciones como HashSet o HashMap. Sin una implementación correcta, estas colecciones no funcionarán como se espera, llevando a bugs muy difíciles de detectar. Por ejemplo, podrías acabar con empleados "duplicados" en un conjunto que debería contener solo entradas únicas.
4) Definiendo Roles: El Empleado Abstracto | Nivel: Fácil
Objetivo:
Utilizar clases abstractas para definir un "contrato" común y compartir código entre diferentes tipos de empleados.
Tarea a realizar:
- Convierte la clase
Empleadoen una clase abstracta. No tiene sentido instanciar un "empleado genérico". - Añade un método abstracto a
Empleadollamadopublic abstract double calcularSalario();. Esto obliga a todas las subclases concretas a definir cómo se calcula su salario. - Crea dos clases concretas que hereden de
Empleado:Programador: Añade un atributosalarioBase(double). SucalcularSalario()simplemente devolverá este valor.Manager: Añade un atributosalarioBasey unbonus(double). SucalcularSalario()devolverá la suma de ambos.
- En tu clase
Main, intenta instanciar unEmpleado. El código no debería compilar. Luego, instancia unProgramadory unManagery muestra sus salarios.
Aplicación en el Mundo Real:
Las clases abstractas son perfectas para modelar entidades que comparten una base común pero cuya implementación completa depende de sus "hijos". Un Vehiculo es abstracto, pero un Coche y una Moto son concretos. En RRHH, un Empleado es un concepto abstracto, mientras que los roles específicos son las implementaciones concretas.
5) ¡Bug en Producción! El Cálculo de Nóminas | Nivel: Fácil
Objetivo:
Depurar un fragmento de código que hace un mal uso del polimorfismo, provocando una ClassCastException.
Setup Inicial:
Utiliza las clases Empleado (abstracta), Programador y Manager del ejercicio anterior. Adicionalmente, crea una nueva clase ConsultorExterno que NO hereda de Empleado pero sí de Persona.
public class ConsultorExterno extends Persona {
public ConsultorExterno(String nombre, String nif) {
super(nombre, nif);
}
public void facturar() {
System.out.println("Facturando como consultor externo...");
}
}
Tarea a realizar:
El siguiente código intenta procesar una lista de Personas. El objetivo es calcular el total de salarios de los Empleados y, específicamente para los Managers, llamar a un método aprobarVacaciones(). El código, sin embargo, se rompe.
- Identifica la causa de la
ClassCastException. - Corrige el código usando el operador
instanceofpara asegurar que los castings sean seguros. - Añade un método
aprobarVacaciones()a la claseManagerpara que el código corregido funcione.
Código con Bug:
import java.util.ArrayList;
import java.util.List;
public class ProcesadorNominas {
public void procesar(List<Persona> personas) {
double totalSalarios = 0.0;
for (Persona p : personas) {
// Asumimos que todos son empleados para el cálculo de salario
Empleado emp = (Empleado) p; // <-- ¡Peligro!
totalSalarios += emp.calcularSalario();
// Y si es un Manager, aprobamos vacaciones
Manager mgr = (Manager) p; // <-- ¡Doble peligro!
mgr.aprobarVacaciones();
}
System.out.println("Total salarios: " + totalSalarios);
}
}
Aplicación en el Mundo Real:
Este es uno de los errores más comunes al trabajar con polimorfismo. Tratar a todos los objetos de una colección como si fueran del mismo subtipo es una receta para el desastre. El uso defensivo de instanceof antes de un downcasting es una práctica de programación esencial para crear software robusto.
Ejercicios de Refuerzo
6) El Manager Moderno: Extendiendo Funcionalidad | Nivel: Medio
Objetivo:
Reforzar el uso de super tanto para llamar a constructores como para extender la funcionalidad de métodos heredados.
Tarea a realizar:
- Añade un método
public String getDetalles()a tu claseEmpleado(abstracta). Este método debe devolver unStringcon el nombre y el ID del empleado. - Ahora, en la clase
Manager, sobrescribe el métodogetDetalles(). - La nueva implementación en
Managerdebe ampliar la del padre. Debe llamar al métodogetDetalles()de la claseEmpleadousandosuper.getDetalles()y añadirle información adicional, como por ejemplo, el bonus que gestiona. El resultado final debería ser algo como:"Nombre: Elena Soler, ID: MGR-01, Gestiona un bonus de: 10000.0". -
Crea un
Mainpara instanciar unManagery unProgramadory llama al métodogetDetalles()en ambos para ver la diferencia. -
Pista: Recuerda que
super.metodo()te permite invocar la versión del método de la clase padre desde la clase hija, ideal para no repetir código.
Aplicación en el Mundo Real:
Esta técnica es extremadamente común. Permite construir funcionalidades complejas de forma incremental. Una clase base proporciona una funcionalidad por defecto, y las clases hijas la refinan o la expanden sin tener que reescribir la lógica original.
7) Habilidades Transversales: La Interfaz Notificable | Nivel: Medio
Objetivo:
Diseñar y utilizar una interfaz para dotar de una capacidad ("puede hacer") a clases que no tienen una relación de herencia directa.
Tarea a realizar:
- Crea una interfaz llamada
Notificableque defina un único método:void enviarNotificacion(String mensaje). - En Innovatec, tanto los
Managers como losConsultorExternos deben poder recibir notificaciones, pero losProgramadores no. - Haz que las clases
ManageryConsultorExternoimplementen la interfazNotificable. Proporciona una implementación simple para el método, por ejemplo, que imprima en consola a quién se notifica y el mensaje. - Crea una clase
SistemaDeNotificacionescon un métodoenviarNotificacionMasiva(String mensaje, List<Object> destinatarios). - Dentro de este método, itera sobre la lista de
destinatarios. Usandoinstanceof, comprueba si cada objeto esNotificabley, si lo es, haz el casting y llama a su métodoenviarNotificacion. -
Prueba tu sistema enviando una notificación a una lista que contenga un
Manager, unProgramadory unConsultorExterno. -
Pista: Piensa en la interfaz como un "contrato". Cualquier clase que lo firme (
implements) está obligada a tener el comportamiento definido en él, sin importar de quién herede.
Aplicación en el Mundo Real:
Las interfaces son la base del diseño de software flexible en Java. Permiten que sistemas como los gestores de eventos (ActionListener), los comparadores (Comparable, Comparator) o los frameworks de Inyección de Dependencias funcionen, conectando componentes que no necesitan conocer los detalles internos de los demás, solo el "contrato" que cumplen.
8) ¿Herencia o Composición? El Dilema del 'Jefe de Equipo' | Nivel: Medio
Objetivo:
Analizar un problema de diseño y justificar por qué la composición es a menudo preferible a la herencia.
Tarea a realizar:
En Innovatec, cualquier Programador puede, temporalmente, asumir el rol de JefeDeEquipo. Este rol le da una nueva responsabilidad: revisarCodigo().
Un enfoque inicial podría ser crear una clase JefeDeEquipo que herede de Programador. Sin embargo, esto es rígido. ¿Qué pasa si un día un DiseñadorUX también puede ser JefeDeEquipo? No podría heredar de Programador.
Tu tarea es modelar esta relación usando composición, siguiendo el principio de "Favorecer la composición sobre la herencia".
- Crea una interfaz
Rolcon un métododesempenar(). - Crea una clase
RolJefeDeEquipoque implementeRol. Su métododesempenar()imprimirá "Revisando código del equipo...". - Modifica la clase
Programadorpara que tenga unRol. Añade un atributoprivate Rol rolActual;. - Añade métodos en
ProgramadorparaasignarRol(Rol nuevoRol)yrealizarRol(). El segundo método comprobará si el rol no es nulo y llamará arolActual.desempenar(). -
En
Main, crea unProgramador. Inicialmente no tendrá rol. Luego, asígnale elRolJefeDeEquipoy haz que lo desempeñe. -
Pista: Con este diseño, podrías crear un
RolMentor,RolFormador, etc., y asignarlos dinámicamente a cualquierEmpleadosin cambiar la jerarquía de clases.
Aplicación en el Mundo Real:
Este es un ejemplo clásico del Patrón de Diseño "Strategy" o "State". Permite cambiar el comportamiento de un objeto en tiempo de ejecución. Es mucho más flexible que la herencia, que fija el comportamiento en tiempo de compilación. Se usa en sistemas de videojuegos (cambiar el estado de un personaje), aplicaciones financieras (cambiar la estrategia de cálculo de impuestos), etc.
9) La Magia del Polimorfismo: La Calculadora de Nóminas | Nivel: Medio
Objetivo:
Aplicar el polimorfismo para procesar una colección de objetos de diferentes tipos de forma uniforme.
Tarea a realizar:
- Asegúrate de tener tu jerarquía de
Empleado(abstracta) con las subclasesProgramadoryManager, cada una con su propia implementación decalcularSalario(). - Crea una clase
CalculadoraNominascon un métodocalcularTotalSalarios(List<Empleado> empleados). - Este método debe iterar sobre la lista de
Empleados. Por cadaempleadoen la lista, debe invocar su métodocalcularSalario()y sumar el resultado a un total. - Fíjate en que el método no necesita saber si el objeto es un
Programadoro unManager. Simplemente confía en que, por ser unEmpleado, tendrá un métodocalcularSalario()disponible. Esta es la esencia del polimorfismo. -
En tu
Main, crea unaList<Empleado>, añade varias instancias deProgramadoryManager, y pasa la lista a tuCalculadoraNominaspara obtener el coste total de la plantilla. -
Pista: La variable del bucle
forserá de tipoEmpleado, pero en tiempo de ejecución, Java sabrá a qué objeto real apunta (unProgramadoro unManager) y llamará a la versión correcta (@Override) del métodocalcularSalario().
Aplicación en el Mundo Real:
El polimorfismo es la clave para escribir código limpio, mantenible y extensible. Si mañana Innovatec crea un nuevo rol DiseñadorUX que hereda de Empleado, la CalculadoraNominas funcionará con él sin necesidad de modificar una sola línea de su código. Esto se conoce como el Principio Abierto/Cerrado (abierto a la extensión, cerrado a la modificación).
Ejercicios de Ampliación
10) Interfaces Evolucionadas: Métodos default | Nivel: Alto
Objetivo:
Investigar y aplicar una característica avanzada de las interfaces en Java (a partir de la versión 8): los métodos default.
Tarea a realizar:
- Investiga: ¿Qué es un método
defaulten una interfaz de Java? ¿Para qué sirve y qué problema soluciona? - Vamos a mejorar nuestra interfaz
Notificable. A veces, queremos enviar una notificación con una prioridad. - Añade un nuevo método a la interfaz
Notificable:void enviarNotificacionUrgente(String mensaje). - En lugar de obligar a todas las clases existentes (
Manager,ConsultorExterno) a implementar este nuevo método (lo que rompería el código existente), impleméntalo como un métododefault. - La implementación por defecto de
enviarNotificacionUrgentellamará al método abstractoenviarNotificacionpero añadiendo un prefijo como "[URGENTE] ". - En tu clase
Main, toma una instancia deManager(que esNotificable) y llama al nuevo métodoenviarNotificacionUrgente(). Verás que funciona sin que hayas tenido que añadir ningún código a la claseManager. - Extra: Sobrescribe el método
defaulten la claseConsultorExternopara darle un comportamiento personalizado (ej: "Enviando SMS URGENTE a...").
Aplicación en el Mundo Real:
Los métodos default fueron una adición crucial a Java. Permiten a los diseñadores de APIs (como las de la propia librería estándar de Java) añadir nueva funcionalidad a interfaces existentes sin romper la compatibilidad con todo el código que ya las utilizaba. Un ejemplo famoso fue la adición del método forEach() a la interfaz Iterable.
11) Refactorización a Patrones: El Patrón Strategy | Nivel: Alto
Objetivo:
Refactorizar un código problemático (usando if-else anidados) aplicando el Patrón de Diseño Strategy. Este patrón es una manifestación perfecta de "favorecer la composición sobre la herencia" y el polimorfismo.
Setup Inicial (Código a refactorizar):
En la clase Manager, el cálculo del bonus se ha vuelto complejo. Actualmente, el código es así y es difícil de mantener:
public class Manager extends Empleado {
// ... atributos ...
private String nivelRendimiento; // "BUENO", "EXCELENTE", "A MEJORAR"
// ... constructor ...
// MÉTODO A REFACTORIZAR
public double calcularBonus() {
if ("EXCELENTE".equals(nivelRendimiento)) {
return (salarioBase + bonus) * 0.20;
} else if ("BUENO".equals(nivelRendimiento)) {
return (salarioBase + bonus) * 0.10;
} else if ("A MEJORAR".equals(nivelRendimiento)) {
return bonus * 0.5; // Solo una parte del bonus base
} else {
return 0.0;
}
}
// ... otros métodos ...
}
Tarea a realizar:
- Crea una interfaz
EstrategiaDeCalculoBonus. Debe tener un métododouble calcular(double salarioBase, double bonus). - Crea tres clases que implementen esta interfaz:
BonusExcelente,BonusBuenoyBonusAMejorar. Cada una contendrá la lógica de cálculo que estaba en elif-else. - Modifica la clase
Manager. Elimina el atributonivelRendimientoy el métodocalcularBonus(). - En su lugar, añade un atributo
private EstrategiaDeCalculoBonus estrategiaBonus;(¡Composición!). - Modifica el constructor del
Managerpara que reciba unaEstrategiaDeCalculoBonusy la asigne. - Crea un nuevo método
public double obtenerBonusFinal()enManagerque delegue el cálculo a su objeto de estrategia:return estrategiaBonus.calcular(this.salarioBase, this.bonus);. - En
Main, crea variosManagers, cada uno con una estrategia diferente, y comprueba que sus bonus se calculan correctamente.
Aplicación en el Mundo Real:
El Patrón Strategy es uno de los patrones de comportamiento más importantes. Se usa para permitir que el algoritmo o la lógica de una operación varíe independientemente de los clientes que lo utilizan. Lo encuentras en sistemas de validación de formularios, estrategias de compresión de archivos, algoritmos de renderizado en videojuegos, etc.
12) Diseño y Revisión por Pares: El Módulo de Fichaje | Nivel: Alto
Objetivo:
Diseñar una solución de software a pequeña escala, documentarla con un diagrama de clases y realizar una revisión por pares (Peer Review) para mejorar el diseño.
Tarea a realizar:
Innovatec necesita un nuevo módulo para el control horario. Los requisitos iniciales son:
* Los empleados deben poder "fichar" al entrar y al salir.
* Existen diferentes tipos de contrato con distintas políticas de fichaje (ej: ContratoFlexible, ContratoEstricto).
* El sistema debe poder generar un reporte de horas trabajadas para un empleado en un periodo.
Parte 1: Tu Diseño (Individual)
1. Diseña la arquitectura de clases para este módulo. Piensa en qué clases necesitas (RegistroFichaje, PoliticaDeFichaje, etc.) y cómo se relacionan.
2. Usa los conceptos de herencia, composición, clases abstractas e interfaces donde creas que aportan más valor.
3. Crea un diagrama de clases en Mermaid que represente tu diseño.
4. Escribe una breve justificación (2-3 párrafos) explicando por qué tomaste ciertas decisiones de diseño (ej: "He usado una interfaz PoliticaDeFichaje para poder añadir nuevas políticas en el futuro sin cambiar el código de Empleado...").
Parte 2: Revisión por Pares (En parejas)
1. Intercambia tu diagrama y justificación con un compañero.
2. Analiza su diseño y proporciona feedback constructivo por escrito sobre al menos dos puntos. El feedback debe ser profesional y razonado. Ejemplos de buen feedback:
* "Tu uso de la herencia en la clase X es interesante, pero ¿has considerado si la composición podría hacerlo más flexible para el requisito Y?"
* "Me gusta mucho cómo has definido la interfaz Z. Creo que simplifica enormemente la lógica. Una posible mejora sería añadirle un método validarFichaje() para encapsular esa lógica."
3. Recibe el feedback de tu compañero y reflexiona sobre cómo podrías mejorar tu diseño inicial.
Aplicación en el Mundo Real:
El diseño de software y la revisión de código (Code Review) son dos de las actividades más importantes en un equipo de desarrollo profesional. Permiten compartir conocimiento, mejorar la calidad del software, detectar errores de diseño antes de que se escriba una sola línea de código y fomentar una cultura de colaboración y mejora continua.