Skip to content

Ejercicios Prácticos: La POO en "TechEvents Platform"

¡Hola, futuro arquitecto de software!

Bienvenidos a la fase práctica de la Programación Orientada a Objetos. En este conjunto de ejercicios, vamos a sumergirnos en el desarrollo de "TechEvents Platform", una aplicación de gestión de eventos tecnológicos. Imagina que somos un equipo de desarrollo que necesita diseñar y construir los cimientos de esta plataforma, utilizando los principios de la POO que acabamos de explorar.

El objetivo es modelar entidades del mundo real (como Evento, Participante, Ponente, Ubicacion) como clases en Java, definir sus atributos, sus comportamientos y cómo interactúan entre sí. Esta es la base de cualquier sistema robusto y escalable. Prepárense para aplicar conceptos como la encapsulación, los constructores, los getters, setters y los miembros estáticos para construir un sistema coherente y funcional.

Cada ejercicio les desafiará a pensar como diseñadores y programadores, pasando de la especificación a la implementación en código Java. ¡Manos a la obra!


Ejercicios de Consolidación

1) Diseñando la Plataforma: Diagrama de Clases UML para 'TechEvents' | Nivel: Fácil

Objetivo: Representar gráficamente las entidades principales de la plataforma 'TechEvents Platform' y sus relaciones utilizando un diagrama de clases UML.
Tarea a realizar:

Basándote en la descripción de una plataforma de gestión de eventos, identifica las principales clases (entidades), sus atributos más relevantes (al menos 3 por clase) y sus métodos clave (al menos 2 por clase, incluyendo constructores o getters/setters si los consideras esenciales).

Considera las siguientes entidades iniciales:
* Evento: Un evento tiene un título, una fecha, una descripción, una capacidad máxima de participantes y un precio.
* Participante: Cada participante tiene un nombre, apellidos, email, y puede registrarse en un evento.
* Ubicacion: Un evento se celebra en una ubicación con un nombre, dirección y capacidad física.

Crea un diagrama de clases UML (puedes usar la sintaxis de Mermaid.js vista en los materiales) que muestre:
1. Las tres clases (Evento, Participante, Ubicacion).
2. Los atributos (private) de cada clase con sus tipos de datos.
3. Los métodos (public) de cada clase (al menos 2 por clase, los constructores cuentan como método).
4. Las relaciones entre estas clases (por ejemplo, un Evento tiene una Ubicacion, un Participante se registra en un Evento). Indica el tipo de relación (asociación, agregación, composición) y su cardinalidad (1 a muchos, 1 a 1, etc.).

Aplicación en el Mundo Real:

Diseñar diagramas UML es una habilidad fundamental en el desarrollo de software. Permite a los equipos visualizar la arquitectura de la aplicación, discutir el diseño antes de escribir código y asegurar que todos los miembros tienen una comprensión común de las entidades del sistema. Es una herramienta clave para la comunicación técnica y la documentación.

2) Creación de la Clase Evento: El Alma de la Plataforma | Nivel: Fácil

Objetivo: Implementar la clase Evento en Java, aplicando los conceptos de atributos, constructores y encapsulación.
Setup Inicial:

Asegúrate de tener un entorno de desarrollo Java (IDE como IntelliJ IDEA o VS Code con el JDK 21).

Tarea a realizar:

