Skip to content

Ejercicios Prácticos: Programación Modular

Narrativa del Proyecto: Módulos para "HR-Suite" de DevSolutions Inc.

¡Bienvenido al equipo de desarrollo de DevSolutions Inc.! Nuestra empresa está creciendo y necesitamos construir una nueva suite de herramientas internas de Recursos Humanos llamada "HR-Suite". Olvídate de las hojas de cálculo y los procesos manuales; vamos a automatizar y modernizar la gestión de empleados.

Tu misión, como nuevo desarrollador Java, es construir los "ladrillos de LEGO" fundamentales de esta aplicación. Cada ejercicio te encargará la creación de un módulo (un método) específico que resolverá un pequeño problema del dominio de RRHH. A través de estos ejercicios, no solo aplicarás los conceptos de programación modular, sino que construirás piezas de código reutilizables y robustas, listas para ser integradas en un proyecto de software real. ¡Manos al código!


Ejercicios de Consolidación

1) El Glosario del Desarrollador de HR-Suite | Nivel: Fácil

Objetivo:

Asegurar la comprensión de los conceptos teóricos fundamentales de la programación modular antes de escribir código.

Tarea a realizar:

Responde a las siguientes preguntas como si estuvieras preparando unas notas para un nuevo integrante del equipo de DevSolutions.

  1. En el contexto de "HR-Suite", vamos a tener un módulo para calculatePayroll() que devuelve el total a pagar, y otro para printPayslip() que solo muestra la nómina en consola. ¿Cuál de los dos sería una función y cuál un procedimiento? Justifica tu respuesta.
  2. Si llamamos al método calculatePayroll(employeeId, month, year), ¿qué son employeeId, month y year en la definición del método? ¿Y qué serían los valores 101, "Enero" y 2024 cuando lo invocamos?
  3. ¿Qué demuestra el Teorema del Programa Estructurado y por qué fue tan importante para abandonar prácticas como el GOTO?
  4. Define con tus propias palabras qué es la programación modular y por qué es esencial para un proyecto como "HR-Suite", donde trabajan varios desarrolladores.
Aplicación en el Mundo Real:

Utilizar la terminología correcta es clave en un entorno profesional para comunicarse de manera efectiva con otros desarrolladores, arquitectos de software y jefes de proyecto.

2) Módulo de Bienvenida al Empleado | Nivel: Fácil

Objetivo:

Crear un procedimiento simple (void) que acepte parámetros para realizar una acción.

Tarea a realizar:

Dentro de la clase Onboarding, crea un procedimiento estático llamado displayWelcomeMessage.
1. Debe aceptar dos parámetros: un String para el nombre del empleado (employeeName) y un String para su departamento (department).
2. El método debe imprimir un mensaje de bienvenida formateado en la consola.

Ejemplo de salida para displayWelcomeMessage("Ana García", "Ingeniería de Software"):

========================================
¡Bienvenida a DevSolutions Inc., Ana García!
Estamos encantados de tenerte en el departamento de Ingeniería de Software.
========================================

Aplicación en el Mundo Real:

Los procedimientos son perfectos para tareas que no necesitan devolver un valor, como mostrar información en la interfaz de usuario, registrar eventos en un log o enviar correos electrónicos.

3) Calculadora de Antigüedad | Nivel: Fácil

Objetivo:

Crear una función simple que realice un cálculo y devuelva un resultado.

Tarea a realizar:

En la clase EmployeeTools, crea una función estática llamada calculateYearsOfService.
1. Debe aceptar un parámetro: un int para el año en que el empleado comenzó a trabajar (startYear).
2. La función debe calcular cuántos años lleva el empleado en la empresa y devolver el resultado como un int. Asume que el año actual es 2025 para este ejercicio.
3. En el main, llama a esta función con un año de ejemplo y muestra el resultado.

Aplicación en el Mundo Real:

Las funciones son la base de cualquier programa. Se usan constantemente para calcular valores, validar datos, procesar información y devolver un resultado que otras partes del programa pueden utilizar.

4) Bug en el Módulo de Reportes | Nivel: Fácil

Objetivo:

Identificar y corregir un error común relacionado con el ámbito (scope) de las variables.

