Programación 3

Universidad de Alicante, 2021–2022

Práctica 3

Plazo de entrega: Hasta el domingo 14 de noviembre de 2021 a las 23:59h
Peso relativo de está práctica en la nota de prácticas: 25%
IMPORTANTE: Todos tus ficheros de código fuente deben utilizar la codificación de caracteres UTF-8. No entregues código que use otra codificación (como Latin1 o iso-8859-1).

Aclaraciones

  • Los métodos launch y patrol no deben lanzar la FighterNotInBoardException generada por Board.removeFighter, deben capturarla y relanzarla como RuntimeException porque el caza que se elimina en launch y patrol se elimina en un punto en el que se sabe seguro que está en el tablero. También es posible implementar launch y patrol sin usar Board.removeFighter (en cuyo caso obviamente no hay que ocuparse de la FighterNotInBoardException). El método patrol sí que debe lanzar la FighterNotInBoardException cuando el caza que va a patrullar no tiene posición asignada en el tablero

  • Aunque los cazas XWing, YWing y AWing son fabricados por la alianza rebelde, y los TIE son fabricados por el imperio, en esta práctica no hay ningún impedimento a que naves imperiales usen cazas fabricados por los rebeldes, ni tampoco al contrario, por lo que el caza es del bando al que pertenece su nave, y no es necesario hacer ninguna comprobación adicional cuando se crean los cazas

  • El método Board.removeFighter debe lanzar la FighterNotInBoardException, como se indica más adelante, por lo que no tiene sentido que devuelva boolean y debe cambiarse el tipo devuelto a void

  • El atributo type de la clase Fighter desaparece, y el método getType debe obtener el tipo del caza del nombre de la clase:

    public String getType() {
        return getClass().getSimpleName();
    }

(más información un poco más adelante, en la sección que habla de la clase Fighter)

ImperialCommander: varios tipos de cazas, y excepciones

Introducción

En esta práctica ampliaremos la práctica 2 con nuevos tipos de cazas derivados de la clase Fighter, y con la creación y gestión de excepciones para tratar los diferentes errores que puedan producirse. Las clases Coordinate y RandomNumber no cambian con respecto a la práctica 2, y las clases Ship y Board tienen cambios relacionados en su mayor parte con las excepciones. La clase Fighter se convierte en una clase abstracta de la que heredan 6 nuevas clases (3 para cazas rebeldes, y 3 para cazas imperiales); además, se añade una clase factoría FighterFactory para crear cazas. Por otro lado, se crearán 6 excepciones que es necesario usar y tratar en las clases Fighter, Ship y Board.

Diagrama de clases

El siguiente diagrama de clases UML representa las clases de nuestro modelo. Solamente se muestra la clase FighterFactory, la clase Fighter y sus clases derivadas, las clases Coordinate, RandomNumber, Ship y Board no aparecen porque su interfaz no cambia. En la clase Fighter solamente se muestran los cambios (los métodos abstractos aparecen en cursiva, y cambia la visibilidad de los constructores y desaparece el argumento type del constructor):

Diagrama de clases UML.
Diagrama de clases UML.

 

Las excepciones derivan todas de la clase estándar java.lang.Exception (o Exception), el diagrama de las clases de excepciones es:

Diagrama de clases de excepciones.
Diagrama de clases de excepciones.

 

La nueva clase FighterFactory pertenece al paquete model, y las clases derivadas de Fighter pertenecen al paquete model.fighters; finalmente, las excepciones pertenecen al paquete model.exceptions.

Hoja de ruta