Crea una nueva clase Java llamada Evento. Esta clase debe cumplir con los siguientes requisitos:

  1. Atributos:

    • titulo (String): El nombre del evento.
    • fecha (String): La fecha del evento (por simplicidad, usa String por ahora, aunque en un futuro usaríamos LocalDate).
    • descripcion (String): Una breve descripción del evento.
    • capacidadMaxima (int): El número máximo de participantes.
    • precio (double): El coste de la entrada al evento.
    • Todos los atributos deben ser private para asegurar la encapsulación.
  2. Constructor:

    • Un constructor público que reciba titulo, fecha, descripcion, capacidadMaxima y precio como parámetros.
    • El constructor debe inicializar todos los atributos de la clase con los valores proporcionados. Usa this para desambiguar si los nombres de los parámetros coinciden con los de los atributos.
  3. Métodos (Getters):

    • Implementa métodos public get para cada uno de los atributos, permitiendo consultar sus valores desde fuera de la clase. Ejemplo: public String getTitulo().
  4. Método toString():

    • Sobreescribe el método toString() para que devuelva una representación en String del evento, incluyendo todos sus atributos de forma legible. Ejemplo: "Evento: [Título], Fecha: [Fecha], Precio: [Precio]€".
  5. Clase Main para Pruebas:

    • Crea una clase Main (o AppEventos) con un método main.
    • Dentro de main, crea al menos dos objetos Evento diferentes, utilizando el constructor que has definido.
    • Imprime los detalles de cada evento utilizando su método toString().
    • Accede a al menos un atributo de cada evento usando su getter e imprímelo por separado.
Aplicación en el Mundo Real:

La creación de clases es el primer paso en la programación orientada a objetos. Encapsular la lógica de una entidad en una clase bien definida mejora la modularidad, facilita el mantenimiento y permite reutilizar el código en diferentes partes de la aplicación.

3) Mejorando la Gestión de Stock: Producto con Encapsulación y Setters | Nivel: Fácil

Objetivo: Crear la clase Producto para la tienda de merchandising de 'TechEvents Platform', implementando atributos, constructor y la encapsulación con getters y setters (incluyendo validación).
Tarea a realizar:

Crea una clase Producto con las siguientes características:

  1. Atributos private:

    • nombre (String)
    • precio (double)
    • stock (int)
    • idProducto (String): Generado automáticamente o asignado de forma única, puede ser un simple String por ahora.
  2. Constructor:

    • Un constructor que reciba idProducto, nombre, precio y stock.
  3. Getters para todos los atributos.

  4. Setters para precio y stock:

    • El setPrecio debe asegurar que el precio nunca sea negativo. Si se intenta establecer un precio negativo, debe dejar el precio sin cambios o establecerlo a 0.0, e imprimir un mensaje de advertencia.
    • El setStock debe asegurar que el stock nunca sea negativo. Si se intenta establecer un stock negativo, debe dejar el stock sin cambios o establecerlo a 0, e imprimir un mensaje de advertencia.
  5. Método toString():

    • Sobreescribe toString() para proporcionar una representación legible del Producto.
  6. Clase Main para Pruebas:

    • En tu clase Main, crea al menos dos objetos Producto.
    • Demuestra el uso de los getters.
    • Demuestra el funcionamiento de los setters con valores válidos e inválidos, observando los mensajes de advertencia.
Aplicación en el Mundo Real:

La encapsulación con setters y validaciones es crucial para mantener la integridad de los datos en cualquier sistema. Evita estados inconsistentes o inválidos de los objetos, lo que reduce bugs y hace el software más robusto y fiable, especialmente en transacciones comerciales.

4) Flexibilidad en la Creación de Objetos: Sobrecarga de Constructores para Participante | Nivel: Fácil

Objetivo: Implementar la clase Participante con múltiples constructores (sobrecarga) para permitir diferentes formas de inicialización.
Tarea a realizar:

Crea una clase Participante con las siguientes especificaciones:

  1. Atributos private:

    • idParticipante (String): Identificador único (puedes autogenerar uno simple o pedirlo).
    • nombre (String)
    • apellidos (String)
    • email (String)
    • registrado (boolean): Indica si el participante ya ha completado su registro (por defecto false).
  2. Sobrecarga de Constructores:

    • Constructor por defecto: Inicializa idParticipante con un valor por defecto (ej. "GEN_000"), nombre y apellidos como "Desconocido", email como "sin_email@example.com", y registrado a false.
    • Constructor con nombre y email: Recibe nombre y email. apellidos será "No proporcionado", idParticipante se autogenera (ej. "PART_" + un número secuencial o aleatorio simple), y registrado a true (asumimos que al dar estos datos ya se registra).
    • Constructor completo: Recibe idParticipante, nombre, apellidos, email y registrado.
  3. Getters para todos los atributos.

  4. Método toString():

    • Sobreescribe toString() para mostrar los detalles del participante.
  5. Clase Main para Pruebas:

    • En tu clase Main, crea un objeto Participante usando cada uno de los constructores.
    • Imprime los detalles de cada participante para verificar su correcta inicialización.