Setup Inicial:

Copia el siguiente código en un fichero ReportGenerator.java. Verás que no compila.

public class ReportGenerator {

    public static final String COMPANY_NAME = "DevSolutions Inc.";

    public static void generatePerformanceReport(String employeeName, double performanceScore) {
        System.out.println("Generando reporte para: " + employeeName);

        if (performanceScore > 4.5) {
            String performanceLevel = "Excelente";
            System.out.println("Nivel de Rendimiento: " + performanceLevel);
        } else if (performanceScore > 3.5) {
            String performanceLevel = "Bueno";
            System.out.println("Nivel de Rendimiento: " + performanceLevel);
        }

        // Esta línea da un error de compilación.
        String reportFooter = "Reporte generado por " + COMPANY_NAME + " para el nivel: " + performanceLevel;
        System.out.println(reportFooter);
    }

    public static void main(String[] args) {
        generatePerformanceReport("Laura Vidal", 4.8);
    }
}
Tarea a realizar:
  1. Identifica el error: ¿Por qué la línea que crea reportFooter no compila? Explica el problema de ámbito en una o dos frases.
  2. Corrige el código: Modifica el método generatePerformanceReport para que funcione correctamente. La variable performanceLevel debe ser accesible al final del método.
Aplicación en el Mundo Real:

Entender el ámbito de las variables es fundamental para evitar errores y escribir código limpio. Un error de scope puede ser frustrante de encontrar si no se comprenden bien las reglas de visibilidad.

5) El Experimento de la Bonificación | Nivel: Fácil

Objetivo:

Comprender la diferencia fundamental entre el paso por valor para tipos primitivos y para objetos (copia de la referencia).

Tarea a realizar:

Observa el siguiente código y, sin ejecutarlo, predice cuál será la salida por consola. Justifica tu predicción para cada una de las dos partes (la primitiva y la del objeto).

public class BonusExperiment {

    // Intenta modificar un tipo primitivo (int)
    public static void calculateBonus(int baseBonus) {
        baseBonus = baseBonus * 2;
        System.out.println("  -> Dentro del método, la bonificación es: " + baseBonus);
    }

    // Modifica el estado de un objeto (array)
    public static void addExtraBonusToTeam(double[] teamBonuses) {
        teamBonuses[0] = teamBonuses[0] + 500.0;
        System.out.println("  -> Dentro del método, la bonificación del líder es: " + teamBonuses[0]);
    }

    public static void main(String[] args) {
        // --- Parte 1: Primitivos ---
        int individualBonus = 1000;
        System.out.println("Bonificación individual ANTES de llamar al método: " + individualBonus);
        calculateBonus(individualBonus);
        System.out.println("Bonificación individual DESPUÉS de llamar al método: " + individualBonus);

        System.out.println("\n-----------------------------------------\n");

        // --- Parte 2: Objetos ---
        double[] engineeringTeamBonuses = {3000.0, 2000.0, 1800.0}; // [Líder, Senior, Junior]
        System.out.println("Bonificación del líder ANTES de llamar al método: " + engineeringTeamBonuses[0]);
        addExtraBonusToTeam(engineeringTeamBonuses);
        System.out.println("Bonificación del líder DESPUÉS de llamar al método: " + engineeringTeamBonuses[0]);
    }
}
Aplicación en el Mundo Real:

Este concepto es una de las principales fuentes de bugs para los programadores que empiezan. Entenderlo te ahorrará horas de depuración al saber si un método puede o no modificar los datos que le pasas.


Ejercicios de Refuerzo

6) Calculadora de Bonificación Avanzada | Nivel: Medio

Objetivo:

Implementar una función con lógica condicional más elaborada, manejando múltiples casos y devolviendo un valor.

Tarea a realizar:

