Programación 3
Universidad de Alicante, 2024–2025
Práctica 5
Peso relativo de está práctica en la nota de prácticas: 15% |
Herencia de interfaz
La herencia de interfaz nos permite modelar los sistemas pensando en la responsabilidad que deben cumplir los objetos en lugar de en cómo están implementados. Este hecho nos permite construir modelos complejos escalables y poco acoplados de manera natural.
En esta práctica trabajaremos sobre un sistema para el procesamiento de textos que hemos diseñado utilizando de manera exhaustiva la herencia de interfaz, empleando sólo la herencia de implementación en las situaciones en que realmente es conveniente.
La librería que hemos creado permite representar documentos que luego podrán ser exportados a distintos formatos. En la implementación actual se permite exportar a HTML y a markdown. Hay cientos de fuentes con información de estos formatos. Esta página y esta otra (en inglés) contienen información sencilla y completa sobre ambos formatos. Como nota al margen, la mayoría de las páginas del sitio web de Programación 3 están escritas en markdown y exportadas a HTML.
Proporcionamos el código de esta librería en el proyecto base adjunto.
NOTA: En el diseño sólo se han considerado asociaciones en lugar de composiciones para simplificar el código de la práctica y evitar tener que realizar copias defensivas.
Estructura de un documento
Los documentos HTML y markdown se estructuran en elementos tipo bloque (block) o tipo “en línea” (inline). Los elementos tipo bloque ocupan todo el ancho de la página aunque el contenido ocupe menos. Cuando ponemos dos contenidos tipo bloque, éstos se apilan verticalmente unos encima de otros. Los elementos tipo “en línea” se ajustan al contenido, y suelen aparecer unos al lado de los otros en la misma línea. Típicamente estos elementos se incrustan en un párrafo. En esta página web hemos rodeado los cuadros de los elementos tipo bloque con líneas discontinuas verdes, y los elementos tipo “en línea” con líneas de puntos de color rojo.
¿Qué tengo que hacer?
Tu trabajo en esta práctica consiste en añadir nuevas funcionalidades a esta librería. Leer detenidamente la sección siguiente (Descripción del diseño) y entender cómo funciona esta librería es clave para poder implementar con éxito lo que te pedimos en la sección Nuevas funcionalidades.
Descripción del diseño
Las distintos interfaces y clases de la librería están repartidos en diferentes paquetes. En los diagramas UML, un paquete se representa mediante una caja con una pestaña superior que contiene el nombre del paquete. Fíjate en cada diagrama para saber donde encontrar o colocar cada interfaz/clase.
Bloques y líneas
Durante la práctica utilizaremos diversas estructuras basadas en herencia de implementación y de interfaz que nos ayudarán a ahorrar código a la vez que nos permitirán ampliar de manera cómoda la aplicación.
En función de lo que hemos descrito anteriormente, podemos decir que
un documento está compuesto por elementos de tipo bloque y elementos de
tipo inline como mostramos en el siguiente diagrama. Como al
final un documento será una agregación de muchos elementos de ambos
tipos, inline (IParagraphContent
) y bloque
(IBlock
), hacemos que ambos hereden de la interfaz
IDocumentElement
.
En los siguientes apartados describimos poco a poco el modelo. Es interesante ver cómo podemos aislar partes de éste sin mostrar el modelo completo porque con los interfaces abstraemos responsabilidades y comportamientos. Ésta es una de las ventajas de la programación orientada a objetos.
Estructuras compuestas anidadas
En el sistema que hemos diseñado nos encontramos en varias ocasiones con elementos de un tipo dado que contienen otros elementos que a su vez pueden ser de ese mismo tipo. Esto nos ocurre con los bloques. Un bloque puede contener elementos de tipo inline pero también contener otros elementos también de tipo bloque, de manera que se pueden crear estructuras anidadas. En el ejemplo de bloques y líneas puedes ver cómo hay elementos bloque (los rodeados en verde), dentro de otros bloques.
Más adelante veremos cómo tenemos otros elementos compuestos similares como listas con viñetas. El propio documento es una agregación de bloques y de elementos inline.
Para no tener que programar varias veces esta estructura, hemos
creado una clase abstracta AbstractComposite
cuya
responsabilidad es contener listas de elementos
IDocumentElement
. Para poder crear estructuras anidadas
hacemos que AbstractComposite
implemente la interfaz
IDocumentElement
.
Así, podremos meter una AbstractComposite
dentro de
otra, y hacer estructuras como ésta:
AbstractComposite
- Bloque
- Inline
- Inline
- Bloque
AbstractComposite
- Bloque
- Inline
- Bloque
- Inline
AbstractComposite
- Bloque
- Inline
- Bloque
- Bloque
Vemos que el método addItem
es protegido porque será
usado sólo por las clases que lo hereden, de forma que podremos
restringir el tipo de elementos que añadamos.
(Para más información sobre métodos que no se comentan en este enunciado, consulta el código fuente de la librería que proporcionamos en el proyecto base. Los métodos export…() los entenderás cuando leas la sección Exportación).
Documento
Un documento (Document
), al contener una estructura
compuesta, posiblemente anidada, de bloques y líneas, hemos hecho que
herede de AbstractComposite
. En un documento, todos los
elementos deben estar dentro de al menos un bloque, por ello sólo
tenemos un método add(IBlock
). Este método, en su
implementación, invocará al addItem
de la clase base
AbstractComposite
(esto lo podemos hacer porque
IBlock
hereda de IDocumentElement
).
Véase cómo con este diseño podríamos en el futuro crear documentos
embebidos dentro de otros documentos pues Document
implementa indirectamente la interfaz IDocumentElement
.
Párrafos
Los párrafos Paragraph
son bloques que contienen otros
bloques o elementos inline. Esta funcionalidad la conseguimos
simplemente haciendo que Paragraph
implemente
IBlock
y herede de AbstractComposite
.
No hemos puesto ningún método para añadir contenido en
Paragraph
. El contenido del párrafo se inicializará con los
parámetros que recibe en el constructor. Hemos hecho dos constructores
sobrecargados, uno con una lista de IParagraphContent
, y
otro poliádico por facilitar luego la instanciación de párrafos simples.
Ambos constructores añaden estos elementos al párrafo usando el método
addItem
heredado de AbstractComposite
.
Véase cómo se puede añadir un párrafo a un documento simplemente
contando con que un párrafo se comporta como un bloque, o lo que es lo
mismo, que Paragraph
implementa IBlock
.
Listas con viñetas
Las listas con viñetas son estructuras como ésta:
- Esto es un elemento de la lista con viñetas no ordenada.
- Éste es otro elemento.
o como ésta:
- Primer elemento de una lista numerada u ordenada.
- Segundo elemento.
Las listas con viñetas son elementos tipo bloque que contienen a su
vez más elementos tipo bloque. Por ello hemos usado el mismo principio
que para diseñar los párrafos, hacemos que una lista con viñetas se
comporte como un bloque y use herencia de implementación para aprovechar
el código de AbstractComposite
.
Podemos ver los dos tipos de listas con viñetas disponibles, las ordenadas y las no ordenadas.
Finalmente, de forma similar a Paragraph
, sólo dejamos
añadir elementos en el constructor.
Encabezamientos
Los encabezamientos, o títulos de sección, son textos que se comportan como bloques. Según la jerarquía del encabezamiento tendrán un nivel (1, 2, etc.) para disponer de “Encabezamiento 1”, “Encabezamiento 2”, etc.
Como es de suponer, en el documento nos encontraremos varios tipos
distintos de objetos textuales. Por ello hemos creado una clase
AbstractTextContent
cuya responsabilidad es la de
encapsular un texto, y en el futuro, añadir alguna funcionalidad más
(como veremos en las tareas que tienes que realizar después).
Como los encabezamientos deben comportarse como bloques y aprovechar
la implementación de AbstractTextContent
utilizamos el
diseño que mostramos en el siguiente diagrama.
Podemos ver cómo podríamos crear un documento sólo con
encabezamientos añadiendo al documento bloques de tipo
Heading
.
Se ha considerado controlar la excepción EditorException
de que una cabecera Heading
no se pueda crear con un nivel
menor de uno.
Bloques de código
Otro tipo de bloque de texto similar al de los encabezamientos es el
que nos permite mostrar código fuente incrustado en un documento. Un
bloque de código es un texto (AbstractTextContent
) que se
comporta como un IBlock
y que opcionalmente tiene
información sobre el lenguaje de programación en que está escrito el
código que contiene.
Líneas de separación
El último tipo de elemento tipo bloque que incluimos en la librería es el de los separadores horizontales en forma de línea como ésta:
Hemos creado una interfaz IMark
para representar todos
los bloques que no tienen contenido adicional (como texto o imágenes).
La clase que representa estos separadores horizontales es
HorizontalRule
.
Imágenes
El primero y más sencillo de los elementos tipo inline es la imagen, como se puede ver en el diagrama de clases UML a continuación.
Hemos dejado en el diagrama los párrafos y el documento para que se
vea cómo se podría crear un documento sólo con imágenes. Para ello
crearíamos una o varias imágenes (que implementan
IParagraphContent
), y construiríamos uno o varios párrafos
pasándoles como parámetro esas imágenes. Finalmente, esos párrafos los
añadiríamos a un Document
.
Textos
Nos vamos a encontrar dos tipos de textos. Aquéllos que se encuentran
dentro de un bloque párrafo y que por tanto se deben comportar como
texto inline(IParagraphContent
) y otros huérfanos
de párrafo que actúan por sí mismos como bloques (IBlock
),
Estos últimos los usaremos en casos como los items de las listas con
viñetas (AbstractListBlock
heredado por
OrderedListBlock
y UnorderedListBlock
).
Usamos la capacidad de aplicar herencia múltiple de interfaz para
modelar este requisito en cualquier elemento de texto a través de la
interfaz IText
.
Mostramos en el UML los párrafos y las listas con viñetas para que se
vea cómo podríamos insertar texto en ambos. Si necesitamos insertar un
texto dentro de un párrafo, crearemos un objeto Text
y lo
pasaremos al constructor de Paragraph
, que al recibir
IParagraphContent
admite un Text
que
implementa esa interfaz.
Si queremos crear una lista con viñetas a la que añadir un texto,
crearemos un Text
que proporcionaremos en la lista de
IBlock
que enviamos al constructor de
AbstractListBlock
, pues Text
también
implementa IBlock
.
Código en línea
Un texto especial es el código en línea InlineCode
. Su
comportamiento e implementación son prácticamente iguales a
Text
. No hemos hecho que herede de éste porque en el futuro
podríamos añadir funcionalidades a Text
que no queremos que
tenga InlineCode
.
Mostramos el diagrama completo a continuación de las clases e interfaces que hemos introducido anteriormente.
Haz una pausa en tu lectura de este enunciado. Antes de continuar, asegúrate de que has entendido cuáles son los diferentes tipos de elementos de que se compone un documento y como se relacionan entre sí.
Exportación
Para exportar el documento a diversos formatos podríamos añadir un método para exportar cada uno de los formatos en cada clase (exportHTML(), exportMarkdown(),…). Sin embargo este enfoque tiene, entre otros, un problema importante: nos obliga a modificar las clases para cada formato nuevo.
Para evitar este problema, y permitir ampliar el número de formatos
de exportación sin necesidad de modificar las clases del modelo, creamos
dos interfaces: IExportable
e IExporter
.
Cada clase que queramos que se pueda exportar deberá implementar la
interfaz IExportable
. El código del método
export
sólo delega la responsabilidad de exportar al
IExporter
que recibe como parámetro con un código similar a
éste:
class X implements IExportable {
····public void export(IExporter e) {
.export(this);
e}
····}
Para que se sepa exportar la clase X
, en el interfaz
IExporter
se deberá añadir un método
String export(X exportable)
. Este procedimiento lo
repetiremos para cada clase que se quiera exportar.
Para exportar a diferentes formatos sólo tendremos que crear clases
concretas que implementen IExporter
.
En el diagrama de clases que mostramos arriba, vemos cómo hemos hecho
que Document
implemente la interfaz
IExportable
para que el documento completo se pueda
exportar. Para forzar que todos los elementos de la jerarquía
implementen el comportamiento de ser exportados, hacemos que
IDocumentElement
herede la interfaz
IExportable
. De esta forma, todas las clases del modelo
tendrán al final un método export
como el que hemos
descrito arriba. En el diagrama anterior hemos añadido
Image
y Paragraph
como ejemplo, pero como
puedes comprobar en todos los diagramas ya mostrados, todas las clases
sobrescriben el método export
porque de manera indirecta
les llega la obligación de implementarlo desde
IExportable
.
Para cada formato que queramos exportar creamos una clase que
implementa IExporter
. Así, para esta práctica hemos creado
dos clases, HTMLExporter
y MarkdownExporter
.
Puedes ver el código fuente en el proyecto Eclipse adjunto para ver cómo
se exporta cada tipo de elemento de un documento.
Comportamientos adicionales
Es habitual tener que añadir más capacidades a las clases que tenemos en la jerarquía de clases actual sin tener que cambiarlas. Esto es especialmente necesario cuando las clases a las que queremos añadir comportamientos están en una librería que no es nuestra y no podemos modificar.
Negritas y cursivas
Para poder añadir nuevas características a las clases, como la
adición de hipervínculos, negritas y cursivas, para cada nuevo
comportamiento añadimos una nueva clase. Para añadir cursiva y negrita
hemos creado las clases ItalicsTextDecorator
y
BoldTextDecorator
respectivamente.
Un AbstractDecorator
es una clase que contiene un
elemento (decoratedElement
) al que añadirá una
característica adicional a través de una subclase. Un
AbstractTextDecorator
es simplemente un
AbstractDecorator
que se comportará como un
IText
. Así, en todos los sitios donde podamos usar un
IText
podremos sustituirlo por cualquier clase que herede
de AbstractTextDecorator
. De esta forma, cuando queramos
poner una negrita, usaremos en lugar de Text
un
BoldTextDecorator
que habremos construido con un código
como éste:
= new BoldTextDecorator(new Text("Texto")); IText element
Si quisiéramos crear un texto en negrita y en cursiva sólo rodeamos la negrita con una cursiva:
= new ItalicsTextDecorator(new BoldTextDecorator(new Text("Texto"))); IText element
Véase cómo estos nuevos elementos de la jerarquía, al implementar
indirectamente IExportable
obligan a que las nuevas clases
sobrecarguen el método export
y que IExporter
disponga de métodos para tratarlas (en el diagrama hemos omitido el
resto de métodos de IExporter
para facilitar la
lectura).
Hipervínculos
Algo similar haremos para añadir enlaces tanto a textos como a imágenes.
Un LinkParagraphContentDecorator
, a través de
AbstractParagraphContentDecorator
, es un
IParagraphContent
.
Si quisiéramos sustituir una image por otra que tenga un hipervínculo
haremos:
= new LinkParagraphContentDecorator(new Image("logo_ua.png", "UA Logo"), "https://www.ua.es/"); IParagraphContent imageWithLink
Mostramos las clases e interfaces introducidos en los dos diagramas de clases mostrados en las Figuras 14 y 15 en éste:
Tareas a realizar
Hasta aquí todo el código del modelo y de los exportadores que hemos descrito y que se encuentran implementados en el proyecto base. Describimos a partir de aquí el trabajo que debes realizar.
Nuevas funcionalidades
Usando como ejemplo funcionalidades similares que ya están diseñadas
debes extender la jerarquía de clases con tres nuevas clases que deberán
heredar y/o implementar lo que sea necesario para que pasen los tests
unitarios que se incluyen en el proyecto de partida. Ten en cuenta que
cada vez que añadas una nueva clase al modelo deberás añadir ese
componente a la interfaz IExporter
con los cambios que eso
generará en las clases descendientes.
- Actualmente tenemos una clase
HorizontalRule
que baja de línea y dibuja una línea horizontal. Se debe crear una claseBreakLine
que sólo provoca un salto de línea. En markdown ese salto se codifica simplemente con un doble\n
, es decir\n\n
. En HTML se utiliza el elemento<br>
. Puedes consultar más información en esta página. Como esta clase se podrá incluir como bloque aislado pero también dentro de un párrafo, deberá implementar adicionalmente la interfazIParagraphContent
. - El sistema incluye formas de especificar negritas y cursivas con las
clases
BoldTextDecorator
eItalicsTextDecorator
. Debes añadir la claseStrikeThroughDecorator
para texto tachado. En markdown se debe rodear el texto a tachar entre “~~” (por ejemplo “~~esto está tachado~~” aparecería comoesto está tachado). En HTML se debe incrustar el texto en un elemento “<del>
” (por ejemplo “<del>esto está tachado</del>
”). Puedes consultar más información en esta página. - Finalmente debes añadir la clase
Quote
para escribir un párrafo de cita como puedes ver en esta página. En markdown sólo debes prefijar el contenido con un carácter ‘>’. En HTML se incluye el contenido en un elemento “<blockquote>
” (véase esta página). Puedes considerar que un párrafo de cita es un párrafo con esta funcionalidad enriquecida usando herencia para ello. Debes sobrecargar los dos constructores heredados deParagraph
. Ten en cuenta que hacer queQuote
herede deParagraph
no es que lo decore sino que se exporta de forma distinta. Para ello deberás sobrescribir el métodoexport
y añadir el métodoexport(Quote)
enIExporter
. En la exportación de HTML deberás cambiar la etiqueta<p>
que se usa en párrafo por<blockquote>
.
Implementación de una clase anónima
En esta parte debes hacer que las clases Image
y
AbstractTextContent
implementen la interfaz
ITextCaseModifiable
. Es conveniente que implementes el
método changeCase
en AbstractTextContent
y no
en sus subclases para evitar repetir el mismo código varias veces.
El resultado de la operación changeCase
modificará el
atributo text
en el caso de
AbstractTextContent
con el código
this.text = modifier.changeCase(this.text);
. En el caso de
Image
, modificará el atributo alt
con el
código this.alt = modifier.changeCase(this.alt);
.
Para ponerlo en práctica deberás completar la clase
TextsToUpperCase
de forma que el método
createTextModifier()
cree y devuelva un objeto de clase
anónima que implemente la interfaz ITextCaseModifier
y pase
un texto a mayúsculas (se comprobará que efectivamente sea anómima y no
se cree una clase con nombre). Puedes usar el método
toUpperCase
de la clase String
para realizar
la transformación.
Uso de la librería
Haciendo uso de la jerarquía de clases ya completada debes rellenar
el código de la clase
es.ua.dlsi.prog3.p5.client.ExampleDocumentCreator
para que
retorne un documento que genere los ficheros
example_document.md
y example_document.html
que se incluyen en los tests unitarios de forma que la prueba unitaria
ExampleDocumentCreatorTest
no falle.
Para poder realizar esta parte de la práctica deberás entender bien el significado de los interfaces y las clases abstractas, y por qué se ha usado herencia de implementación o herencia de interfaz.
Puedes consultar un ejemplo de creación de otra estructura de objetos
similar en el en el método setUp() del test unitario
es.ua.dlsi.prog3.p5.model.DocumentTest
que proporcionamos
en el proyecto base.
Cuando uses el constructor de Heading
deberás capturar
la excepción EditorException
y relanzarla como excepción no
verificada, pues estamos nosotros creando las clases y no debería lanzar
excepción nunca porque estamos creando encabezamientos correctos.
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
No es necesario documentar esta práctica
Puedes documentar el código que escribas, de hecho te lo recomendamos, pero no se evaluará.
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 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.
Entrega de la práctica
La práctica se entrega durante el control 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-p5.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-p5.tgz
al servidor de prácticas.
Sigue las instrucciones de la página para entrar como usuario y subir tu
trabajo.
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.
- Si quieres saber más, la práctica se ha diseñado empleando patrones de diseño, en concreto los patrones Composite, Visitor y Decorator. El diseño con patrones no se estudia en esta asignatura sino en cursos posteriores.