Aplicación en el Mundo Real:

La sobrecarga de constructores es esencial para proporcionar flexibilidad en la creación de objetos. Permite a los desarrolladores instanciar objetos con diferentes niveles de detalle o en diferentes estados iniciales, adaptándose a las necesidades del flujo de la aplicación (ej. registro rápido vs. registro completo).

5) this() para Evitar Duplicidad: Refactorizando Ubicacion | Nivel: Fácil

Objetivo: Refactorizar la clase Ubicacion para usar encadenamiento de constructores (this()) y centralizar la lógica de inicialización.
Setup Inicial:

Partiendo de una clase Ubicacion similar a la definida en el ejercicio 1 o 2.

Tarea a realizar:

Crea o modifica la clase Ubicacion con las siguientes características:

  1. Atributos private:

    • nombre (String)
    • direccion (String)
    • capacidadTotal (int): La capacidad máxima de personas.
    • tieneParking (boolean): Indica si la ubicación dispone de parking (por defecto false).
  2. Constructores sobrecargados utilizando this():

    • Constructor completo: public Ubicacion(String nombre, String direccion, int capacidadTotal, boolean tieneParking). Este será el constructor "principal" que inicialice todos los atributos.
    • Constructor con datos esenciales: public Ubicacion(String nombre, String direccion, int capacidadTotal). Este constructor debe llamar al constructor completo, estableciendo tieneParking a false por defecto.
    • Constructor básico: public Ubicacion(String nombre, String direccion). Este constructor debe llamar al anterior, estableciendo capacidadTotal a 0 por defecto y tieneParking a false.
  3. Getters para todos los atributos.

  4. Método toString():

    • Sobreescribe toString() para mostrar todos los detalles de la ubicación.
  5. Clase Main para Pruebas:

    • Crea una instancia de Ubicacion usando cada uno de los tres constructores.
    • Imprime cada objeto para verificar que los valores por defecto se establecen correctamente a través del encadenamiento.
Aplicación en el Mundo Real:

El uso de this() para el encadenamiento de constructores es una técnica fundamental para escribir código DRY (Don't Repeat Yourself). Evita la duplicación de lógica de inicialización, hace el código más limpio, más fácil de leer y mucho más mantenible, ya que un cambio en la inicialización básica solo necesita hacerse en un único lugar.

6) El Bug del Contador de Participantes: Depuración con Atributos Estáticos | Nivel: Medio

Objetivo: Identificar y corregir un bug relacionado con la implementación incorrecta de un contador de instancias utilizando un atributo de instancia en lugar de uno estático.
Setup Inicial:

Se te proporciona el siguiente código incompleto para la clase Participante.

// Participante.java (CON BUG)
import java.util.UUID;

public class Participante {
    private String idParticipante;
    private String nombre;
    private String email;
    // BUG: Este atributo DEBERÍA ser estático
    private int contadorParticipantes; 

    public Participante(String nombre, String email) {
        this.idParticipante = "PART_" + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
        this.nombre = nombre;
        this.email = email;
        this.contadorParticipantes++; // Incrementa el contador
    }

    public int getContadorParticipantes() {
        return contadorParticipantes;
    }

    @Override
    public String toString() {
        return "ID: " + idParticipante + ", Nombre: " + nombre + ", Email: " + email + ", Contador (incorrecto): " + contadorParticipantes;
    }
}