En el módulo de RRHH, es necesario calcular la bonificación anual de un empleado. Crea una función estática calculateAnnualBonus en una clase Compensation.
1. La función debe recibir dos parámetros: double baseSalary y int yearsOfService.
2. La bonificación se calcula según estas reglas:
* Si yearsOfService es menor a 2, la bonificación es del 5% del baseSalary.
* Si yearsOfService está entre 2 y 5 (inclusive), la bonificación es del 8% del baseSalary.
* Si yearsOfService es mayor a 5, la bonificación es del 10% del baseSalary.
* Regla adicional: Si el salario base es superior a 80,000€, se añade un 2% extra a la bonificación final, independientemente de la antigüedad.
3. La función debe devolver el monto final de la bonificación como un double.
4. Prueba la función en el main con al menos tres casos distintos que cubran todas las reglas.

  • Pista: Calcula primero el porcentaje de bonificación base según la antigüedad y luego, si aplica, añade el porcentaje extra.
Aplicación en el Mundo Real:

Las funciones que encapsulan reglas de negocio complejas son extremadamente comunes. Un módulo como este permite que la lógica de cálculo de bonificaciones se mantenga en un solo lugar, facilitando su actualización si las políticas de la empresa cambian.

7) Módulo de Logging con Sobrecarga | Nivel: Medio

Objetivo:

Practicar la sobrecarga de métodos para ofrecer diferentes maneras de invocar una misma acción, aumentando la flexibilidad del módulo.

Tarea a realizar:

En la clase SystemLogger de "HR-Suite", necesitamos un método para registrar eventos, pero a veces solo tendremos el mensaje, y otras veces tendremos más detalles.
1. Crea tres métodos estáticos void llamados logEvent, todos en la misma clase:
* logEvent(String message): Imprime el mensaje con un nivel por defecto "INFO" y la fecha/hora actual.
* logEvent(String message, String level): Imprime el mensaje con el level especificado y la fecha/hora.
* logEvent(String message, String level, int employeeId): Imprime el mensaje, el level, el ID del empleado y la fecha/hora.
2. Para evitar repetir código, haz que las versiones más simples llamen a las más complejas con valores por defecto. Por ejemplo, logEvent(message) podría llamar a logEvent(message, "INFO").
3. En el main, demuestra el uso de las tres versiones del método.

  • Pista: Puedes usar java.time.LocalDateTime.now() para obtener la marca de tiempo actual.
Aplicación en el Mundo Real:

La sobrecarga de métodos es muy común en las librerías y APIs de Java. Por ejemplo, los constructores de muchas clases están sobrecargados para permitir crear objetos de diferentes maneras. Esto hace que el código sea más intuitivo y fácil de usar para otros programadores.

8) Gestión de Estado de Empleados | Nivel: Medio

Objetivo:

Reforzar el concepto de paso por copia de referencia modificando el estado de un objeto a través de un método.

Setup Inicial:

Crea una clase simple Employee con los siguientes atributos y métodos:
* Atributos: String name y boolean isActive.
* Constructor: public Employee(String name) que inicialice name y ponga isActive a true por defecto.
* Métodos "getter" y "setter" para isActive: public boolean isActive() y public void setActive(boolean active).
* Un método toString() para mostrar el estado del empleado.

Tarea a realizar:
  1. Crea una clase EmployeeManagement con un procedimiento estático deactivateEmployee(Employee employee).
  2. Este método debe recibir un objeto Employee y cambiar su estado a inactivo llamando al método setActive(false).
  3. En el main de EmployeeManagement:
    • Crea una instancia de Employee.
    • Muestra su estado inicial (debería ser activo).
    • Llama al método deactivateEmployee, pasándole el objeto que has creado.
    • Vuelve a mostrar su estado (debería haber cambiado a inactivo), demostrando que el objeto original fue modificado.
Aplicación en el Mundo Real:

Esta es la forma más común de trabajar con objetos en programación. Rara vez pasas objetos a métodos para que no hagan nada con ellos. Normalmente, los métodos interactúan y modifican el estado de los objetos para cumplir con una tarea, como desactivar un usuario, actualizar un producto o guardar cambios en una base de datos.

9) Secuencia de Fibonacci Recursiva | Nivel: Medio

Objetivo:

Implementar una solución recursiva a un problema clásico, identificando el caso base y la llamada recursiva.

Tarea a realizar:

La secuencia de Fibonacci es una serie de números donde cada número es la suma de los dos anteriores: 0, 1, 1, 2, 3, 5, 8, ....
1. Crea una clase MathUtils con una función estática recursiva fibonacci(int n).
2. La función debe devolver el n-ésimo número de la secuencia de Fibonacci.
3. Lógica recursiva:
* fibonacci(n) es fibonacci(n-1) + fibonacci(n-2).
* Casos Base:
* Si n es 0, el resultado es 0.
* Si n es 1, el resultado es 1.
4. En el main, prueba la función pidiendo el 7º número de la secuencia (que debería ser 13).

  • Pista: Un método recursivo necesita una condición de parada (el caso base) para no llamarse a sí mismo infinitamente. En este caso, tienes dos.
Aplicación en el Mundo Real:

Aunque la recursividad no es la solución más eficiente para todos los problemas (a veces un bucle es mejor), es una herramienta mental muy poderosa. Es perfecta para problemas que pueden ser definidos en términos de sí mismos, como procesar estructuras de datos en árbol (sistemas de ficheros, organigramas) o en algoritmos de "divide y vencerás".


Ejercicios de Ampliación

10) Módulo Validador de Contraseñas | Nivel: Alto

Objetivo:

Diseñar y construir un pequeño módulo autocontenido que combine una función (validación) y un procedimiento (generación y log), aplicando el principio de responsabilidad única.

Setup Inicial:

Necesitarás generar una contraseña aleatoria. Investiga cómo usar java.security.SecureRandom para generar números aleatorios seguros y cómo construir un String a partir de caracteres seleccionados.

Tarea a realizar:

Crea una clase PasswordManager para "HR-Suite".
1. Función de Validación: Crea una función estática public static boolean isPasswordSecure(String password) que verifique si una contraseña cumple con las siguientes políticas de DevSolutions:
* Mínimo 8 caracteres de longitud.
* Contiene al menos una letra mayúscula.
* Contiene al menos una letra minúscula.
* Contiene al menos un número.
* Contiene al menos un carácter especial (ej: !@#$%^&*()).
La función debe devolver true si cumple todas las reglas, y false en caso contrario.

  1. Procedimiento de Generación: Crea un procedimiento estático public static void generateAndAssignPassword(String employeeName) que:
    • Genere una contraseña aleatoria de 12 caracteres que cumpla siempre con las políticas de seguridad.
    • Llame a un método de logging (puedes crearlo o reutilizar el del ejercicio 7) para registrar un evento como: INFO: Nueva contraseña generada para el empleado: Ana Reyes. No debe imprimir la contraseña por seguridad.
    • Imprima en consola la contraseña generada (para este ejercicio, sí la mostraremos para poder verificarla).
Aplicación en el Mundo Real:

En los sistemas reales, la gestión de la seguridad es crucial. Un módulo como este centraliza toda la lógica relacionada con las contraseñas. Si en el futuro la política de seguridad cambia (ej: se requieren 10 caracteres), solo tienes que modificar este módulo en un único lugar, y el cambio se aplicará en toda la aplicación.

11) Refactorización: Del Caos del main a la Claridad Modular | Nivel: Alto

Objetivo:

Aplicar la filosofía "Divide y Vencerás" refactorizando un método monolítico en varios métodos pequeños con responsabilidades únicas.

Setup Inicial:

El siguiente código en un único método main simula el proceso de dar de alta a un nuevo empleado. Es difícil de leer, de probar y de mantener.

import java.util.Random;

