Programación 3
Universidad de Alicante, 2024–2025
Práctica 3
Herencia de implementación
Peso relativo de está práctica en la nota de prácticas: 15% |
Un sistema de entrada/salida
Proyecto base
Descarga el proyecto base para Eclipse de esta práctica, que ya contiene algunas clases y test unitarios. Impórtalo como proyecto Java en Eclipse
Introducción
En esta práctica introducimos la herencia de implementación, que hemos estudiado en la unidad 4.
En el paquete es.ua.dlsi.prog3.p3.lowlevel
nos han
proporcionado una librería de bajo nivel para comunicación entre
dispositivos de entrada/salida no específicos (InputDevice
y OutputDevice
) que se comunican a través de un canal
(Channel
). Sin embargo, queremos que el código cliente que
nos proporcionan en el paquete de código fuente
es.ua.dlsi.prog3.p3.client
trabaje con objetos que
representen dispositivos concretos como un teclado, una pantalla, un
ratón, etc.. Para ello tenemos que diseñar e implementar las clases del
paquete es.ua.dlsi.prog3.p3.highlevel
, que son las que usa
el código cliente para definir dispositivos de entrada/salida.
El proyecto Eclipse ya contiene algunas clases del paquete lowlevel y del paquete client mencionados, así como algunos test unitarios. Al principio tendrás errores de compilación en el paquete client, es normal, ya que depende del paquete highlevel; desaparecerán conforme vayas implementando este último.
Al realizar tu implementación, ten en cuenta los diferentes niveles de visibilidad de cada elemento (atributos y métodos) tal y como se representan en el diagrama UML. Esto es especialmente importante cuando se trabaja con herencia.
Aquí tienes el diagrama UML de estas clases (se omite el paquete
es.ua.dlsi.prog3.p3.client
):
En este UML se representan sin relaciones de herencia. Debes diseñar
las relaciones de herencia necesarias para que las clases del paquete
es.ua.dlsi.prog3.p3.highlevel
, junto a las clases
InputDevice
y OutputDevice
del paquete
es.ua.dlsi.prog3.p3.lowlevel
formen en realidad parte de
una única jerarquía de clases cuya raíz será, como se explica más abajo,
IODevice
.
La forma de comunicar dos dispositivos entre sí es crear un canal común a ambos mediante una instancia de la clase Channel:
Keyboard keyboard = new Keyboard();
LinePrinter printer = new LinePrinter();
Channel channel = new Channel(keyboard, printer);
y a partir de ahí, todo lo que enviemos al canal desde el dispositivo de entrada (keyboard) puede ser leído por el dispositivo de salida (printer):
keyboard.put((char)4);
for (char c='A'; c<'D'; c++) keyboard.put(c);
String line = printer.printLine(); // devuelve la cadena "ABCD"
Cada dispositivo de salida tiene un protocolo de comunicaciones muy
sencillo que define qué se debe enviar al canal para que el dispositivo
lo pueda leer. Esto se detalla más abajo, en la descripción de los
dispositivos de salida del paquete
es.ua.dlsi.prog3.p3.highlevel
.
Paquete es.ua.dlsi.prog3.p3.lowlevel
Las clases Channel
e IODevice
de este
paquete se dan ya implementadas. NO debes modificarlas. Mientras
lees esta descripción, ve consultando el código fuente del paquete para
entender mejor cómo funcionan internamente sus clases.
Como puedes ver en el diagrama UML, un Channel
se crea a
partir de dos dispositivos IODevice
. Los dos argumentos del
constructor de Channel
son, por este orden, el dispositivo
de entrada y el de salida. El canal se construye con un buffer
donde se almacena la información enviada desde el dispositivo de
entrada, que luego será leída por el dispositivo de salida. Estos
dispositivos usarán los métodos input(byte)
y
output()
de Channel
para enviar y recibir
datos del canal byte a byte.
Clase Channel
Disponemos de un constructor y tres métodos públicos que podemos usar para consultar el estado del canal:
Channel(IODevice, IODevice) Crea un canal y asocia los dispositivos pasados como parámetros a este canal. El primer argumento es el dispositivo de entrada y el segundo el de salida. Este constructor reserva memoria para un buffer del tamaño especificado por el dispositivo de salida, por lo cual siempre que nos refiramos al buffer de un dispositivo de salida, en realidad nos estamos refiriendo al buffer del canal asociado al dispositivo.
isFull() : indica si el canal está lleno (y por tanto no se puede escribir más en él)
hasData() : indica que hay bytes almacenados en el canal pendientes de leer.
getBufferSize() : devuelve la capacidad máxima del canal (el tamaño de su buffer), independientemente de que contenga información disponible para leer o no.
Además, Channel
dispone de otros tres métodos que los
dispositivos pueden usar:
- input(byte) : envía un byte al canal. Lanzará la excepción BufferOverflowException si el buffer del canal ya está lleno.
- output() : lee un byte del canal. Lanzará la excepción BufferUnderflowException si el buffer del canal no contiene datos para leer.
- resetBuffer(int bufferSize) : inicializa el buffer del canal al tamaño especificado por bufferSize. Los datos que puediera contener anteriormente el buffer del canal se perderán. Lanza IllegalArgumentException si el argumento no es mayor que cero.
Clase IODevice
Los dispositivos IODevice
son conectados a un canal por
el constructor de Channel
. Para crear un dispositivo
disponemos de dos constructores: el constructor por defecto se utiliza
para crear un dispositivo de entrada, mientras que el constructor
sobrecargado se utiliza para crear dispositivos de salida. Este último
necesita como parámetro el tamaño del buffer que deberá tener el canal
mediante el cual se le enviará información al dispositivo. El
constructor de Channel
se encargará de consultar esta
información para crear el canal.
Los métodos de IODevice
que podemos utilizar para
implementar los dispositivos de alto nivel son:
- getChannel() : devuelve el canal asociado a este dispositivo. Lanza IllegalStateException si el dispositivo no tiene asociado ningún canal.
- setChannel(Channel) : Asocia el dispositivo al canal pasado como argumento. Lanza NullPointerException si el argumento es null.
- getBufferSize() : obtiene el tamaño del buffer del canal asociado a este dispositivo.
Clases InputDevice
y OutputDevice
Estas clases, que tienes que implementar, representan, respectivamente, un dispositivo de entrada y un dispositivo de salida no específicos.
InputDevice
- InputDevice() crea un nuevo dispositivo de entrada.
- sendToChannel(byte) : enviar un byte al canal asociado.
- put(byte[]) permite enviar un array de bytes al canal asociado.
Tanto sendToChannel() como put() lanzan las excepciones estándar IllegalStateException si el dispositivo no tiene canal asociado y BufferOverflowException si el canal ya está lleno o se llena al ir enviando los datos.
OutputDevice
OutputDevice(int) : Crea un dispositivo de salida. El tamaño del buffer del dispositivo se indica en el parámetro del constructor.
receiveFromChannel() : lee un byte del canal asociado. Lanza la excepción estándar IllegalStateException si no hay canal asociado a este dispositivo y BufferUnderflowException si no hay datos en el canal.
get(int num_bytes) : lee como máximo num_bytes del canal asociado. Devuelve un array de bytes de tamaño igual al número de bytes leídos (que puede ser cero si el canal no contiene datos).
- Lanza la excepción estándar IllegalStateException si el dispositivo no tiene canal asociado.
- Si el parámetro num_bytes no es mayor que cero y menor o igual al tamaño del buffer de este dispositivo, lanza la excepción estándar IllegalArgumentException.
readStoredString() (Este método se da ya implementado; ver código fuente en el proyecto base). Lee una cadena de caracteres enviada al canal y la devuelve como un objeto de tipo String. El método supone que en el buffer del canal se encuentra una cadena de caracteres con el siguiente formato: el primer byte indica el número de bytes que hay a continuación en el buffer, los cuales representan caracteres ASCII imprimibles. Por ejemplo, si en el canal se encuentra la cadena “PROG3”, estará almacenada así en su buffer:
| 5 | 80 | 82 | 79 | 71 | 51 |
.- Devuelve la cadena leída, o bien la cadena vacía si no hay datos en el canal
- Lanza la excepción estándar IllegalStateException si el dispositivo no tiene canal asociado.
- Lanza BufferUnderflowException si el canal se vacía antes de poder leer toda la cadena, es decir, si los datos en el canal no tienen el formato correcto.
Paquete es.ua.dlsi.prog3.p3.highlevel
En este paquete necesitamos crear una jerarquía de clases que permita
definir dispositivos de entrada y salida específicos, basados en
InputDevice
y OutputDevice
. Debe ser posible
utilizarlos como en el ejemplo de código cliente de la introducción, o
el que puedes encontrar en el paquete
es.ua.dlsi.prog3.p3.client
.
Debes diseñar la jerarquía de herencia que forman las clases de este
paquete junto a InputDevice
y OutputDevice
.
Todas ellas son subclases de
es.ua.dlsi.prog3.p3.lowlevel.IODevice
, directa o
indirectamente. Organiza las clases en base a los conceptos que
representan. Los conceptos más específicos heredarán de aquellos más
generales. Si tu jerarquía de clases no está bien diseñada, las pruebas
del oráculo no compilarán correctamente.
IMPORTANTE: Que un método lance una excepción no implica que en él deban hacerse las comprobaciones pertinentes, puede que use otros métodos que ya las hacen.
Dispositivos de entrada
Las clases Keyboard
y Mouse
representan
dispositivo de entrada específicos. Simulan un teclado y un ratón,
respectivamente. Keyboard
permite enviar caracteres uno a
uno, mientras que Mouse
simula enviar la posición actual
del cursor de un ratón en la pantalla.
Keyboard
:- Keyboard() crea un teclado para enviar caracteres.
- put(char) envía un caracter al canal asociado. Lanza las excepciones estándar IllegalStateException si el dispositivo no tiene canal asociado y BufferOverflowException si el canal ya está lleno.
Mouse
:- Mouse() crea un ratón que permite enviar coordenadas 2D de la posición del cursor en pantalla.
- put(byte x, byte y) envía dos bytes que representan una coordenada 2D: el byte ‘x’ en primer lugar, seguido del byte ‘y’. Lanza las excepciones estándar IllegalStateException si el dispositivo no tiene canal asociado y BufferOverflowException si el canal ya está lleno o se llena tras enviar el primer byte.
Dispositivos de salida
Las clases Display
y LinePrinter
representan diferentes tipos de dispositivo de salida específicos.
Simulan, respectivamente, una pantalla y una impresora que imprime
caracteres línea a línea.
LinePrinter
Este dispositivo espera que en su buffer se encuentre una cadena de caracteres con el siguiente formato: el primer byte indica el número de bytes que hay a continuación en el buffer, los cuales representan caracteres ASCII imprimibles (ver método OutputDevice.readStoredString()).- LinePrinter() crea una impresora con un tamaño de buffer indicado por la constante LinePrinter.MAX_LINE_LENGTH más uno.
- printLine() devuelve un String que
contiene la cadena leída del buffer.
- Lanza la excepción estándar IllegalStateException si el dispositivo no tiene canal asociado.
- Lanza la excepción de usuario verificada
NoLineForPrintingException
si el buffer está vacío. Esta excepción se definirá en el subpaquetees.ua.dlsi.prog3.p3.highlevel.exceptions
y no recibirá ningún parámetro en su constructor. - Lanza la excepción estándar BufferUnderflowException si el canal se vacía antes de poder leer toda la cadena, es decir, si los datos en el canal no tienen el formato correcto.
Display
simula una pantalla cuadrada de NxN píxeles en blanco y negro, representada por una matriz de NxN bytes. El tamaño N está almacenado en el atributopixel_rows
, que indica el número de filas (y columnas) de la matriz. Un cero en la matriz representa un píxel ‘blanco’ o no activado. Cualquier otro número representa un píxel ‘negro’ o activado. Ten en cuenta que la matriz de bytes representa el contenido de una pantalla gráfica donde el eje X es el eje horizontal, y el eje Y es el vertical, por lo que una coordenada (x,y) corresponde al pixel en la columna ‘x’ y la fila ‘y’ de la matriz. Las cuatro esquinas de una pantalla de tamaño NxN corresponden por tanto a estas coordenadas: - esquina superior izquierda: (0,0) - esquina superior derecha: (N-1,0) - esquina inferior izquierda: (0,N-1) - esquina inferior derecha: (N-1,N-1)- Display(int N) crea una pantalla con N filas x N columnas de píxeles. El canal asociado a este dispositivo tendrá un buffer de tamaño NxN*2, ya que se necesitan dos bytes para cada pixel y queremos que en el buffer quepan eventualmente las coordenadas de todos los píxeles.
- getDisplaySize() : devuelve el número de filas (o columnas) de esta pantalla.
- refresh() Este método espera encontrar en el canal
coordenadas de pixeles que deben activarse en la pantalla. Por tanto
debe leer éstas desde el canal mientras queden datos en él y activar los
píxeles correspondientes a ellas, poniendo a un valor distinto de cero
las posiciones correspondientes de la matriz display. El canal
contendrá por tanto pares de bytes (x,y) que representen coordenadas
para píxeles de esta pantalla. Ten en cuenta que no tiene por qué
contener tantas coordenadas como pixels tiene la pantalla, puede haber
menos o incluso ninguna. Si el canal no contiene datos, este método no
modificará la matriz display.
- El método retorna una copia defensiva de la propiedad
display
. - Si el dispositivo no tiene canal asociado, se lanzará la excepción estándar IllegalStateException.
- Si al intentar leer una coordenada desde el canal, no hay suficientes bytes en el (dos al menos), se lanza la excepción estándar BufferUnderflowException.
- Las coordenadas (x,y) leídas del buffer deben corresponder a algún
pixel de la pantalla, es decir:
(0 <= x < N) y (0 <= y < N)
. En caso contrario, se lanzará la excepción estándar IndexOutOfBoundsException con un mensaje apropiado.
- El método retorna una copia defensiva de la propiedad
- clear() borra por completo la pantalla, desactivando todos sus píxeles.
Paquete es.ua.dlsi.prog3.p3.client
La clase InputOutputClient
, que ya se da implementada,
usa los dispositivos definidos en el paquete
es.ua.dlsi.prog3.p3.highlevel
para demostrar su
funcionamiento. En concreto, conecta un Mouse
con un
Display
, siguiendo el movimiento del ratón durante 20
segundos y dibujando el contenido del Display en una pantalla
ASCII en la consola. Luego conecta un Keyboard
con un
LinePrinter
.
Para poder ejecutar este programa, es necesario hacerlo desde un terminal y en una máquina que tenga una única pantalla (si no dará errores). Abre un terminal y sitúate en el directorio raíz del proyecto Eclipse de la práctica. Allí, ejecuta el programa así:
$ java -cp bin es.ua.dlsi.prog3.p3.client.InputOutputClient
Aquí abajo puedes ver un ejemplo de salida de este programa:
Y aquí hay un pequeño vídeo que muestra el funcionamiento del programa.
Pruebas unitarias
Proporcionamos pruebas unitarias en la carpeta test/
del
proyecto base que comprueban el adecuado comportamiento de las clases.
Es importante que entiendas qué se prueba y cómo se hace.
Documentación
Este apartado no se realizará en el control
Debes incluir en los ficheros fuente todos los comentarios necesarios en formato javadoc. Estos comentarios deben definirse para:
- Ficheros: debe incluir nombre y dni de los autores usando la anotación @author
- Clases: propósito de la clase: mínimo 3 líneas
- Operaciones/métodos: 1 línea para funciones triviales, y un mímimo de 2 líneas, parámetros de entrada, parámetros de salida y funciones dependientes para operaciones más complejas.
- Atributos: propósito de cada uno de ellos: como mínimo 1 línea
Puedes usar un comentario no javadoc cuando sea necesario.
No es necesario generar en HTML la documentación javadoc.
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. Evita 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.
Entrega de la práctica
La práctica se entrega en el servidor de prácticas del DLSI.
Debes subir allí un archivo comprimido con tu código fuente (sólo archivos .java). En un terminal, sitúate en el directorio ‘src’ de tu proyecto Eclipse e introduce la orden
tar czvf prog3-p3.tgz *
Esto comprimirá todo el código que hay en src/, incluyendo el de aquellas clases que ya se daban implementadas. Esto es correcto y debes entregarlo así.
Sube este fichero prog3-p3.tgz
al servidor de prácticas.
Sigue las instrucciones de la página para entrar como usuario y subir tu
trabajo.
Esta entrega sólo sirve para que se evalue la parte de documentación y obtener el resultado del oráculo.
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 Clase(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”.
Aclaraciones
- Aunque no se recomienda, 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.
- Cualquier aclaración adicional aparecerá en este enunciado.
- Te aconsejamos que implementes las clases en el orden que aparecen en este enunciado.
- Cuando se envían datos al canal de tipo
int
,char…
hay que hacer casting a(byte)
. - Conforme vayas implementando clases deberías ir probando que los test unitarios de cada una funcionan.
- Recuerda usar el análisis de cobertura de Eclipse para detectar aquellas partes de tu código que los test no están comprobando.
Display
, las coordenadas(x,y)
que se leen del canal indican eje horizontal y eje vertical de la ‘pantalla’, respectivamente, es decir, con una coordenada(x,y)
leída del canal, nos estamos refiriendo a las componentes de la matriz display así:display[y][x]
.- Dado que el objetivo de la práctica es aprender a usar la herencia, a la hora de implementar subclases debes aprovechar el comportamiento, tanto normal como de gestión de errores, que ya se encuentra implementado en las superclases. Si implementas correctamente la herencia entre clases, podrás implementar muchos de los métodos de las subclases en una o dos líneas.