// Main.java (para probar el bug)
public class Main {
    public static void main(String[] args) {
        Participante p1 = new Participante("Ana García", "ana@example.com");
        Participante p2 = new Participante("Luis Pérez", "luis@example.com");
        Participante p3 = new Participante("Marta Soto", "marta@example.com");

        System.out.println(p1);
        System.out.println(p2);
        System.out.println(p3);

        System.out.println("\nContador de P1: " + p1.getContadorParticipantes());
        System.out.println("Contador de P2: " + p2.getContadorParticipantes());
        System.out.println("Contador de P3: " + p3.getContadorParticipantes());
        System.out.println("¿Número total de participantes creados? Esperado: 3. Obtenido: " + p3.getContadorParticipantes());
    }
}

Tarea a realizar:
  1. Analiza el código: Ejecuta el código proporcionado. Observa la salida e identifica el comportamiento inesperado del contadorParticipantes.
  2. Explica el bug: ¿Por qué el contadorParticipantes no muestra el número total de participantes creados, sino siempre 1 para cada uno? Relaciona esto con el concepto de miembros de instancia vs. miembros de clase.
  3. Corrige el código: Modifica la clase Participante para que el contadorParticipantes (renombrado a totalParticipantesCreados para mayor claridad) sea un atributo static y cuente correctamente el número total de instancias de Participante creadas.
  4. Adapta el acceso: Asegúrate de que el acceso a este contador estático se realice de la forma recomendada (Clase.atributoEstatico) tanto para incrementarlo en el constructor como para consultarlo en la clase Main.
  5. Verifica la corrección: Ejecuta el código modificado y confirma que la salida ahora muestra el recuento total de participantes correctamente.
Aplicación en el Mundo Real:

Depurar y entender el comportamiento de los miembros static es crítico. Los errores en su uso pueden llevar a datos inconsistentes en toda la aplicación, difíciles de rastrear. Este ejercicio simula un escenario común donde un atributo que debería ser compartido globalmente se implementa erróneamente por instancia.


Ejercicios de Refuerzo

7) Gestión de 'TechTokens': Atributos Estáticos y Métodos de Utilidad | Nivel: Medio

Objetivo: Implementar una clase TechTokens que gestione una moneda virtual común para la plataforma, utilizando atributos y métodos estáticos.
Tarea a realizar:

En la plataforma "TechEvents Platform", los usuarios ganan "TechTokens" por participar y pueden canjearlos. Vamos a crear una clase para gestionar aspectos globales de esta moneda.

  1. Clase TechTokens:

    • Crea una clase llamada TechTokens. Esta clase no debe poder instanciarse (pista: ¿qué tipo de constructor lo impide?).
    • Atributo Estático private: tasaConversionEuro (double): Representa cuántos TechTokens equivale 1 Euro. Inicialízalo a 10.0 (1€ = 10 TT).
    • Atributo Estático private: nombreMoneda (String): El nombre de la moneda, inicialízalo a "TechTokens".
    • Método Estático public: convertirEurosATokens(double euros): Recibe una cantidad en euros y devuelve la cantidad equivalente en TechTokens.
    • Método Estático public: getNombreMoneda(): Devuelve el nombre de la moneda.
  2. Clase Main para Pruebas:

    • En tu clase Main, utiliza los métodos estáticos de TechTokens para realizar y mostrar las siguientes operaciones:
      • Consultar el nombre de la moneda.
      • Calcular cuántos TechTokens se obtienen por 50.00€.
      • Calcular cuántos Euros son 250 TechTokens.
Pista:

Recuerda que los métodos y atributos estáticos se acceden directamente a través del nombre de la clase, sin necesidad de crear un objeto: NombreClase.metodoEstatico().

Aplicación en el Mundo Real:

Las clases de utilidad con miembros estáticos son omnipresentes en el desarrollo. La clase Math de Java es el ejemplo perfecto. Se usan para encapsular reglas de negocio o cálculos universales que no dependen del estado de ningún objeto específico.