public class MonolithicOnboarding {
    public static void main(String[] args) {
        // Datos del nuevo empleado
        String firstName = "Carlos";
        String lastName = "Ramírez";
        double baseSalary = 45000.0;
        String department = "Ventas";

        // --- 1. Generar nombre de usuario ---
        System.out.println("Generando credenciales...");
        String firstInitial = firstName.substring(0, 1).toLowerCase();
        String lastNameShort = lastName.toLowerCase();
        if (lastNameShort.length() > 6) {
            lastNameShort = lastNameShort.substring(0, 6);
        }
        int randomNumber = new Random().nextInt(900) + 100;
        String username = firstInitial + lastNameShort + randomNumber;
        System.out.println("Username creado: " + username);

        // --- 2. Calcular salario con bonificación inicial ---
        System.out.println("Calculando paquete salarial...");
        double initialBonus = 0.0;
        if (department.equals("Ventas")) {
            initialBonus = 2000.0;
        } else if (department.equals("Tecnología")) {
            initialBonus = 3500.0;
        }
        double finalSalary = baseSalary + initialBonus;
        System.out.println("Salario final con bonificación: " + finalSalary + "€");

        // --- 3. Enviar correo de bienvenida ---
        System.out.println("Enviando notificación de bienvenida...");
        String emailBody = "Estimado/a " + firstName + ",\n\n" +
                           "Te damos la bienvenida a DevSolutions en el departamento de " + department + ".\n" +
                           "Tus credenciales de acceso son: " + username + "\n" +
                           "Tu paquete salarial inicial es de " + finalSalary + "€.\n\n" +
                           "Un saludo,\nRecursos Humanos.";
        System.out.println("--- INICIO CORREO ---");
        System.out.println(emailBody);
        System.out.println("--- FIN CORREO ---");

        System.out.println("\nProceso de alta para " + firstName + " " + lastName + " completado.");
    }
}
Tarea a realizar:
  1. Crea una nueva clase ModularOnboarding.
  2. Refactoriza el código del main de MonolithicOnboarding dividiéndolo en al menos tres métodos estáticos y bien definidos. Sugerencias:
    • generateUsername(String firstName, String lastName): Una función que devuelve el String del nombre de usuario.
    • calculateInitialSalary(double baseSalary, String department): Una función que devuelve el salario final como double.
    • sendWelcomeEmail(String firstName, String username, String department, double finalSalary): Un procedimiento que imprime el correo.
  3. El nuevo método main de ModularOnboarding debe ser muy limpio y legible. Simplemente debe orquestar las llamadas a los nuevos métodos que has creado.
Aplicación en el Mundo Real:

La refactorización es una tarea diaria para un desarrollador. Constantemente estamos mejorando código existente para hacerlo más mantenible, legible y eficiente. Saber identificar bloques de código que pueden ser extraídos a sus propios métodos es una habilidad fundamental.

12) Reto Recursivo: Coste Total de un Departamento | Nivel: Alto

Objetivo:

Resolver un problema complejo utilizando recursividad sobre una estructura de objetos jerárquica, un caso de uso perfecto para esta técnica.

Setup Inicial:

Crea una clase Employee que pueda representar una jerarquía.
* Atributos: String name, double salary, y List<Employee> subordinates (una lista de sus subordinados directos).
* Constructor: public Employee(String name, double salary) que inicialice el nombre, el salario y una lista de subordinados vacía.
* Método: public void addSubordinate(Employee employee) para añadir un empleado a su lista de subordinados.

Tarea a realizar:
  1. Crea una clase OrgChart con una función estática y recursiva: public static double getTotalSalary(Employee manager).
  2. Esta función debe calcular la suma del salario del manager más la suma de los salarios de todos sus subordinados, directos e indirectos.
  3. Lógica Recursiva:
    • El coste total de un manager es su propio salario más el coste total de cada uno de sus subordinados directos.
    • Caso Base: Un empleado sin subordinados (subordinates está vacía). Su coste total es simplemente su propio salario.
  4. En el main, construye un organigrama de ejemplo (un jefe con varios managers, y esos managers con sus propios equipos) y llama a getTotalSalary para calcular el coste total del CEO.
Tarea de Revisión por Pares (Peer Review):

Intercambia tu solución con un compañero. Revisa su código y responde a estas preguntas: 1. Correctitud: ¿La función calcula correctamente el total para un organigrama complejo? Prueba a añadir un nivel más de jerarquía a su estructura. 2. Claridad del Caso Base: ¿Está el caso base claramente identificado y es correcto? ¿Qué pasaría si se le pasa un Employee que es null? ¿Lo maneja el código? 3. Legibilidad: ¿El código es fácil de entender? ¿Podrían mejorarse los nombres de las variables?

Aplicación en el Mundo Real:

Calcular el coste de un departamento, renderizar un árbol de componentes en una interfaz gráfica (como en React), o recorrer un sistema de archivos para encontrar un fichero son problemas del mundo real que se resuelven de forma muy elegante y natural con recursividad.