Las dos partes de la práctica (herencia y excepciones) son bastante independientes, por lo que aunque en esta hoja de ruta se aconseja empezar por la herencia, también sería posible empezar por las excepciones. El orden aconsejado para ir construyendo tu práctica es:

  • Añade la clase factoría FighterFactory, y utiliza el método createFighter para crear cazas (en vez de new Fighter(...)
  • Convierte la clase Fighter en abstracta (añadiendo abstract entre public y class), modifica los argumentos y la visibilidad de los constructores (ahora son protected) y declara como abstractos los métodos que aparecen en el diagrama: copy y getSymbol
  • Crea el paquete model.fighters e implementa las clases derivadas (son todas muy similiares, puedes copiar/pegar desde la ventana del proyecto)
  • Crea el paquete model.exceptions e implementa las excepciones (también son todas muy parecidas, puedes copiar/pegar)
  • Finalmente, modifica las clases Fighter, Ship y Board para que lancen y capturen las excepciones

Recuerda, ve diseñando pruebas unitarias según vayas terminando cada clase, y utilízalas para depurar tu código antes de pasar a la siguiente clase.


Clases

A continuación se describen los métodos de cada una de las clases del modelo que cambian en esta práctica.

FighterFactory

Se trata de una clase factoría de la que nunca se crean objetos y sólo tiene un método estático para crear cazas. Debes usar este método para crear cazas porque el constructor de Fighter será protected y sólo lo utilizarán sus subclases.

createFighter()

Este método recibe los mismos argumentos que el constructor de Fighter de la práctica anterior, y debe crear un caza del tipo indicado por el argumento type (puedes usar un switch con esa cadena, desde la versión 1.8 de Java se permite usar switch con String). Como en un primer momento no tendrás creadas las subclases de Fighter, haz que devuelva null y completa el método cuando termines de implementar dichas subclases.

Si el argumento type no coincide con ninguna de las subclases de Fighter definidas, el método debe devolver null. Como debes utilizar este método para crear cazas nuevos en addFighters, si devuelve null no se añadirá a la flota de la nave ningún caza de ese tipo (esto sólo puede ocurrir si se indica en la cadena un tipo de caza que no existe, p.ej. 6/UWing)

Fighter y sus subclases

Esta clase se convierte en abstracta, por lo que en esta práctica no va a ser posible crear objetos Fighter, aunque sí seguiremos manejando variables de tipo Fighter, que harán referencia a un objeto de una de sus subclases. Como se puede observar en el diagrama UML, los dos constructores de Fighter pasan a ser protected, por lo que solamente serán usados desde sus subclases (con super(…)).

El atributo type de la clase Fighter desaparece (debes eliminar la línea private String type y las referencias al atributo en los constructores), y el método getType debe obtener el tipo del caza del nombre de la clase:

    public String getType() {
        return getClass().getSimpleName();
    }

En los métodos que necesiten el atributo type debes usar el método getType en su lugar.

Para crear un nuevo caza se utilizará el método createFighter de la clase FighterFactory (que creará el caza usando el constructor correspondiente de la subclase indicada en el argumento type), y para obtener una copia de un caza se debe usar el método copy, que tendrá que implementarse en todas las subclases de forma que invoque al constructor de copia de la subclase, que a su vez invocará al constructor de copia de Fighter (que es la única forma de construir una copia). De esta manera, cuando queremos copiar una variable de tipo Fighter invocamos al método copy (que se transformará automáticamente en una llamada al método copy de la subclase de Fighter a la que pertenezca el objeto contenido en la variable).

Los métodos abstractos se declaran en Fighter de esta manera (la documentación de cada método debe estar en Fighter, en las subclases no se debe poner):

        public abstract Fighter copy();

        public abstract char getSymbol();

En cada una de las subclases se debe implementar todos los métodos abstractos, por ejemplo en la clase AWing el método getSymbol se implementaría así:

        @Override
        public char getSymbol() {
                return 'A';
        }

Por otro lado, como se puede ver en el diagrama UML, muchos cazas tienen su propio método getDamage porque cambia la forma de calcular el daño en función de qué tipo de caza sea el caza enemigo. Solamente en las clases TIEFighter y XWing no se implementa este método porque no hay refinamiento (no cambia el comportamiento con respecto al método de la superclase Fighter), en cuyo caso se usará el método de la superclase.

NOTA: la directiva @Override se debe colocar antes de los métodos que sobreescriben un método de la superclase. No es obligatoria pero sí muy aconsejable, sirve para indicar al compilador que ese método sobreescribe un método de una clase superior, de forma que si hubiera algún error en la signatura del método derivado, el compilador nos daría un error. Por ejemplo, si en el método equals el tipo del argumento no fuera Object, al poner @Override el compilador daría error; si no hubiéramos puesto @Override en equals el compilador no daría error y llamaría al método equals de la clase superior (en ese caso, Object, aunque no tiene relación con que el argumento sea Object). En el caso de los métodos abstractos, el compilador detectaría otro error (que falta por implementar un método abstracto), pero aún así es aconsejable usar @Override siempre que se sobreescribe un método (especialmente en el caso de equals, hashCode y toString).

Los métodos de las subclases de Fighter tienen que hacer lo siguiente:

Constructor

Tienen que llamar al constructor de la superclase (con super), y modificar después algunos valores de la velocidad (v), ataque (a) y escudo (s), según se indica en esta tabla:

Caza Cambios
AWing +40v +5a -50s
XWing +10v +20a
YWing -20v -10a +30s
TIEBomber -30v -50a +35s
TIEFighter +10v +5a -10s
TIEInterceptor +45v +5a -20s

Por ejemplo, si el constructor de Fighter pone inicialmente la velocidad a 100, el ataque a 80 y el escudo a 80 (algo que podría cambiar en el futuro), el AWing tendría una velocidad de 140, un ataque de 85 y un escudo de 30.

Constructor de copia

En todos los casos es un método privado que debe llamar al constructor de copia de su superclase.

copy

Este método debe devolver un nuevo objeto de la subclase (aunque el tipo que devuelve es Fighter) obtenido con el constructor de copia de la subclase.

getSymbol

Este método debe devolver un carácter que representa el tipo de caza. Este carácter se usará en las prácticas siguientes, y es una letra mayúscula para los cazas rebeldes, y una minúscula para los imperiales:

Caza Símbolo
AWing A
XWing X
YWing Y
TIEBomber b
TIEFighter f
TIEInterceptor i

getDamage

Como se ha explicado antes, las clases XWing y TIEFighter no refinan el método de la superclase. Las demás subclases obtienen el daño estándar llamando al método de la superclase, y lo modifican en función del enemigo

  • AWing: si el caza enemigo es un TIEBomber le causa el doble de daño
  • YWing: si el enemigo es un TIEFighter o un TIEInterceptor le causa un tercio del daño; si es un TIEBomber, la mitad
  • TIEBomber: si el enemigo es un XWing o un YWing, le causa la mitad del daño; si es un AWing, un tercio
  • TIEInterceptor: si el enemigo es un YWing, le causa el doble de daño; si es un AWing, la mitad

Excepciones

A partir de esta práctica manejaremos casi todas las situaciones de error por medio de excepciones. Cuando se produce una excepción, se altera el flujo normal de ejecución del programa: el método que se está ejecutando aborta su ejecución y devuelve el control al método que lo invocó. Este método puede capturar y manejar la situación de error (lo que se conoce como capturar la excepción) o abortar su ejecución y devolver el control al método que lo invocó (propagar la excepción).
Si la excepción no es manejada por ningún método en la secuencia de invocaciones de métodos, el programa se aborta.

Las excepciones en esta práctica son todas muy similares, su código debe ser como en el siguiente ejemplo:

package model.exceptions;

@SuppressWarnings("serial")
public class ExampleException extends Exception {

    private int size;
    
    public ExampleException(int size) {
        super();
        this.size = size;
    }
    
    public String getMessage() {
        return "Informative message with "+size;
    }
}

El método getMessage debe devolver un mensaje indicando qué tipo de error se ha producido (incluyendo el escalar u objeto que ha provocado el error, en el ejemplo size). La directiva @SuppressWarings sirve para que el compilador no genere un warning (como su nombre indica).

Lanzamiento de excepciones

Cuando un método detecta una situación de error en su código que no puede tratar o que debe ser tratada en alguno de los métodos que lo han invocado, debe lanzar la excepción de esta manera:

   if (situacion de error provocada por 'a')
      throw new ExampleException(a);

Cuando un método lanza alguna excepción, debe declararlo añadiendo una cláusula throws en su declaración (y una @throws en la documentacion de Javadoc):

   public void metodoQueHaceAlgo(int a,Objeto b) throws ExampleException {
    ...
      throw new ExampleException(a);
    ...

El entorno Eclipse ayuda en esta tarea (como en otras muchas) y sugiere añadir la cláusula throws justo después de completar la línea throw new ..., y también añade la cláusula @throws a la documentación (que debe completarse indicando en qué situación se lanza la excepción, p.ej. algo como @throws ExampleException cuando el argumento ‘a’ es negativo)

Captura y propagación de excepciones

Cuando un método Y invoca a otro método Z que lanza excepciones, el método Y tiene que elegir entre dos opciones:

  1. Propagar la excepción y que sea el método que lo ha invocado (p.ej. X o uno anterior en la secuencia de invocaciones) el que trate la excepción. Para esto simplemente se debe añadir la cláusula throws a la declaración (y la @throws en la documentación)

  2. Capturar la excepción, envolviendo al método Z en un bloque try-catch

Cuando el entorno Eclipse detecta esta situación (que, hasta que se elija una opción, genera un error de compilación) sugiere todas las opciones posibles, que pueden ser bastantes cuando hay más métodos que lanzan excepciones en el método Y.

Excepciones utilizadas en esta práctica

En muchos casos, las excepciones se producen por errores de programación, por lo que al capturarlas se debe lanzar una excepción especial:

  throw new RuntimeException();

Las excepciones Runtime no se declaran con throws, son un error de ejecución que no tiene tratamiento posible dentro de la lógica del programa porque son un error de programación.

Las excepciones en esta práctica deben capturarse en el programa principal salvo que se indique lo contrario. En posteriores prácticas es posible que se capturen en otras clases. Si un método (p.ej. launch) puede lanzar más de una excepción, debe lanzarlas en el orden en el que aparecen en este apartado.

El mensaje que hay que devolver en los métodos getMessage debes decidirlo tú, y debe incluir el valor que ha causado la excepción y la cadena ´ERROR:´ al principio, como por ejemplo:

    public String getMessage() {
        return "ERROR: invalid size "+size;
    }

FighterIsDestroyedException

Debe lanzarse en el método Fighter.fight cuando alguno de los cazas implicados en la lucha es un caza ya destruido antes de empezar la lucha. Debe capturarse en los métodos de la clase Board y lanzar una RuntimeException

NoFighterAvailableException

Debe lanzarse en el método Ship.getFirstAvailableFighter cuando no sea posible encontrar en la flota un caza no destruido del tipo pedido (o de cualquier tipo, si el argumento type es una cadena vacía).

InvalidSizeException

Debe lanzarse en el constructor de Board si el argumento size es menor que 5.

FighterAlreadyInBoardException

Debe lanzarse en el método Board.launch si el caza que se quiere situar en el tablero ya tiene posición asignada.

OutOfBoundsException

Debe lanzarse cuando una coordenada en la que queremos colocar un caza o de la que queremos obtener la vecindad está fuera del tablero. En el caso de que se lance al intentar obtener la vecindad, debe capturarse y debe lanzarse una RuntimeException (esto sólo puede ocurrir si un caza que patrulla está mal posicionado).

FighterNotInBoardException

Debe lanzarse cuando el caza que queremos eliminar del tablero no está en el tablero o no es igual que el caza situado en su posición (esto claramente sería un error de programación). También debe lanzarse cuando el caza con el que queremos patrullar no tiene posición asignada en el tablero.

Programa principal

Descarga este fichero MainP3.java y copialo en la carpeta ‘src/mains’ de tu proyecto.

Puedes usar MainP3.java como un ejemplo inicial muy sencillo de partida parcial que usa las clases implementadas en esta práctica. Ten presente, en cualquier caso, que este código explora un subconjunto muy reducido de todas las posibles situaciones que se pueden dar en el juego. El archivo salida-MainP3.txt contiene la salida de este programa.


Pruebas unitarias

El archivo prog3-ImperialCommander-p3-pretest.tgz contiene una carpeta model con pruebas unitarias, algunas debes completarlas tú. En la carpeta verás las pruebas unitarias de la práctica 2 adaptadas para la práctica 3, y alguna prueba nueva, pero debes añadir más pruebas (el conjunto no es completo).

Documentación

Debéis incluir en los ficheros fuente todos los comentarios necesarios en formato Javadoc. Estos comentarios deben definirse para:  

  • Ficheros: debe incluir el nombre y dni del autor de la práctica usando la anotación @author
  • Clases: propósito de la clase
  • Métodos: propósito del método + parámetros de entrada (@param), valor de retorno (@return) y funciones dependientes para operaciones más complejas, y excepciones cuando corresponda (@throws).
  • Atributos: propósito de cada uno de ellos

No se debe entregar los ficheros HTML que genera Javadoc, y tampoco debes generar la documentación automáticamente con plugins, es una buena práctica generar la documentación a mano, justo en el momento en que se empieza a escribir el método.

Estructura de paquetes y directorios

En esta práctica crearemos dos nuevos paquetes:

  • model.fighters para las subclases de Fighter
  • model.exceptions para las clases que tratan de las excepciones.

Requisitos mínimos para evaluar la práctica

  • La práctica debe poder ejecutarse sin errores de compilación. 
  • Ninguna operación debe emitir ningún tipo de comentario o mensaje por salida estándar, a menos que se indique lo contrario. Evitad también los mensajes por la salida de error. 
  • Se debe respetar de manera estricta el formato del nombre de todas las propiedades (públicas, protegidas y privadas) de las clases, tanto en cuanto a ámbito de visibilidad como en cuanto a tipo y forma de escritura. En particular se debe respetar escrupulosamente la distinción entre atributos de clase y de instancia, así como las mayúsculas y minúsculas en los identificadores.
  • La práctica debe estar suficientemente documentada, de manera que el contenido de la documentación que se genere mediante la herramienta javadoc sea significativo.  
  • Se pueden añadir los atributos y métodos privados que se considere oportuno a las clases. No obstante, eso no exime de implementar TODOS los métodos presentes en el enunciado, ni de asegurarse de que funcionan tal y como se espera, incluso si no se utilizan nunca en la implementación de la práctica. 

Entrega de la práctica

La práctica se entrega en el servidor de prácticas del DLSI.

Debes subir allí un archivo comprimido que incluya el directorio model y toda la estructura de directorios y ficheros fuente tal y como se especifica en la práctica. En un terminal, sitúate en el directorio ‘src’ de tu proyecto Eclipse e introduce la orden

tar czvf prog3-imperialCommander-p3.tgz model

Esa orden crea el fichero prog3-imperialCommander-p3.tgz, que incluye el directorio model y los subdirectorios exceptions y fighters. Sigue las instrucciones de la página del servidor de prácticas para entrar como usuario y subir tu fichero.

Evaluación 

La corrección de la práctica es automática. Esto significa que se deben respetar estrictamente los formatos de entrada y salida especificados en los enunciados, así como la interfaz pública de las clases, tanto en la signatura de los métodos (nombre del método, número, tipo y orden de los argumentos de entrada y el tipo devuelto) como en el funcionamiento de éstos. Así, por ejemplo, el método model.Coordinate(int,int) debe tener exactamente dos argumentos de tipo int.

Tienes más información sobre el sistema de evaluación de prácticas en la ficha de la asignatura.

Además de la corrección automática, se va a utilizar una aplicación detectora de plagios. Se indica a continuación la normativa aplicable de la Escuela Politécnica Superior de la Universidad de Alicante en caso de plagio:

“Los trabajos teórico/prácticos realizados han de ser originales. La detección de copia o plagio supondrá la calificación de”0" en la prueba correspondiente. Se informará la dirección de Departamento y de la EPS sobre esta incidencia. La reiteración en la conducta en esta u otra asignatura conllevará la notificación al vicerrectorado correspondiente de las faltas cometidas para que estudien el caso y sancionen según la legislación vigente".