8) final y el ADN del Objeto: La Clase Ponente | Nivel: Medio

Objetivo: Utilizar la palabra clave final en atributos de instancia para asegurar que la identidad de un objeto no cambie una vez creado.
Tarea a realizar:

Crea la clase Ponente para la plataforma. La información de un ponente, como su identificador y su nombre, no debe poder cambiar una vez que se ha asignado.

  1. Clase Ponente:

    • Atributos private final:
      • idPonente (String)
      • nombreCompleto (String)
    • Atributos private no-final:
      • temaPrincipal (String)
      • tarifaHora (double)
  2. Constructor:

    • Crea un constructor que reciba todos los atributos. Obligatoriamente, el constructor debe inicializar los atributos final.
  3. Métodos:

    • Implementa getters para todos los atributos (incluyendo los final).
    • Implementa setters solo para los atributos no-final (temaPrincipal y tarifaHora).
    • Añade un método calcularCostoCharla(int horas) que devuelva tarifaHora * horas.
  4. Intento de Setter para un final:

    • Intenta crear un setter para idPonente (public void setIdPonente(String nuevoId)). Observa el error de compilación. Incluye el comentario sobre el error en tu solución.
  5. Clase Main para Pruebas:

    • Crea un objeto Ponente.
    • Intenta modificar un atributo final (esto debe fallar en la compilación).
    • Modifica con éxito un atributo no-final (tarifaHora).
    • Imprime el costo de una charla de 4 horas.
Aplicación en el Mundo Real:

El uso de final es un pilar de la programación defensiva. Se utiliza en identificadores únicos (ID, UUID), nombres, o cualquier valor que deba permanecer inalterable. Asegura que el estado fundamental de un objeto no pueda ser modificado accidentalmente, lo cual es vital en entornos multihilo o sistemas de bases de datos.

9) Paso por Valor en el Mundo Real: Simulación de Transferencia de Fondos | Nivel: Medio

Objetivo: Comprender y demostrar las diferencias en el paso de parámetros (por valor de primitivos vs. por valor de referencia) en un contexto de objetos del mundo real.
Setup Inicial:

Utiliza la clase CuentaBancaria del material teórico como base (simplificada para solo tener saldo).

// CuentaBancaria.java (Simplificada para el ejercicio)
public class CuentaBancaria {
    private double saldo;

    public CuentaBancaria(double saldoInicial) {
        this.saldo = saldoInicial;
    }

    public double getSaldo() { return saldo; }
    public void setSaldo(double nuevoSaldo) { this.saldo = nuevoSaldo; }

    public void ingresar(double cantidad) {
        if (cantidad > 0) {
            this.saldo += cantidad;
            System.out.println("Ingreso realizado. Nuevo saldo: " + this.saldo);
        }
    }
}

Tarea a realizar:

Crea una clase GestorFondos con dos métodos estáticos y realiza pruebas en Main.

  1. Clase GestorFondos:
    • Método Estático A (Paso de Primitivo):
      public static void intentarModificarMonto(double monto) {
          monto = 0.0; // Se intenta modificar el valor recibido
          System.out.println("  [Dentro de A] Monto modificado a: " + monto);
      }
      
    • Método Estático B (Paso de Objeto):
      public static void transferir(CuentaBancaria cuentaOrigen, double cantidad) {
          System.out.println("  [Dentro de B] Saldo inicial en cuenta origen: " + cuentaOrigen.getSaldo());
          cuentaOrigen.ingresar(-cantidad); // ¡BUG: Intenta retirar de forma NO segura! (se ingresa con valor negativo)
          cuentaOrigen.setSaldo(cuentaOrigen.getSaldo() - cantidad); // Simulación de retiro
          System.out.println("  [Dentro de B] Saldo final en cuenta origen: " + cuentaOrigen.getSaldo());
      }
      
  2. Clase Main (Pruebas):

    • Prueba 1 (Primitivo): Declara un double llamado montoTransferir = 100.0. Llama a GestorFondos.intentarModificarMonto(montoTransferir). Imprime el valor de montoTransferir después de la llamada.
    • Prueba 2 (Objeto): Crea un objeto CuentaBancaria llamado miCuenta con saldo inicial 500.0. Llama a GestorFondos.transferir(miCuenta, 50.0). Imprime el saldo de miCuenta después de la llamada.
  3. Análisis: En los comentarios del Main, explica por qué el valor de montoTransferir no cambió, pero el saldo de miCuenta sí que se modificó, aun cuando Java solo usa "paso por valor".

Aplicación en el Mundo Real:

En el desarrollo de APIs y librerías, es fundamental que los desarrolladores entiendan qué información se está pasando a un método y si los cambios internos afectarán al objeto que se pasa como argumento. La mala comprensión de esto es una fuente constante de bugs sutiles en la manipulación de objetos.


Ejercicios de Ampliación

10) Diseño de un Sistema de Notificaciones Modular | Nivel: Alto

Objetivo: Diseñar e implementar una jerarquía básica de clases para un sistema de notificaciones, aplicando herencia y polimorfismo (introducción conceptual) y el uso de constantes estáticas.
Tarea a realizar:

La plataforma necesita enviar notificaciones. Vamos a modelar las notificaciones como una clase base que luego será extendida.

  1. Clase de Utilidad ConfiguracionNotificacion:

    • Crea una clase ConfiguracionNotificacion que no sea instanciable.
    • Define una constante public static final String REMITENTE_DEFAULT = "no-reply@techevents.com";
    • Define una constante public static final int MAX_LONGITUD_TEXTO = 500;
  2. Clase Base Notificacion:

    • Define la clase Notificacion con los siguientes atributos private:
      • remitente (String)
      • destinatario (String)
      • mensaje (String)
    • Asegura la encapsulación con getters.
    • El constructor debe recibir destinatario y mensaje. El remitente debe tomar el valor por defecto de la constante estática REMITENTE_DEFAULT de la clase ConfiguracionNotificacion.
    • El constructor debe recortar el mensaje si supera MAX_LONGITUD_TEXTO.
  3. Método de Comportamiento Polimórfico:

    • Añade un método public String enviar() que devuelve el texto: "Enviando Notificación al [Destinatario] desde [Remitente]: [Mensaje Recortado/Completo]".
  4. Clase Main para Pruebas:

    • Crea un objeto Notificacion con un mensaje muy largo (ej. más de 500 caracteres).
    • Crea otro objeto Notificacion con un mensaje corto.
    • Llama al método enviar() en ambos y comprueba que el remitente es el por defecto y el mensaje largo ha sido recortado.
Peer Review (Revisión por Pares):

Pídele a un compañero que revise tu implementación de la lógica de recorte del mensaje en el constructor. ¿Ha respetado el principio de encapsulación y ha usado correctamente las constantes estáticas? Pídele que te sugiera una alternativa para el recorte de texto sin usar la constante estática de longitud máxima, para discutir pros y contras.

Aplicación en el Mundo Real:

Este ejercicio modela cómo las grandes aplicaciones manejan tareas como el envío de correos o SMS. Las configuraciones globales se definen en clases estáticas, y los objetos base (como Notificacion) gestionan la lógica común de negocio (como el remitente o el límite de caracteres).

11) El Patrón Singleton para el Gestor de Conexiones | Nivel: Alto

Objetivo: Implementar el patrón de diseño Singleton para la clase GestorConexionBD, garantizando que solo exista una única instancia de este recurso en toda la aplicación.
Tarea a realizar:

En la plataforma, solo debe haber un gestor de conexiones a la base de datos para controlar los recursos. Utiliza el patrón Singleton para asegurar esto.

  1. Clase GestorConexionBD:

    • Atributo private static: instancia de tipo GestorConexionBD. Inicializado a null.
    • Atributo private: urlConexion (String). Inicializada en el constructor a "jdbc:oracle:thin:@servidor_prod:1521:XE".
    • Constructor private: El constructor debe ser private. Imprime un mensaje: "Se ha creado la única instancia del Gestor de Conexión."
    • Método public static: getInstance(): Este es el punto de acceso global.
      • Debe verificar si instancia es null. Si lo es, crea la nueva y única instancia (new GestorConexionBD()) y la asigna a instancia.
      • Si no es null, simplemente devuelve la instancia existente.
    • Método public de instancia: conectar(): Imprime "Conectando a la BD: " + urlConexion.
  2. Clase Main para Pruebas:

    • Prueba 1: Llama a GestorConexionBD.getInstance() dos veces, asignando el resultado a dos variables diferentes (conexion1 y conexion2).
    • Verificación: Imprime el resultado de comparar las dos referencias (conexion1 == conexion2). Debería ser true.
    • Prueba de unicidad: Comprueba la salida. Solo se debe imprimir el mensaje del constructor UNA sola vez.
    • Llama al método conectar() en ambas variables.
Peer Review (Revisión por Pares):

Pídele a un compañero que intente crear una segunda instancia del GestorConexionBD sin usar el método getInstance() (por ejemplo, con new GestorConexionBD()). Debe obtener un error de compilación. Pídele que te explique por qué el constructor debe ser privado.

Aplicación en el Mundo Real:

El Singleton es uno de los patrones de diseño más utilizados. Es vital para recursos de aplicación que deben ser únicos: gestores de bases de datos, gestores de logs, configuración de sistema y thread pools. Aplicarlo correctamente asegura el control estricto de los recursos compartidos.

12) Reto de Modelado: Creando una Clase de Utilidad de Propósito Múltiple | Nivel: Alto

Objetivo: Consolidar el uso de static, final y la no-instanciación, creando una clase de servicio o utilidad que resuelva cálculos para la plataforma.
Tarea a realizar:

Crea una clase UtilidadEvento que actúe como un conjunto de herramientas para la gestión de eventos.

  1. Clase UtilidadEvento:

    • Haz que esta clase no sea instanciable (constructor privado).
    • Constante Estática final: IVA_APLICABLE (double), con valor 0.21 (21%).
    • Método Estático: calcularPrecioFinal(double precioBase): Recibe un precio base y devuelve el precio con el IVA aplicado.
    • Método Estático: generarCodigoAsistencia(String email, String fecha):
      • Genera y devuelve un código de asistencia único (String).
      • El código debe ser la concatenación de las primeras 3 letras del email, las últimas 4 cifras de la fecha (asumiendo que la fecha es DD/MM/YYYY) y un número aleatorio de 3 dígitos (ej: System.currentTimeMillis() % 1000).
      • Ejemplo: email="pepe@corp.com", fecha="20/11/2024" -> Código: PEPE2024XXX (donde XXX es el aleatorio).
    • Método Estático: esFinDeSemana(int diaDeLaSemana): Recibe un entero representando el día de la semana (1=Lunes, 7=Domingo) y devuelve true si es fin de semana (6 o 7).
  2. Clase Main para Pruebas:

    • Muestra el cálculo del precio final para una entrada de 100.00€.
    • Genera e imprime el código de asistencia para el email "alicia@events.net" y la fecha "05/10/2024".
    • Comprueba si el día 1 (Lunes) y el día 7 (Domingo) son fin de semana.
Peer Review (Revisión por Pares):

Pídele a un compañero que revise tu método generarCodigoAsistencia. ¿Qué posibles problemas de robustez o seguridad tiene este método? ¿Cómo podría mejorarse usando la clase UUID o SecureRandom en lugar de la forma simple que se pide? Discute estas alternativas.

Aplicación en el Mundo Real:

La creación de códigos únicos (como códigos QR, códigos de barras o claves de licencia) y la gestión de cálculos fiscales son tareas de utilidad comunes en el desarrollo empresarial. Este ejercicio simula la centralización de estas lógicas en una clase de servicios.