Documentación de requisitos

  • Jordi Pradel Miquel

     Jordi Pradel Miquel

    Ingeniero de Informática por la Universidad Politécnica de Cataluña. Socio fundador e ingeniero de software en Agilogy, profesor en el Departamento de Ingeniería de Servicios y Sistemas de Información de la UPC, consultor de los Estudios de Informática y Multimedia en la Universitat Oberta de Catalunya y miembro del Grupo de Investigación de Ingeniería del Software para Sistemas de Información de la UPC, en el que ha publicado varios artículos de investigación en el campo de la ingeniería del software y de la aplicación a sistemas de información.

  • Jose Raya Martos

     Jose Raya Martos

    Ingeniero de Informática por la Universidad Politécnica de Cataluña. Compagina su actividad como ingeniero de software en Agilogy (empresa de la cual es socio fundador) con la de consultor en el Área de Ingeniería del Software en la UOC y la de profesor a tiempo parcial en el Departamento de Ingeniería de Servicios y Sistemas de Información de la UPC. A lo largo de los años ha trabajado en proyectos de sectores varios, como el financiero, la Administración pública o las telecomunicaciones, ejerciendo tareas técnicas y de gestión, lo que le ha dado una amplia perspectiva sobre el mundo del desarrollo de software y de su diversidad.

PID_00191251

Los textos e imágenes publicados en esta obra están sujetos –excepto que se indique lo contrario– a una licencia de Reconocimiento-Compartir igual (BY-SA) v.3.0 España de Creative Commons. Se puede modificar la obra, reproducirla, distribuirla o comunicarla públicamente siempre que se cite el autor y la fuente (FUOC. Fundació per a la Universitat Oberta de Catalunya), y siempre que la obra derivada quede sujeta a la misma licencia que el material original. La licencia completa se puede consultar en: https://creativecommons.org/licenses/by-sa/3.0/es/legalcode.ca

Índice

Introducción

Dedicaremos este módulo a estudiar la documentación de requisitos. La finalidad de la documentación de requisitos es guardar un registro de cuáles son los requisitos que hemos decidido incorporar al producto de software.
Empezaremos analizando las características deseables de la documentación de los requisitos. Esta documentación es muy importante, puesto que la información que recoge es la base para el posterior desarrollo del sistema.
También analizaremos los diferentes estilos de documentación. Por un lado, veremos que podemos dar prioridad a la agilidad (documentar lo mínimo necesario y confiar en otros mecanismos de comunicación) o a la exhaustividad (documentar el mayor número posible de requisitos con el máximo detalle posible), y, por otro lado, veremos que podemos usar un lenguaje natural (si queremos que la documentación sea fácil de entender) o un lenguaje formal (si queremos ser más rigurosos con el significado de lo que decimos).
Profundizaremos también en el estudio de los casos de uso, que ya conocemos de asignaturas anteriores, y veremos el lenguaje formal OCL para la documentación de restricciones de integridad y contratos en modelos orientados a objetos.

Objetivos

El principal objetivo de este módulo es que el estudiante tenga una visión general de las diferentes opciones que están a su disposición a la hora de documentar los requisitos de un software.
En concreto, al finalizar este módulo, el estudiante debe ser capaz de:
  1. Saber cuáles son las características deseables de una buena especificación de requisitos.

  2. Elegir entre los diferentes estilos de documentación (ágil / exhaustivo, formal / no formal) para un proyecto de software determinado.

  3. Saber evitar los errores más frecuentes en la utilización de los casos de uso.

  4. Crear un modelo del dominio en lenguaje UML y saber expresar en lenguaje OCL sus restricciones de integridad.

1.Documentación de requisitos: perspectiva general

La especificación de requisitos recoge el contrato entre los desarrolladores y los stakeholders y sirve como herramienta de comunicación para los dos grupos. Su estilo y formalidad dependerán del proyecto, pero también del contexto en que se desarrolla y de las personas participantes. Una de las tareas del ingeniero del software será, pues, evaluar cuál es la necesidad de formalidad y detalle a la hora de documentar los requisitos para un proyecto.
Hay que tener en cuenta que, desde el punto de vista del éxito o fracaso de un proyecto, es tan peligroso intentar desarrollar el proyecto sin la suficiente documentación de requisitos como dedicar los recursos y el tiempo disponible a crear una documentación innecesariamente detallada o formal.

1.1.Características deseables de la documentación de los requisitos

Según el estándar IEEE-830, una buena documentación de requisitos tendría que ser:
  • Correcta: Cada uno de los requisitos que constan es un requisito que debe cumplir el sistema que hay que desarrollar.

  • Inambigua: Cada uno de los requisitos especificados solo se puede interpretar de una manera.

  • Completa: Aparecen todos los requisitos del sistema que hay que desarrollar, con independencia del tipo de requisito.

  • Consistente: No contiene ninguna contradicción.

  • Ordenada: Está ordenada en función de la importancia y la estabilidad de los requisitos.

  • Verificable: Cada uno de los requisitos que hay es verificable; es decir, existe un proceso finito y con un coste razonable mediante el cual una persona o una máquina pueden determinar si el sistema cumple o no dicho requisito.

  • Modificable: Permite que se puedan realizar con facilidad cambios en la documentación (por ejemplo, que tenga tablas de contenidos, referencias cruzadas, etc.).

  • Trazable: El origen de cada requisito está claramente identificado y, al mismo tiempo, se facilita la identificación de cada requisito para poder referenciarlo más adelante durante el desarrollo del sistema.

1.2.Buenas prácticas de la documentación de requisitos

Pero ¿cómo conseguimos un documento que tenga todas las cualidades de una buena especificación de requisitos de software? A continuación, enumeramos algunas buenas prácticas que nos pueden ayudar a conseguirlo:
1) Usar un identificador para cada requisito, de tal manera que podamos hacer referencia a cada uno de ellos de forma unívoca.
2) Describir los requisitos utilizando un punto de vista global, en lugar de centrarnos en el punto de vista técnico, que tiene en cuenta cómo implementar el sistema.
3) Definir correctamente la granularidad de los requisitos para no tener una lista excesivamente larga de requisitos demasiado detallados, pero tampoco una lista muy corta de requisitos demasiado generales.
4) No dar por supuesta una solución determinada e intentar recoger los requisitos con la mente abierta con respecto a qué soluciones serán las más adecuadas (con independencia de la tecnología). Por ejemplo, no dar por supuesta una interfaz gráfica de usuario.
5) Definir, para cada requisito, unas condiciones de aceptación que favorezcan su verificabilidad.
6) Utilizar plantillas de documentos de requisitos que nos ayuden a conseguir la completitud de la documentación de requisitos.

2.Estilos de documentación de requisitos

Tal y como habéis visto, todo desarrollo de software que no sea trivial requiere que los requisitos obtenidos y seleccionados estén documentados, no solo para tenerlos registrados (y no tener que recordarlos), sino también como herramienta de comunicación entre las personas involucradas en el proceso de desarrollo.

2.1.Clasificación de las necesidades de documentación de requisitos

Las necesidades específicas de documentación pueden variar mucho en función del tipo de proyecto y de las metodologías de desarrollo empleadas. Por lo que respecta a las necesidades de documentación de requisitos de un proyecto determinado, podemos distinguir dos ejes:
75593m4001.gif
En el eje horizontal, podemos clasificar las necesidades de documentación según si esta tiene que ser exhaustiva (hay que documentar todos los requisitos y todos los detalles de cada requisito) o puede ser más ágil y permite documentar solo los detalles más relevantes de los requisitos más importantes, de forma que será necesario mantener conversaciones con los stakeholders (que no habrá que documentar) para aclararlos.
En el eje vertical, podemos clasificar las necesidades de documentación según si es necesario utilizar un lenguaje formal que garantice que la documentación es inambigua, consistente y verificable, o se puede usar el lenguaje natural de las personas que intervienen en el desarrollo.
En este apartado explicaremos primero los dos grandes ejes del gráfico anterior, correspondientes a las siguientes necesidades de documentación de requisitos:
  • Documentación exhaustiva-ágil

  • Documentación formal - no formal

A continuación proponemos un ejemplo de técnicas y métodos existentes para cada una de las cuatro grandes áreas del gráfico, correspondientes a las siguientes necesidades de documentación de requisitos:
  • Documentación exhaustiva y no formal (IEEE-830),

  • Documentación exhaustiva y formal (OCL),

  • Documentación ágil y no formal (historias de usuario), y

  • Documentación ágil y formal (especificación por ejemplos).

75593m4002.gif
Hay que tener en cuenta que la clasificación propuesta no es de tipo binario. Un proyecto puede requerir documentación que tiende a ser ágil y que está a medio camino entre el lenguaje formal y el no formal, por ejemplo. Teniendo esto en cuenta, los ejemplos que se muestran a continuación han sido elegidos por ser representativos de los cuatro grandes cuadrantes.
2.1.1.Documentación exhaustiva/ágil
Tal como se ha mencionado anteriormente, el estándar IEEE-830 recomienda que una especificación tenga, entre otras, la cualidad de estar completa. En este sentido, la documentación debería especificar todos los requisitos, tanto funcionales como no funcionales, e incluir la definición de las respuestas a cualquier combinación de entradas en todo tipo de situaciones.
Si queremos conseguir esta propiedad de la documentación de requisitos, necesitamos que la documentación sea totalmente exhaustiva. Este tipo de documentación nos permite, por ejemplo, que un equipo que no ha participado en la documentación de requisitos pueda desarrollar el software a partir de ella.
Pero la creación de documentación exhaustiva requiere un gran esfuerzo. El enfoque ágil del desarrollo de software reconoce que, en muchos casos, esta documentación exhaustiva se acaba por no hacer o, peor todavía, acaba yendo en detrimento del software que funciona. Este enfoque manifiesta que, a pesar de que la documentación exhaustiva tiene valor, el valor del software que funciona es aún mayor y, por este motivo, no se busca obtener una documentación exhaustiva, sino una documentación suficiente (para crear el software que funcione).
Así pues, hablamos de documentación ágil de requisitos para referirnos a las situaciones en que la exhaustividad no es una cualidad prioritaria de dicha documentación. En estos entornos, un requisito escrito en una tarjeta en papel puede ser suficiente si va acompañada de las conversaciones necesarias para aclarar los detalles del requisito; y dichas conversaciones no se documentan.
2.1.2.Documentación en lenguaje no formal - formal
Como hemos visto, el estándar IEEE-830 también recomienda que una especificación de requisitos sea inambigua, consistente y verificable. Nuestro idioma, así como cualquier otro lenguaje no formal, es muy flexible pero, a la vez, requiere mucha atención para asegurarse de no caer en ambigüedades y de que no se introduzcan inconsistencias; además, al ser totalmente abierto, puede dificultar la verificación de la documentación.
Por ejemplo, si documentamos que queremos que Viajes UOC tenga un listado de destinos ordenado según la popularidad, estamos introduciendo una importante ambigüedad. ¿Qué significa popularidad? ¿Hay que listar primero los destinos que tienen una puntuación media más alta? ¿O quizá los que son más visitados por los usuarios de la aplicación? ¿O tal vez los destinos hacia los cuales se contratan más viajes?
Para evitar estas ambigüedades, otra posibilidad consiste en usar un lenguaje formal a la hora de describir los requisitos. Se trata, por lo tanto, de usar un lenguaje definido formalmente y creado para no admitir ambigüedades y, a la vez, facilitar la verificación y la comprobación de la consistencia de la documentación de requisitos.
Por ejemplo, el lenguaje UML permite documentar ciertos tipos de requisitos, como los requisitos de datos, basándose en un modelo conocido y especificado formalmente, que hace inambigua la especificación. Por otro lado, existen herramientas que permiten comprobar que un modelo UML no tenga inconsistencias.

2.2.Documentación exhaustiva y no formal: IEEE-830

Un ejemplo representativo de la documentación exhaustiva pero en lenguaje natural (y, por lo tanto, no formal) es el estándar IEEE-830, publicado el 1998. Además de indicar las propiedades deseables de la documentación de requisitos (entre las cuales incluye la completitud), contiene indicaciones sobre cómo realizar una buena especificación de requisitos del software y propone una estructura base para ella.
2.2.1.Contenidos de la especificación
Según el estándar IEEE-830, la especificación de requisitos, que deberían escribir clientes y proveedores en colaboración, tiene que responder a las siguientes preguntas:
  • Funcionalidad: ¿Qué tiene que hacer el sistema que hay que desarrollar?

  • Interfaces externas: ¿Cómo interactúa el software con las personas, con el hardware o con otros sistemas software?

  • Rendimiento: ¿Cuál es la velocidad, disponibilidad, tiempo de respuesta, tiempo de recuperación, etc. que esperamos del software?

  • Atributos: ¿Cuáles son las consideraciones respecto a portabilidad, corrección, mantenibilidad, seguridad, etc.?

  • Restricciones de diseño para la implementación: ¿Debe cumplir algún estándar? ¿Presenta alguna restricción por lo que respecta al lenguaje de programación que hay que utilizar? ¿Cuáles son las políticas de gestión de datos (por ejemplo, relativas a la integridad de los datos)? ¿Existen limitaciones con respecto al entorno de operación? Etc.

2.2.2.Estructura de la especificación de requisitos del software
A continuación describimos los diferentes apartados que el estándar IEEE-830 propone para una especificación de requisitos del software. Sin embargo, hay que decir que el propio estándar ya avisa de que no es preciso que sigamos esta estructura al pie de la letra ni que usemos los mismos nombres para los apartados.
1) Introducción
La introducción de la especificación de requisitos del software tiene que proporcionar una visión general de todo el contenido del documento. En concreto, debería incluir las siguientes subsecciones:
  • Objetivo: Describe qué pretende el documento y a quién va dirigido.

  • Ámbito: Describe cuál es el producto que hay que desarrollar, identificado por su nombre (por ejemplo, “Sistema integral de ventas de viajes UOC”), qué hará y qué no hará el producto que hay que desarrollar, los beneficios que aportará el nuevo sistema a la organización y los objetivos que se espera conseguir.

  • Definiciones, acrónimos y abreviaturas: Contiene la explicación de todos los términos necesarios para entender correctamente la especificación de requisitos del software.

  • Referencias: Contiene tanto la lista completa de documentos referenciados más adelante durante la especificación de requisitos del software, donde cada documento está identificado correctamente (título, número de informe si lo tiene, fecha, editorial), como las fuentes donde se puede obtener dicho documento.

  • Sinopsis: Hace una descripción general del contenido del resto de la especificación de requisitos del software y explica la estructura del documento.

2) Descripción general
La segunda sección de la especificación de requisitos del software describe los factores generales que afectan al software y a sus requisitos sin indicar requisitos específicos (se trata de dar un contexto a los requisitos, no de detallarlos). En concreto, consta de los siguientes subapartados:
  • Perspectiva del producto: sitúa el producto que hay que desarrollar con respecto a otros productos de software. Por ejemplo, indica si forma parte de un sistema más grande (y, en tal caso, cómo encaja dentro de dicho sistema) o si es un producto autocontenido que funciona de manera aislada. Por lo tanto, contiene la descripción de las interfaces (con los usuarios, con otros sistemas software, con el hardware, etc.) que debe presentar el producto que hay que desarrollar.

  • Funciones del producto: resume las principales funcionalidades que ofrecerá el producto, así como su relación entre ellas (ya sea de forma textual o con la ayuda de algún diagrama).

  • Características de los usuarios: contiene información sobre cómo son los usuarios que se espera que utilicen el software (nivel de estudios, experiencia en el dominio, experiencia con la tecnología, etc.). De nuevo, se trata de dar la visión de por qué existen unos requisitos concretos más que de dar los requisitos en sí.

  • Restricciones: describe las leyes aplicables, limitaciones de hardware, necesidades de auditoría, requisitos de seguridad, etc.

  • Asunciones y dependencias: contiene la lista de factores que pueden afectar a los requisitos en el sentido de que un cambio en uno de estos factores puede afectar a los requisitos. Por ejemplo, si hemos supuesto que cierto sistema operativo estará disponible para el hardware sobre el que queremos ejecutar el software, debemos documentarlo en este apartado porque, si se diera el caso de que ese sistema operativo no estuviera disponible, habría que cambiar algunos de los requisitos del software.

  • Requisitos aplazables: son los requisitos que, a pesar de haberse identificado ya, se pueden aplazar para futuras versiones.

3) Requisitos específicos
Esta sección debe contener todos los requisitos de software con suficiente nivel de detalle para permitir a los desarrolladores desarrollar el sistema y a los expertos en pruebas, comprobar si se ha desarrollado correctamente.
Cada requisito debería incluir, como mínimo, una descripción de las entradas al sistema, las salidas resultantes y las funciones que el sistema tiene que llevar a cabo en respuesta a cada petición.
Estos requisitos se pueden documentar, a su vez, en subsecciones específicas:
  • Interfaces externas: requisitos de la interfaz gráfica de usuario, interfaces de hardware o de software e interfaces de comunicación.

  • Funciones: requisitos funcionales del sistema.

  • Requisitos de rendimiento: requisitos relativos al rendimiento esperado del software.

  • Requisitos lógicos de bases de datos: requisitos sobre qué datos se tienen que guardar en una base de datos, qué frecuencia de uso tendrán, qué capacidades de acceso a ellos habrá, qué restricciones de integridad de datos, etc.

  • Restricciones de diseño: restricciones impuestas en el diseño, por ejemplo por la presencia de otros estándares, limitaciones del hardware, etc.

  • Cumplimiento de estándares: estándares que debe cumplir el software.

  • Atributos del sistema de software: requisitos no funcionales, como la fiabilidad, la disponibilidad, la seguridad, la mantenibilidad y la portabilidad.

2.3.Documentación exhaustiva y formal: OCL

El lenguaje UML se puede utilizar como un lenguaje de modelado formal, ya que permite, para determinados ámbitos de la documentación de requisitos, como por ejemplo los requisitos de datos, expresar formalmente los requisitos mediante modelos que se expresan en diagramas que siguen cierta notación gráfica.
Sin embargo, hay cierta información que dicha notación no puede expresar, como algunas restricciones de integridad (por ejemplo, decir que el tiempo mínimo de conexión entre dos vuelos es de una hora) o las reglas de derivación de la información derivada. En estos casos, necesitamos usar otro lenguaje (que podría ser el lenguaje natural) para complementar los diagramas UML.
Limitaciones de la UML
Hay que tener en cuenta que UML no permite expresar formalmente cualquier tipo de requisito. Los requisitos no funcionales, por ejemplo, no se pueden expresar mediante un diagrama UML formal.
Por esta razón, la misma organización que publica el estándar UML publica otro estándar llamado OCL (1) .
Este es un lenguaje declarativo formal para describir reglas que se aplican a modelos UML y que fue creado, precisamente, para permitir crear modelos formales más completos.

2.4.Documentación ágil y no formal de requisitos: historias de usuario

La mayoría de los proyectos de desarrollo ágil utilizan una documentación no exhaustiva y no formal.
El desarrollo ágil se caracteriza por priorizar la comunicación verbal de los requisitos por encima de su documentación escrita con el fin de que la comunicación sea más fluida y ágil. En un entorno de desarrollo ágil, se suele usar la técnica de las historias de usuario para organizar los requisitos.
Una historia de usuario recoge qué es lo que necesita un usuario del software (por ejemplo, qué cosas debe poder hacer) para alcanzar sus objetivos. Los componentes básicos de una historia de usuario son:
  • una descripción corta (una frase) de la historia de usuario, que sirve como recordatorio de que existe la historia y es útil para planificar, etc.;

  • una serie de conversaciones, que sirven para definir y aclarar los detalles de la historia de usuario, y

  • un conjunto de criterios de aceptación, que documentan los detalles y que permiten determinar cuándo está implementada completamente la historia de usuario.

75593m4003.jpg
La principal ventaja de las historias de usuario y, al mismo tiempo, su principal inconveniente, es que están muy enfocadas a la comunicación verbal. Esto permite que la comunicación sea más ágil y fluida pero, en cambio, no deja constancia de todos los detalles por escrito. Sin embargo, hay que tener en cuenta que la conclusión final de las conversaciones sí queda documentada en forma de pruebas de aceptación.
2.4.1.Radiadores de información
Otra característica de la documentación escrita en un entorno de desarrollo ágil es que esta no siempre se encuentra en forma de documento textual, sino que suele tomar otras formas, como por ejemplo lo que se conoce como radiadores de información.
Un radiador de información es un elemento visual ubicado de tal manera que las personas lo tienen visible mientras trabajan o cuando pasan delante de él.
Un radiador de información se caracteriza por:
  • ser grande y visible a simple vista (no hay que buscarlo);

  • ser fácil de interpretar de un vistazo;

  • estar actualizado, de forma que resulta útil consultarlo con mayor o menor frecuencia, y

  • ser fácil de actualizar.

Existen muchos tipos de radiadores de información en un entorno ágil de desarrollo, desde los que indican el progreso realizado dentro de la iteración hasta los que indican el estado de la base de código (por ejemplo, si la última versión del código pasa todas las pruebas o no).
En nuestro caso, nos centraremos en los dos tipos que están más relacionados con la documentación y la gestión de requisitos: el panel de tareas y el panel del backlog.
En algunos casos (los casos extremos), estos paneles suponen toda la documentación escrita que se genera sobre los requisitos del sistema en desarrollo y, una vez implementada una funcionalidad, se destruye dicha documentación. Desde el punto de vista de la gestión de requisitos, algunos aspectos se pueden representar visualmente, como por ejemplo la prioridad (que se representaría según la posición dentro del panel o utilizando un código de colores) o el tamaño de los requisitos.
Panel de tareas
Uno de los radiadores de información más habituales, y uno de los que más llama la atención, es el panel de tareas.
El panel de tareas se puede organizar de formas muy distintas pero, en general, contiene información sobre las funcionalidades que hay que desarrollar durante una iteración, su descomposición en tareas y el progreso de cada una de dichas tareas.
Mike Cohn (2) propone, en su página sobre task boards, organizar el panel en filas, donde cada fila se corresponde a una historia de usuario (una funcionalidad que hay que implementar). Por lo tanto, la primera columna del panel Scrum contendrá las tarjetas de las diferentes historias de usuario.
Para considerar cada historia de usuario completada, debemos finalizar las diferentes tareas que hay que hay que llevar a cabo (codificación, pruebas, documentación adicional, etc.). Estas tareas pasan por un proceso muy sencillo: to do, mientras no están asignadas a ningún desarrollador; in process / to verify, cuando están asignadas a un desarrollador y este todavía no ha finalizado la tarea en concreto. Una tarea está en estado in process cuando hay cosas pendientes de hacer o to verify, cuando solo queda verificar algo. Finalmente, una tarea está en estado done cuando se ha finalizado o se ha verificado lo que había que verificar.
Ejemplo de panel de tareas
La ventaja de este panel es que, como buen radiador de información, nos permite ver de un vistazo cuál es el nivel de progreso de las diferentes historias de usuario que forman parte de la iteración actual.
75593m4004z.gif
Panel del backlog
El panel de tareas nos da una visión detallada del trabajo correspondiente a una iteración. Sin embargo, podemos aplicar esta misma filosofía a la documentación del backlog del producto. En este panel, en vez de mostrar el trabajo relativo a la iteración actual, se muestra el trabajo previsto para las iteraciones futuras (el backlog del producto).
Roman Pichler (3) , por ejemplo, propone dividir el panel en tres áreas: una para el propio backlog (las historias de usuario pendientes), una para los requisitos no funcionales, donde se incluirían algunos aspectos relevantes del diseño de la interfaz gráfica de usuario, y una para modelos del sistema.
75593m4005z.gif
El área de historias de usuario está dividida en dos partes: por un lado, las historias que están lo bastante detalladas como para empezar a implementarlas durante la siguiente iteración y, por otro lado, las historias de alto nivel, que, dado que no son tan prioritarias, todavía no están detalladas (las denomina epics) y se organizan en temas (grupos de historias de usuario relacionadas).
En el caso de Viajes UOC, una epic podría ser “Como usuario, quiero poder encontrar un viaje interesante y contratarlo”. Una historia como esta no tendría suficiente nivel de detalle como para comenzar a trabajar y, en el momento en que se priorizara para iteraciones cercanas, se descompondría en historias de usuario más pequeñas.
En el área de restricciones se incluye un recordatorio de los principales requisitos no funcionales. Estas restricciones nos ayudan a la hora de tomar decisiones relacionadas, por ejemplo, con la experiencia de usuario o con la arquitectura del sistema.
Finalmente, en el área de modelos se encuentran los diagramas de procesos, que nos ayudan a entender mejor cómo se relacionan entre sí las diferentes historias de usuario, así como cuál es su relación con los diferentes actores del sistema que hay que desarrollar.

2.5.Documentación ágil formal: especificación por ejemplos

A pesar de preferir la comunicación verbal, el desarrollo ágil prioriza el software que funciona, y solo se puede comprobar que un software funciona si los requisitos son verificables. De hecho, uno de los doce principios del manifiesto ágil es utilizar el software que funciona como principal medida de progreso. En este sentido, el uso de un lenguaje formal puede facilitar mucho la verificación de los requisitos para saber si el software funciona como es debido o no.
Aun así, como ya hemos visto, los lenguajes formales de especificación plantean el inconveniente de que, debido a su rigidez, dificultan la fluidez de la comunicación entre los clientes y los desarrolladores. Por eso, los métodos de desarrollo ágiles han creado lenguajes nuevos que permiten equilibrar la necesidad de agilidad en la comunicación y la rigurosidad en la descripción del problema.
Para encontrar este punto de equilibrio, estos métodos se centran en dos aspectos: por un lado, documentar lo mínimo imprescindible para poder determinar, más adelante, si el sistema cumple los requisitos o no (los criterios de aceptación) y, por otro lado, hacerlo en un lenguaje que puedan interpretar al mismo tiempo tanto los stakeholders como los programas de ejecución automatizada de las pruebas.
Por ejemplo, podríamos documentar los criterios de aceptación de la siguiente manera:
Cuando sumamos 2 y 2, el resultado es 4.
Cuando sumamos 5 y 20, el resultado es 25.
Esta descripción es muy fácil de entender para un stakeholder y, al mismo tiempo, es muy fácil de interpretar por parte de un programa, puesto que las dos frases tienen la misma estructura:
Cuando sumamos <num1> y <num2 > el resultado es <num1 + num2>
Una aproximación formal a la documentación de los criterios de aceptación consiste en escribir directamente pruebas de aceptación del sistema que se puedan ejecutar de forma automatizada. Dado que tales pruebas se escriben en forma de ejemplos de utilización del software, a menudo se usa el término especificación por ejemplos para hacer referencia a este estilo de especificación.
Las pruebas de aceptación verifican el comportamiento del sistema en comparación con los requisitos.
De hecho, al estudiar las historias de usuario, hemos visto que uno de los tres elementos que forman la historia de usuario son los criterios de aceptación.
El uso de un lenguaje formal para definir los criterios de aceptación de una funcionalidad nos permite automatizar la ejecución de las pruebas de aceptación.
Lo que nos interesa buscar en este lenguaje es que sirva, por un lado, para que los clientes escriban sus criterios de aceptación y, por otro, para que un programa pueda interpretarlos y ejecutarlos a fin de verificar si el sistema cumple dichos criterios o no.

3.Documentación de requisitos mediante casos de uso

Los casos de uso son una técnica de documentación de requisitos muy extendida, entre otros motivos porque UML le da soporte. Se trata de un enfoque en la manera de documentar requisitos que permite utilizar varios grados de detalle y de formalismo, lo cual los hace adecuados a escenarios muy diversos.
Un caso de uso recoge el contrato entre el sistema y los stakeholders mediante la descripción del comportamiento observable del sistema.
Un actor es una persona, una organización o un sistema informático que tiene capacidad de interactuar con el sistema y que presenta un comportamiento propio. Cada caso de uso tiene un actor llamado actor principal, que es quien utiliza el sistema para satisfacer un objetivo. El caso de uso describe cuál es el comportamiento observable del sistema durante esta interacción.
Sin embargo, en un mismo caso de uso, además del actor principal, pueden aparecer uno o más actores de apoyo, también denominados secundarios. Estos son actores externos al sistema que le proporcionan un servicio.

3.1.Flexibilidad de los casos de uso por lo que respecta al estilo de documentación

Una de las ventajas de los casos de uso es que son muy flexibles por lo que respecta al nivel de formalismo y la exhaustividad de su descripción. La misma técnica nos permite hacer documentaciones desde bastante ágiles hasta muy exhaustivas y desde nada formales hasta bastante formales. Por eso los estudiamos de manera separada del resto de los escenarios de documentación.
Aplicando los casos de uso, podemos situarnos en diferentes áreas del gráfico según hagamos descripciones más o menos detalladas (ágiles o exhaustivas), si utilizamos el lenguaje natural (no formal) o si hacemos contratos en el lenguaje formal OCL.
Aplicando los casos de uso, podemos situarnos en diferentes áreas del gráfico según hagamos descripciones más o menos detalladas (ágiles o exhaustivas), si utilizamos el lenguaje natural (no formal) o si hacemos contratos en el lenguaje formal OCL.
Por ejemplo, podríamos describir el caso de uso “Consultar recomendaciones sobre un hotel” de las siguientes maneras:
Caso de uso: Consultar recomendaciones sobre un hotel (nivel usuario)
El cliente indica unos términos de búsqueda (habrá una búsqueda simple y una búsqueda avanzada que permita refinar mejor los criterios de búsqueda) y el sistema muestra un listado de hoteles que los cumplen. El cliente puede elegir cualquiera de los hoteles del listado para ver todas las recomendaciones que hay sobre él. También puede pedir ver todas las recomendaciones hechas por el autor de una de las anteriores.
En este caso, se trata de una descripción muy informal y poco exhaustiva (en el sentido de que tiene pocos detalles), pero que nos puede servir, de manera similar a las historias de usuario, como recordatorio de que existe esta funcionalidad que debemos detallar antes de implementar.
Veamos ahora una versión más exhaustiva:
Caso de uso: Consultar una recomendación.
Actor principal: Cliente.
Ámbito: Sistema informático de ventas de Viajes UOC.
Nivel de objetivo: Usuario.
Stakeholders e intereses:
Agente de viajes: quiere que el cliente vea la recomendación y que sepa quién la ha hecho.
Cliente: quiere ver la recomendación.
Precondición: -
Garantías mínimas: -
Garantías en caso de éxito: El sistema mostrará la recomendación al cliente.
Escenario principal de éxito:
1) El cliente busca el hotel o destino sobre el cual quiere leer recomendaciones.
2) El sistema muestra la ficha detallada del hotel o del destino.
3) El cliente pide ver todas las recomendaciones sobre aquel hotel o destino.
4) El sistema muestra todas las recomendaciones sobre aquel hotel o destino
Extensiones:
3a. El cliente pide ver solo las recomendaciones positivas.
3a1. El sistema muestra solo las recomendaciones positivas de aquel hotel o destino.
3b. El cliente pide ver solo las recomendaciones negativas.
3b1. El sistema muestra solo las recomendaciones negativas de aquel hotel o destino.
4a. El cliente pide ver más recomendaciones del mismo autor que una de las ya consultadas.
4a1. El sistema muestra todas las recomendaciones hechas por aquella persona junto con el nombre del hotel y el destino si se trata de una recomendación sobre un hotel, o solo el nombre del destino si se trata de una recomendación sobre un destino.
Nota: En el caso de este ejemplo, supondremos que los casos de uso de nivel de tarea “Buscar un hotel” y “Buscar un destino” están descritos en otro lugar.
Como podemos ver, esta segunda descripción es mucho más detalla y, por lo tanto, mucho más exhaustiva que la anterior. Por otro lado, por el estilo del redactado, a pesar de usar un lenguaje natural, resulta más formal, en el sentido de que plantea menos ambigüedades. Sin embargo, a cambio de estas ventajas, plantea los inconvenientes de que es mucho más costosa de hacer y menos modificable, puesto que incluye muchos más detalles.
Todavía se podría hacer una descripción menos ambigua si usáramos un lenguaje formal. Por ejemplo, se podría asociar a cada acontecimiento del escenario principal de éxito una operación descrita mediante un nombre, unos parámetros de entrada y un resultado, y después hacer un contrato formal de cada operación para describir el comportamiento del sistema con precisión. De este modo, nos obligaríamos a detallar aspectos como, por ejemplo, qué significa que el nombre del hotel “coincida” con el término de búsqueda: ¿tiene que ser una coincidencia completa? ¿Solo en la primera parte del nombre? ¿Debe ser una coincidencia exacta, o basta con que suene similar?

3.2.Errores habituales en la documentación de los casos de uso

A continuación mostramos ejemplos de los errores que Cockburn plantea en su libro como los más habituales en la documentación de los casos de uso.
3.2.1.Casos de uso sin sistema
Veamos el siguiente ejemplo:
Caso de uso: Buscar un hotel.
Actor principal: Cliente.
Escenario principal de éxito:
1) El cliente introduce un término de búsqueda.
2) El cliente elige un hotel de la lista y ve la ficha detallada.
En este caso no hemos documentado nada de lo que debe hacer el sistema, por lo que, para cumplir lo que dice el caso de uso, bastaría con que el sistema incluyera las pantallas, sin aplicar ningún tipo de lógica. Obviamente, esta no es una situación deseable y, por lo tanto, lo que debemos hacer es documentar cuál es el comportamiento esperable del sistema.
La versión correcta sería:
Caso de uso: Buscar un hotel.
Actor principal: Cliente.
Escenario principal de éxito:
1) El cliente introduce un término de búsqueda.
2) El sistema muestra la lista de hoteles que coinciden en el nombre o en el nombre del destino con el término de búsqueda. Para cada hotel muestra su nombre y el nombre del destino.
3) El cliente elige un hotel de la lista.
4) El sistema muestra la ficha detallada del hotel.
3.2.2.Casos de uso sin actor principal
Veamos el siguiente ejemplo:
Caso de uso: Buscar un hotel.
Actor principal: Cliente.
Escenario principal de éxito:
1) El sistema muestra la lista de hoteles que coinciden en el nombre o en el nombre del destino con el término de búsqueda. Para cada hotel muestra su nombre y el nombre del destino.
2) El sistema muestra la ficha detallada del hotel.
En este caso, a pesar de que hay un actor principal (el cliente), no hemos descrito cómo interactúa con el sistema. A pesar de que en este caso es sencillo imaginarse cuál es su comportamiento, podría suceder que descuidáramos información importante sobre él.
En cualquier caso, es más difícil entender un caso de uso como este porque, como hemos dicho antes, tenemos que hacer el esfuerzo de imaginar la mitad de la interacción que queremos documentar. Además, el riesgo de ambigüedad y malentendidos es mayor, puesto que no es difícil que dos personas diferentes imaginen comportamientos distintos (esta reflexión también es aplicable al caso en que no hemos documentado el comportamiento del sistema).
3.2.3.Casos de uso demasiado dependientes de la interfaz de usuario
Veamos el siguiente ejemplo:
Caso de uso: Buscar un hotel.
Actor principal: Cliente.
Escenario principal de éxito:
1) El cliente introduce un término de búsqueda en la caja de texto de la parte superior derecha y pulsa el botón Buscar.
2) El sistema muestra la lista de hoteles que coinciden en el nombre o en el nombre del destino donde se encuentran con el término de búsqueda en forma de listado cebrado. Para cada hotel muestra su nombre y el nombre del destino. El nombre del hotel es un enlace y el cursor se convierte en una mano cuando se pasa por encima.
3) El cliente elige un hotel de la lista haciendo clic sobre el enlace del nombre del hotel.
4) El sistema muestra la ficha detallada del hotel.
Este es, posiblemente, el tipo de error más habitual: en vez de un documento de requisitos, ¡la descripción parece un manual de usuario! Añadir demasiados detalles sobre la interfaz de usuario no solo resta legibilidad a la descripción (se tiene que leer más texto para obtener la misma información), sino que, además, limita innecesariamente las posibilidades de implementación.
En el caso del ejemplo, cuando decimos que la caja de texto está situada en la parte superior derecha (o también cuando decimos que el nombre del hotel es un enlace), estamos descartando otras posibilidades que quizás serían más cómodas para el usuario.
El problema que tenemos es que estamos mezclando dos tipos de información muy diferentes: por un lado, desde el punto de vista de requisitos, lo que queremos es que el cliente nos indique un término de búsqueda. Por otro lado, por cuestiones de usabilidad, hemos pensado que el cliente encontrará más fácilmente la caja de texto si la situamos en la parte superior derecha y por eso la queremos situar ahí.
Como podemos ver, se trata de dos decisiones independientes entre sí y que se tienen que tomar teniendo en cuenta factores muy diferentes y, por lo tanto, no conviene mezclarlas (por lo menos de momento).
3.2.4.Objetivos de demasiado bajo nivel
Veamos el siguiente ejemplo:
Caso de uso: Buscar un hotel.
Actor principal: Cliente.
Escenario principal de éxito:
1) El cliente introduce un término de búsqueda.
2) El cliente pulsa el botón Buscar.
3) El sistema calcula la lista de hoteles que coinciden en el nombre o en el nombre del destino donde se encuentra con el término de búsqueda.
4) El sistema muestra, para cada hotel, su nombre y el nombre del destino donde se encuentra.
5) El sistema muestra, para cada hotel, la opción de ver la ficha detallada.
6) El cliente selecciona la opción de ver la ficha detallada de un hotel.
7) El sistema muestra una foto del hotel.
8) El sistema muestra los datos de contacto (dirección, teléfono, etc.) del hotel.
9) El sistema muestra el precio medio por estancia en el hotel.
10) El sistema muestra, en cada valoración que se haya hecho del hotel, el texto de la valoración.
11) El sistema muestra, en cada valoración que se haya hecho del hotel, el nombre de su autor.
El problema con este tipo de descripción es que, dado que los objetivos logrados a cada paso son de demasiado bajo nivel, resulta demasiado larga y pesada de leer. En este caso, la solución pasa por explicar lo mismo pero de forma más compacta:
  • Agrupando todas las interacciones que van en una misma dirección en un solo paso. Los pasos uno y dos, por ejemplo, se pueden resumir en uno solo: “El cliente introduce el término de búsqueda y pulsa el botón Buscar”. Sin embargo, a veces nos encontramos con que tenemos dos interacciones que, a pesar de ir en la misma dirección, son bastante diferentes entre sí (por ejemplo, “El cliente introduce sus datos personales” y “El cliente introduce los datos del producto que quiere comprar”). En estos casos, es mejor no agruparlas.

  • Identificando objetivos más generales. Los pasos tres a cinco, por ejemplo, se pueden considerar parte de un único objetivo: “Mostrar el listado de hoteles que cumplen los criterios de búsqueda”. Explicado de este modo, tenemos suficiente con un único paso, ya que no necesitamos detallar cómo se llevará a cabo este paso hasta que lleguemos al diseño del sistema.

  • Agrupando elementos de datos. Los pasos siete a once se pueden resumir como: “El sistema muestra la ficha detallada del hotel”. Si se considera que la ficha detallada es un concepto demasiado ambiguo, siempre se puede especificar exactamente la lista de campos que debe presentar en un glosario y reutilizar el término en las diferentes descripciones de casos de uso que vamos haciendo.

3.2.5.Casos de uso inconsistentes
Veamos el siguiente ejemplo:
Caso de uso: Buscar un hotel.
Actor principal: Agente de viajes o cliente.
Stakeholders e intereses:
Cliente: quiere ver información y recomendaciones sobre un hotel.
Agente de viajes: quiere ver información sobre un hotel.
Agente de viajes: quiere que su nombre aparezca junto a sus recomendaciones.
Escenario principal de éxito:
1) El cliente introduce un término de búsqueda.
2) El sistema muestra la lista de hoteles que coinciden en nombre o en el nombre del destino donde se encuentra con el término de búsqueda. Para cada hotel muestra su nombre y el nombre del destino.
3) El cliente elige un hotel de la lista.
4) El sistema muestra la ficha detallada del hotel.
5) El cliente selecciona una de las recomendaciones que hay sobre el hotel.
6) El sistema muestra todas las recomendaciones hechas por el agente de viajes que ha hecho la recomendación mencionada en el paso 5.
7) El cliente selecciona una de las recomendaciones mencionadas en el paso 6.
8) El sistema muestra la ficha detallada del hotel para el cual se hizo la recomendación (volvemos al paso 4).
En este caso, hay varias inconsistencias que deberíamos corregir:
  • Actor principal. A pesar de que se dice que el actor principal puede ser el cliente o el agente de viajes (tiene sentido que los dos puedan buscar un hotel), a la hora de describir los escenarios, hablamos solo del cliente. Sería preciso o bien considerar solo al cliente como actor principal, o bien modificar la descripción de los escenarios para tener en cuenta también a los agentes de viajes.

  • Intereses y escenarios no alineados. A la hora de describir el escenario, se ha apuntado toda una serie de pasos que no están relacionados con el objetivo del actor principal tal y como está expresado: el caso de uso se denomina “Buscar un hotel”, pero, en cambio, en la descripción se habla de todo el proceso de navegación entre fichas detalladas de hoteles, lo que daría, como mínimo, para un segundo caso de uso.

3.2.6.Otros errores frecuentes
La lista de errores frecuentes en casos de uso puede llegar a ser muy larga. Otros errores comunes que no discutiremos con tanto de detalle pero que es conveniente tener en cuenta son los siguientes:
  • Falta de definición del alcance y de los límites del sistema. Si de entrada no queda muy claro lo que consideramos que queda dentro del sistema y lo que no, podemos acabar confundiendo los términos.

  • Inconsistencia en la identificación de actores.

  • Demasiados casos de uso. Este error se puede considerar una variante del error de describir casos de uso a demasiado bajo nivel. Sin embargo, en este caso el error reside en hacer un número de casos de uso excesivo, de granularidad demasiado fina.

  • Relaciones entre actores y casos de uso excesivamente complejos. Si tenemos pocos actores y las relaciones entre actores y casos de uso se convierten en una telaraña indescifrable, quizás sea preciso hacer generalizaciones para detectar actores más generales, que reúnan los casos de uso en común de varios actores más específicos.

  • Especificaciones textuales inexistentes, demasiado largas o confusas.

3.3.Limitaciones de los casos de uso para la documentación de requisitos

Los casos de uso son una herramienta que, desde su creación en los años ochenta, han tenido un gran éxito, por lo que han sido adoptados por muchas organizaciones y equipos de desarrollo como forma de documentación.
Origen del concepto
El concepto casos de uso fue creado en 1986 por Jacobson, que fue uno de los impulsores de UML y del proceso unificado. Desde entonces, se han hecho muchas mejoras en la definición de la expresión, siendo la más significativa la de Cockburn, en su libro Writing effective use cases, publicado en 1999.
Sin embargo, esta popularidad ha hecho que en ocasiones se abuse de su utilización al intentar aplicarlos a situaciones donde no son la mejor herramienta para documentar requisitos. En este apartado enumeraremos algunas de las limitaciones de los casos de uso y, por lo tanto, en qué situaciones no son la herramienta más indicada para la documentación de requisitos.
3.3.1.Casos de uso para requisitos que no implican una interacción entre actores y el sistema
Los casos de uso no resultan muy adecuados para describir requisitos que no se pueden documentar como una interacción entre uno o más actores y el sistema. Es el caso, por ejemplo, de los requisitos no funcionales o de los requisitos de datos.
Esta limitación se puede paliar añadiendo observaciones o notas a la descripción de los casos de uso para documentar estos otros requisitos. Sin embargo, aun así, cuando un requisito afecta a muchos casos de uso, esta solución no resulta suficiente.
3.3.2.Carencia de formalismo y de estandarización
Como ya hemos comentado, la técnica de los casos de uso es tan flexible que se puede utilizar en situaciones muy diversas. De hecho, aparte de las nociones básicas sobre actores, escenarios y relaciones entre casos de uso, todo lo demás se tiene que definir cada vez que se utiliza. Así, por ejemplo, no hay una plantilla o formato de descripción textual concretos.
Esta flexibilidad no es por sí misma un defecto, pero los equipos y organizaciones que utilicen casos de uso deben tener claro que se verán obligados a definir estos aspectos para que los casos de uso resulten lo más expresivos posible y para que todas las personas implicadas en su escritura utilicen las mismas convenciones y formatos.
A priori, se podría pensar que formalizar más el concepto y el formato de los casos de uso supondría una posible mejora de esta situación. Sin embargo, esto haría que los casos de uso perdieran la flexibilidad, lo que en realidad es una de sus ventajas.
3.3.3.Falta de agilidad
También hemos mencionado que los casos de uso se pueden utilizar en situaciones en las que se requiere bastante agilidad en la documentación de requisitos. Aun así, para algunos equipos y proyectos, la agilidad que permiten conseguir no es suficiente.
El hecho de que los casos de uso obliguen a describir una interacción entre los actores y el sistema hace que, en un determinado nivel de abstracción, no se pueda escoger la granularidad de los requisitos tan libremente como con otras técnicas, como las historias de usuario.
El caso de las historias de usuario
No tiene demasiado sentido, por ejemplo, crear un caso de uso que aglutine funcionalidades muy relacionadas pero que llevan a cabo actores diferentes o un mismo actor en secuencias totalmente inconexas. En cambio, en el caso de las historias de usuario no existe ninguna limitación sobre cómo agrupar funcionalidades en historias de usuario de granularidad gruesa, siempre que quede claro qué objetivo satisface la historia de usuario.
3.3.4.Casos de uso para otras actividades de la ingeniería del software
Los casos de uso tienen su utilidad para documentar requisitos del sistema, pero no son útiles para otras actividades de la ingeniería del software como el diseño del software, el diseño de interfaces gráficas de usuario, las pruebas, etc.
Que los casos de uso no sean recomendables para actividades como el diseño del software o el diseño de interfaces gráficas no solo significa que no se deben utilizar para ello, sino que no podemos pretender no llevar a cabo estas actividades y esperar que los casos de uso las sustituyan.
Así, por ejemplo, no se puede pretender evitar diseñar el software y derivarlo de forma directa a partir de los casos de uso. Lo mismo podemos decir del diseño de la interfaz gráfica de usuario, una tarea que muchas personas derivan a los casos de uso, algo que Cockburn no recomienda.
Debemos tener en cuenta que el diseño de interfaces de usuario (como el diseño de software) es una actividad que requiere los conocimientos adecuados y que hay que llevar a cabo una vez que sabemos cuáles son los requisitos que tenemos. Por lo tanto, cuando documentamos los requisitos mediante casos de uso, debemos procurar no estar tomando decisiones de diseño al mismo tiempo que se deberían tomar una vez se conozcan los requisitos.
Por lo que respecta a las pruebas, los casos de uso no son, por sí mismos, un plan de pruebas, en el sentido de que no incluyen muchos de los elementos claves que debe incluir un plan de pruebas, como por ejemplo los valores para los datos de pruebas. Esto no significa que no sean una fuente de información esencial a la hora de crear un plan de pruebas funcionales.

4.Documentación del modelo del dominio mediante UML y OCL

4.1.El modelo del dominio

El modelo del dominio es una representación de clases conceptuales del mundo real en un dominio de interés.
Este modelo debe servir a las personas que intervienen en el desarrollo del software para entender mejor el dominio del sistema que tienen que desarrollar.
Si se quiere documentar formalmente el modelo del dominio de forma estándar, se pueden utilizar los diagramas de clases UML.
Las clases del dominio son aquellos conceptos del espacio del problema que resultan relevantes en el sistema que se analiza. En cada clase identificamos los atributos, que indican la información (los datos) que conocemos sobre los objetos de una clase determinada. Las clases tienen, también, asociaciones entre ellas, que nos permiten modelar qué otros objetos conocen los objetos de una clase. Una asociación entre dos clases indica que las instancias de cada clase conocen, como parte de sus responsabilidades, la instancia o instancias de la otra clase con las que están relacionadas.
Finalmente, entre dos clases de un modelo UML puede existir una relación de herencia. Decimos que una clase (subclase) hereda de otra clase (superclase) si todo lo que decimos de las instancias de la superclase también es aplicable a las instancias de la subclase, pero las instancias de la subclase presentan rasgos característicos comunes que no comparten las instancias de la superclase.
Ejemplo del modelo del dominio con clases, atributos, asociaciones y relaciones de herencia
75593m4007.gif
En este caso, el modelo del dominio nos indica que un concepto importante del dominio es la reserva (cuya fecha de inicio, fecha de finalización y código conocemos) y que, de hecho, hay dos tipos de reservas, las reservas de vuelos y las reservas de hotel.
Las reservas de vuelo están compuestas por uno o más trayectos, y a cada uno le corresponde un número de vuelo, una fecha y hora, una terminal, un aeropuerto y una ciudad de salida y de llegada y, además, los trayectos están relacionados entre sí de forma que un trayecto puede enlazar con otro.
Por lo que respecta a las reservas de hotel, indican una hora de check-in, una hora de check-out, un tipo de habitación (que puede ser individual o doble) y las observaciones que haya hecho el hotel o el agente de viajes. Además, cada reserva de hotel está relacionada con un hotel (cuyos nombre, dirección y número de teléfono sabemos).
4.1.1.Restricciones de integridad e información derivada
Una restricción de integridad es una regla o condición que los datos del software deben cumplir para que podamos decir que los datos son consistentes.
En el ejemplo anterior, la multiplicidad 1 junto a la clase Hotel nos indica que toda reserva de hotel tiene que estar asociada obligatoriamente a un hotel y solo a uno. Por lo tanto, si nos encontráramos con una reserva que no estuviera asociada a ningún hotel, querría decir que los datos del sistema no son consistentes.
Los diagramas de clases UML no permiten representar todas las restricciones de integridad que el sistema debe tener presentes sobre los datos que representa. Para ello es preciso documentarlas textualmente como anexo del diagrama.
Uno de los tipos de restricción de integridad que habrá que documentar son las claves de las clases. En el mundo real, identificamos los objetos y conceptos que utilizamos según una combinación de características únicas de dichos objetos. Por lo tanto, en el modelo del dominio la mayoría de las clases tienen una o más claves, un conjunto de propiedades de las instancias de aquella clase que las identifican de forma única.
En el caso de Viajes UOC, por ejemplo, podemos decir que no puede haber dos reservas que tengan el mismo código. Por lo tanto, diremos que código es una clave de la clase Reserva.
Otras restricciones de integridad indican, simplemente, situaciones que no son válidas en el modelo del dominio. Se trata de documentar situaciones que el diagrama de clases permitiría pero que se consideran incorrectas.
En Viajes UOC, con el modelo de dominio del que disponemos, por ejemplo, podría darse el caso de una reserva que tuviera como fecha de finalización una fecha anterior a la fecha de inicio, puesto que el diagrama UML lo permite. Si queremos documentar que esta situación no es correcta, debemos indicarlo como restricción de integridad textual.
Del mismo modo, podemos documentar que no es posible que dos reservas de un mismo viaje tengan solapamientos o no sean consecutivas.
Las restricciones de integridad se pueden expresar con lenguaje natural (“la fecha de finalización de una reserva no puede ser anterior a su fecha de inicio”) o bien mediante un lenguaje formal de especificación como OCL que veremos a continuación (“contexto Reserva inv: self.fechaFin<= self.fechaInicio”).
La información derivada es información que representamos gráficamente en el diagrama pero que se podría obtener mediante un cálculo sobre la información disponible.
Por ejemplo, supongamos que un viaje está compuesto por una serie de reservas:
75593m4008.gif
Si, por ejemplo, quisiéramos expresar que el viaje se tiene que pagar, como muy tarde, quince días antes de la fecha de salida, podríamos hacerlo de varias maneras:
  • El viaje se tiene que pagar, como mínimo, quince días antes de la fecha de inicio de la reserva del viaje que tenga la fecha de inicio anterior a todas las otras.

  • El viaje se tiene que pagar, como mínimo, quince días antes de su fecha de inicio.

En el segundo caso, hemos usado el atributo “fecha de inicio”. Este atributo es especial en el sentido de que su valor se puede calcular a partir de las fechas de las reservas, motivo por el que decimos que es un atributo derivado. La ventaja de los atributos derivados es que nos ayudan a crear nuevo vocabulario que facilita la explicación del modelo (en este caso, es mucho más fácil de entender el segundo enunciado que el primero).
Como podemos observar, toda información derivada lleva asociada una regla de derivación, que nos indica el cálculo que tenemos que hacer para obtener el valor de dicha información.
Igual que en el caso de las restricciones de integridad, podemos usar el lenguaje natural (como hemos hecho antes) o un lenguaje formal como OCL para expresar esta regla.
4.1.2.UML como lenguaje formal de documentación de requisitos
El lenguaje de modelado UML permite expresar parte de los requisitos desde un punto de vista totalmente formal, puesto que su especificación incluye un metamodelo que describe con precisión el significado de cada elemento del lenguaje.
Así, por ejemplo, en un diagrama de clases, la semántica de un atributo está claramente definida por la especificación del lenguaje UML. Y este atributo presenta unas características (como por ejemplo el tipo, la multiplicidad, etc.) que también tienen una semántica claramente definida.
Ahora bien, tal como hemos visto, UML no nos permite expresar toda la información posible sobre el dominio que analizamos:
  • La notación que ofrece UML no permite expresar todas las restricciones de integridad de los modelos estáticos del dominio.

  • UML no ofrece ninguna notación para expresar cómo se deriva la información derivada.

  • UML no ofrece ninguna notación que permita expresar la semántica de una operación del sistema o de una clase, a pesar de que sí que las hay para expresar el diseño.

OCL es un lenguaje textual estandarizado por el OMG que permite complementar el lenguaje UML para solucionar las carencias comentadas. Sus principales características son:
  • Se trata de un lenguaje de expresiones. OCL permite enunciar las llamadas expresiones: sentencias que tienen un resultado (como “cierto”, “23” o un conjunto de instancias de la clase Reserva) pero que no tienen efectos laterales; es decir, la evaluación de una expresión OCL no modifica nunca el estado de la población de objetos sobre la que se evalúa.

  • Se trata de un lenguaje formal. Cada expresión OCL correcta solo se puede interpretar de una manera, que es la que describe la especificación del lenguaje. Por lo tanto, no hay ninguna ambigüedad posible.

OCL se puede usar para expresar invariantes (restricciones de integridad), reglas de derivación y contratos de operaciones (sus precondiciones y postcondiciones). En este módulo solo nos centraremos en el uso de OCL para expresar restricciones de integridad y reglas de derivación de los modelos del dominio.

4.2.El lenguaje OCL

4.2.1.Introducción a OCL mediante ejemplos
La mejor manera de formarse una primera impresión sobre el lenguaje OCL es ver un ejemplo aplicado. Fijaos en un fragmento del modelo del dominio sobre el que estamos trabajando:
75593m4009z.gif
4.2.2.Restricciones de integridad y reglas de derivación en OCL
Tal como hemos visto, este modelo gráfico en UML no permite expresar cierta información que, hasta ahora, hemos expresado con lenguaje natural (en nuestro caso, el castellano). Pero este lenguaje natural tiene el inconveniente de ser no formal y, además, en muchas ocasiones resulta ambiguo.
Veamos algunos ejemplos de restricciones de integridad y cómo se expresan en OCL:
1) La fecha de finalización de una reserva debe ser posterior a la fecha de inicio:
context Reserva inv:
self.fechaFin> self.fechaInicio
2) Los hoteles reservados en un viaje deben estar en el destino del viaje:
context Viaje inv:
self.reserva->forAll(r | r.oclIsTypeOf(ReservaHotel) implies
r.hotel.destino = self.destino)
3) No puede haber más de un viaje del mismo cliente con la misma fecha de inicio (es decir, la clave de Viaje es cliente + fechaInicio):
context Viaje inv:
Viaje.allInstances()->forAll(v1,v2 | v1<>v2 implies not(v1.cliente=v2.cliente and v1.fechaInicio=v2.fechaInicio))
Debemos tener en cuenta que, a pesar de ser un lenguaje formal, OCL permite escribir una misma expresión de formas diferentes. Así, por ejemplo, el último invariante también se puede expresar como:
context Cliente inv:
self.viaje->isUnique(fechaInicio)
Por lo que respecta a las reglas de derivación, podemos ver cómo se expresa en OCL la regla de derivación del atributo fechaInicio de la clase Viaje. La fecha de inicio de un viaje es la fecha de inicio de la primera reserva del viaje
context Viaje::fechaInicio:Fecha
derive: self.reserva.fechaInicio->min()
4.2.3.Propiedades
Cómo hemos visto, las expresiones OCL se escriben en el contexto de un modelo de clases en el que la expresión self y otras expresiones obtenidas a partir de esta hacen referencia a objetos de este modelo.
El lenguaje nos permite, si tenemos un cierto objeto obj, acceder a cualquiera de sus atributos poniendo un punto '.' y, a continuación, el nombre del atributo. Veamos algunos ejemplos:
Si v1 es de tipo Viaje, v1.fechaInicio será la fecha de inicio de v1 y será, por lo tanto, de tipo Fecha.
Si h1 es de tipo Hotel, h1.nombre será el nombre del hotel h1 y será, por lo tanto, de tipo String.
De forma parecida, OCL nos permite acceder a los objetos asociados a un cierto objeto. Para hacerlo, sin embargo, usamos el nombre de rol que tienen los objetos asociados en la asociación que nos interesa. Veamos, también, algunos ejemplos:
Si rh1 es una instancia de ReservaHotel, rh1.hotelReservado será el hotel asociado a la reserva rh1, y será de tipo Hotel.
A veces, sin embargo, una asociación no define nombres de rol para las clases que participan en ella. En tal caso, en ninguna parte del nombre de rol usamos el nombre de la clase sustituyendo la primera letra por una minúscula.
Si v1 es un viaje, v1.destino será el destino del viaje v1, puesto que la asociación entre Viaje y Destino no define ningún nombre de rol en el lado de Destino y, por lo tanto, usamos como nombre de rol el nombre de la clase Destino pero cambiando la primera letra por una minúscula: destino.
¡Hay que ir con cuidado de no usar el nombre de rol equivocado!
Si h1 es un hotel, no podemos usar h1.hotelReservado, puesto que no hay ninguna asociación entre la clase Hotel y otra clase en la que el nombre de rol "hotelReservado" esté junto a esta otra clase. De manera parecida, si d1 es un destino, d1.destino no es una expresión correcta, puesto que un viaje v1 tiene asociado un destino, pero un destino no tiene asociado ningún otro destino.
Por lo tanto, desde el punto de vista de OCL, una instancia de una clase tiene tantas propiedades como atributos y asociaciones tenga.
Así, si tuviéramos una instancia v1 de tipo Viaje, su lista de propiedades completa sería:
  • v1.fechaInicio, de tipo Fecha, uno de los atributos

  • v1.fechaFin, de tipo Fecha, el otro atributo

  • v1.destino, de tipo Destino, el destino asociado al viaje

  • v1.agente, de tipo Agente, el agente asociado al viaje

  • v1.cliente, de tipo Cliente, el cliente asociado al viaje

  • v1.reserva, un conjunto de instancias de Reserva, las reservas asociadas al viaje

Tipo de las propiedades y expresiones
En OCL el tipo de las expresiones es importante. Según de qué tipo sea una expresión, se podrán aplicar unas funciones y operadores u otros.
Así, por ejemplo, a or b sería correcto si tanto a como b son expresiones booleanas, pero no sería correcto si una de las dos no lo es, puesto que el operador or sólo es aplicable a dos expresiones booleanas.
El tipo de una propiedad se puede conocer a partir del modelo mostrado en el diagrama de clases:
  • Si la propiedad es univaluada (un atributo o extremo de asociación con multiplicidad 1 o 0..1), el tipo de la propiedad es el tipo del atributo o el de la clase de aquel extremo de asociación.

  • Si la propiedad es un atributo o asociación multievaluados (indica una multiplicidad como por ejemplo [*] o [1..*]), el tipo será una colección. Las colecciones se ven más adelante, pero el tipo típico será un Set(T) donde T es el tipo de la propiedad.

Propiedades y herencia
Hay que tener en cuenta que el concepto de herencia en orientación a objetos (que ya conocéis) también se aplica a OCL, puesto que el modelo es un modelo orientado a objetos. Así, si una clase C2 es subclase de una clase C1, todas las instancias de C2 también son de tipo C1 y, por lo tanto, todo aquello que digamos de las instancias de tipo C1 también será cierto para las instancias de tipo C2.
Así, si tenemos una expresión en la que self es de tipo Cliente, la lista de propiedades de self será:
  • self.direccion

  • self.telefono

  • self.viaje (el conjunto de viajes asociados al cliente)

  • self.nombre (el nombre del cliente self, heredado de la clase Persona)

  • self.email (la dirección de correo del cliente self, heredada de la clase Persona)

Es decir, como todo cliente es también una persona, según nuestro diagrama de clases, y todas las personas tienen nombre y email, self también tiene nombre y email. Fijaos que no hay que indicar de ninguna manera especial que estas propiedades son heredadas.
4.2.4.Sintaxis básica OCL
Cada expresión OCL se escribe en el contexto de una instancia de cierto tipo. El contexto de una expresión OCL se indica en la propia expresión de una forma que depende del uso que hagamos de OCL.
Restricciones de integridad
Para expresar restricciones de integridad, el contexto es una instancia de una clase del modelo y se indica mediante la palabra clave context seguida del nombre de la clase a la que pertenece la instancia y la palabra clave inv. Opcionalmente, podemos indicar un nombre para el invariante y para la instancia. Si no damos nombre a la instancia, entonces podemos referenciarla mediante la palabra clave self.
context [nombreInstancia:] nombreClase inv [nombreInvariante]:
invariante
Así, por ejemplo, para indicar que el nombre de todas las personas debe tener más de un carácter (como mínimo dos), se indica:
context Persona inv self.nombre.size() > 1
Esta expresión se podría leer de la forma siguiente: “Para toda persona debe ser cierto que su nombre tiene una longitud mayor que 1”. Si se quiere indicar el nombre del invariante, se puede hacer de la manera siguiente:
context Persona inv: nombreMin2Caracteres: self.nombre.size() > 1
También se puede indicar un nombre diferente para la instancia, si se desea poder leer la expresión como “Para toda persona p tiene que ser cierto que el nombre de p debe tener una longitud mayor que 1”:
context p:Persona inv: p.nombre.size() > 1
Fijaos que un invariante es una expresión OCL de tipo booleano; es decir, una expresión que tiene que ser cierta o falsa. Así pues, una expresión que tenga como invariante una expresión que no sea booleana no tendría sentido:
contexto Persona inv: self.nombre
Esta restricción se leería como "Para toda persona ha de ser cierto el nombre de la persona". Pero el nombre de una persona no es un booleano sino un String y, por lo tanto, no puede ser cierto o falso.
Reglas de derivación
Para expresar las reglas de derivación, el contexto es un atributo o extremo de asociación que se indica de una forma parecida, pero con la palabra clave derive:
context nombreClase:nombreAtributo:tipoAtributo derive: expresionDerivacion
o bien
context nombreClase: nombreExtremAsoc: tipoExtremAsoc derive: expresionDerivacion
Por ejemplo, para indicar que la fecha de inicio de un viaje es la fecha de inicio de la reserva que empieza primero, indicamos:
context Viaje::fechaInicio:Fecha derive: self.reserva.fechaInicio->min()
Podemos leer esta expresión como “El atributo fechaInicio, de tipo Date, de la clase Viaje, se deriva como la mínima fechaInicio de las reservas del viaje”.
Nota
Como veremos más adelante, self.reserva.fechaInicio es una colección de las fechas de inicio de todas las reservas del viaje self, y min() es una operación que devuelve el elemento más pequeño de la colección.
Por lo tanto, nos estamos quedando con la fecha más pequeña de todas las fechas de inicio de las reservas de self; es decir, con la fecha de inicio de la reserva que empieza antes que cualquier otra.
No hay que confundir la forma de expresar las reglas de derivación con la de las restricciones de integridad. En particular, una expresión de derivación no indica una propiedad que tiene que ser cierta:
Supongamos que hay un atributo derivado /duracion:Tiempo en la clase Viaje. Queremos indicar que la duración de un viaje es el resto de su fechaFin menos su fechaInicio (suponiendo que el resto de fechas nos devuelve algo de tipo Tiempo). La expresión siguiente sería incorrecta, puesto que usa algo parecido a una invariante para indicar el valor de una expresión derivada en lugar de indicar directamente el valor:
-- ¡incorrecta!
contexto Viaje::duracion:Tiempo derive: duracion = self.fechaFin - self.fechaInicio
Este ejemplo, si lo leemos en castellano, viene a decir algo así como: "La duración de un viaje, de tipo Tiempo, es que la duración sea igual a la diferencia entre la fecha de fin y la de inicio del viaje". Cómo se puede ver, no tiene sentido.
La expresión correcta sería:
contexto Viaje::duracion:Tiempo derive: self.fechaFin - self.fechaInicio
Esta se leería como: "La duración de un viaje self, de tipo Tiempo, es la diferencia entre la fecha de fin de self y la fecha de inicio de self".
Por lo que respecta a los tipos de datos, OCL define cinco tipos básicos: tres numéricos (Real, Integer y UnlimitedNatural), uno para cadenas de texto (String) y uno para booleanos (Boolean).
Tipos numéricos
Los tipos numéricos ofrecen las operaciones típicas. Las cuatro operaciones básicas (suma, resta, multiplicación y división) y las de comparación se representan mediante operadores en notación infija (por ejemplo, “a + b” para invocar a la operación suma de a con el parámetro b), mientras que el resto utiliza la notación prefija habitual de OCL (por ejemplo, “a.max(b)” para invocar a la operación max de a con el parámetro b).

Operación

Ejemplo

Detalles

+, *, /

3+4

Suma, multiplicación y división de dos números.

-

-3, 3-4

Resta o cambio de signo (sólo Integer y Real).

abs()

-2.abs()

Valor absoluto.

max(n), min(n)

3.max(5)

Máximo y mínimo entre dos números.

<, <=, >, >=

3<=6

Comparación de dos números.

toString()

3.toString()

Conversión en una cadena de texto.

div(n)

15.div(4)

División entera (solo Integer y UnlimitedNatural).

mod(n)

15.mod(4)

Módulo de la división entera (solo Integer y UnlimitedNatural).

floor()

3.4.floor()

Truncar a entero (solo Real).

round()

3.4.round()

Redondear a entero (solo Real).

Booleanos
Los booleanos también tienen los operadores típicos. La mayoría se utiliza en notación infija:

Operación

Ejemplo

Detalles

or, xor

a or b

O no exclusiva / O exclusiva.

and

a and b

Y lógica.

not

not(false)

Negación lógica.

implies

a implies b

Equivalente a not(a) or (a and b).

If-then-else-endif

if a then b else c endif

Devuelve b o c en función de a

toString()

false.toString()

Conversión en una cadena de texto.

Hay que ir con especial cuidado con la función if-then-else-endif. El resultado de una expresión OCL if a then b else c endif es de tipo b si a es cierto y c si a es falso. Por lo tanto, tanto b como c tendrían que ser del mismo tipo. No hay que confundir esta expresión, pues, con implies, puesto que el resultado de evaluar if-then-else-endif no es necesariamente un booleano.
Ejemplo
La expresión siguiente es de tipo String, puesto que tanto si la condición es cierta como si es falsa devuelve un string
if <condición> then 'a' else 'b' endif
String

Operación

Ejemplo

Detalles

+

‘a’ + ‘b’

Concatenación de cadenas.

concat()

‘a’.concat(‘b’)

Equivalente a +.

size()

‘a’.size()

Número de caracteres.

substring(i,s)

‘hola’.substring(1,3)

Extrae una subcadena.

at(i)

‘hola’.at(2)

Carácter en la posición i.

indexOf(s)

‘hola’.indexOf(‘ol’)

Posición ocupada por un string dentro de otro.

toInteger, toReal, toBoolean

‘3’.toReal()

Conversión de tipo.

toUppercase, toLowercase

‘hola’.toUppercase()

Conversión a mayúsculas o minúsculas.

equalsIgnoreCase(s)

‘h’.equalsIgnoreCase(‘H’)

Comparación sin tener en cuenta mayúsculas o minúsculas.

<, <=, >, >=

‘a’ < ‘b’

Comparación.

OclAny
OCL define un tipo que es supertipo de todos los tipos posibles, incluyendo los tipos básicos vistos hasta ahora y cualquier otro (como por ejemplo clases o enumeraciones) que se defina en los modelos. Este tipo se denomina OclAny, y tiene algunas operaciones que, por lo tanto, se pueden usar sobre cualquier valor OCL:

Operación

Ejemplo

Detalles

=, <>

a = b

Comparación en función de si los dos valores son el mismo.

OCL es un lenguaje estáticamente tipado. Esto significa que cada expresión OCL se evalúa a un valor de cierto tipo y que este tipo se puede conocer antes de evaluar la expresión. Para trabajar con esta premisa, OCL ofrece una serie de operaciones en el tipo OclAny (y, por lo tanto, heredadas por todos los tipos):

Operación

Ejemplo

Detalles

oclType()

‘a’.oclType()

Devuelve el tipo del objeto sobre el que se invoca.

oclIsTypeOf(t)

‘a’.oclIsTypeOf(String)

Cierto si el objeto es exactamente del tipo indicado.

oclIsKindOf(t)

‘a’.oclIsKindOf(Any)

Cierto si el objeto es del tipo indicado o de uno de sus subtipos.

oclAsType(t)

p.oclAsType(Estudiante)

Devuelve una referencia al objeto del tipo indicado (siempre que sea de un tipo compatible).

OclVoid y OclInvalid
OCL también define dos valores especiales, cada uno con su propio tipo.
  • El valor null, de tipo OclVoid, representa la ausencia de valor.

  • El valor invalid, de tipo OclInvalid, representa un error de evaluación de una expresión.

Para poder trabajar, el tipo OclAny define dos operaciones de consulta:

Operación

Ejemplo

Detalles

oclIsUndefined

p.departament.oclIsUndefined()

Indica si un valor es null.

oclIsInvalid

‘a’.oclAsType(Real).oclIsInvalid()

Indica si un valor es invalid.

En el contexto de un trayecto, self.previo es el trayecto previo... que puede estar o no, ya que previamente tiene multiplicidad 0..1. Es decir, self podría tener un trayecto previo o no tenerlo. self.previo.oclIsUndefined será cierto si self no tiene trayecto previo y será falso en caso contrario.
De manera parecida, en el contexto de una ReservaHotel, self.observacionesHotel.oclIsUndefined será cierto si y solo si la ReservaHotel self no tiene observacionesHotel.
4.2.5.Variables
Como acabamos de ver, OCL nos permite simplificar expresiones complejas permitiéndonos definir propiedades y operaciones que después podemos utilizar en otras expresiones.
Sin embargo, en ocasiones solo queremos simplificar una expresión concreta y no queremos definir nuevas propiedades ni operaciones para hacerlo. Con este propósito, OCL nos permite definir variables que, a diferencia de las propiedades y operaciones, solo se pueden utilizar en la misma expresión donde se definen:
let nombre-variable:tipo-variable = valor-variable
in expresion- que- utiliza- nombre- variable
Supongamos, por ejemplo, que queremos documentar que la salida de un trayecto debe ser posterior a la llegada, pero que, por el motivo que sea, no queremos utilizar la operación anteriorA antes definida ni ninguna otra operación o propiedad que no esté en el diagrama UML. Podemos definir dos variables, fechaHoraSalida y fechaHoraLlegada, que nos permitan simplificar la expresión y hacerla más fácil de entender:
context Trayecto inv:
let fechaHoraSalida:FechaHora = self.salida.fecha in
let fechaHoraLlegada:FechaHora = self.llegada.fecha in
fechaHoraSalida < fechaHoraLlegada
4.2.6.Colecciones
OCL define un tipo Collection para las colecciones de valores. Este tipo presenta cuatro subtipos:
  • Set: colección de elementos sin elementos repetidos y sin orden.

  • OrderedSet: colección de elementos sin elementos repetidos pero en un orden concreto.

  • Bag: colección de elementos donde pueden haber repetidos y sin orden.

  • Sequence: colección de elementos donde pueden haber repetidos y con un orden concreto.

Cuando se indica el tipo de una propiedad (o de cualquier valor) para las colecciones, también se indica el tipo del elemento que contiene la colección. Así, se dice que una colección sin elementos repetidos ni orden de instancias de Reserva es de tipo Set(Reserva), mientras que una colección sin orden pero con elementos repetidos de cadenas de texto esLas colecciones sin elementos repetidos (Set y OrderedSet) no pueden contener más de una vez el mismo elemento. En cambio, las colecciones con elementos repetidos sí pueden contener el mismo elemento más de una vez.
Así, Set{1,2,5} es un set que contiene los elementos 1, 2 y 5 y es lo mismo que Set{2,5,1}, en el sentido de que no hay ningún orden. Bag{1,2,2,5} es un Bag que contiene los elementos 1, 2 (dos veces) y 5, y es lo mismo que Bag{5,2,1,2}, puesto que tampoco tiene orden.
En cambio, OrderedSet{1,2,5} no es lo mismo que OrderedSet{2,5,1}, puesto que, en este caso, el orden sí importa. De forma parecida, Sequence{1,2,2,5} no es lo mismo que Sequence{2,5,1,2}.
Las operaciones OCL de las colecciones tienen una notación diferente de las de los otros valores, puesto que, en lugar de indicar el uso de una operación mediante el punto (como self.edad), se utiliza una flecha (como self.reserva ->size()):
Supongamos, por ejemplo que para todo viaje, la colección de reservas del viaje debiera tener más de un elemento (todo viaje debiera tener como mínimo dos reservas):
context Viaje inv:
self.reserva->size()>1
Uso de propiedades y colecciones
Cuando se accede a una propiedad (normalmente un extremo de asociación) con multiplicidad máxima mayor que 1 (que no sea, por lo tanto, 0..1 ni 1), se obtiene una colección. Esta será un Set, a menos que la asociación tenga la propiedad {ordered} (y, por lo tanto, esté ordenada), en cuyo caso será un OrderedSet.
Así, en el contexto de un Cliente, self.viaje es un set(Viaje) que contiene los viajes que dicho cliente tiene asociados.
En el caso de las clases asociativas, también se puede usar el nombre de la clase asociativa.
Supongamos que tenemos el siguiente diagrama (una variante de la solución propuesta para las recomendaciones, donde Recomendacion es una clase asociativa):
75593m4010.gif
En el contexto de un Agente, self.recomendable es un Set(Recomendable) con todas las instancias de “Recomendable” que el agente tiene asociadas como recomendaciones, mientras que self.recomendacion es un Set(Recomendacion) con las recomendaciones.
Colección de todas las instancias de una clase
Hay un caso especial que debemos tener en cuenta: OCL define una operación allInstances() para las clases, que devuelve todas las instancias de aquella clase.
Así, Persona.allInstances() es un Set(Persona) que tiene todas las instancias de la clase Persona.
Select y reject
Una de las operaciones básicas sobre colecciones es la selección de un subconjunto de la colección. La sintaxis de esta operación es:
coleccion ->select(expresion-booleana)
La operación devuelve una colección del mismo tipo pero que solo incluye aquellas instancias que satisfacen la expresión booleana indicada. Opcionalmente, se puede indicar una variable para referirse al elemento actual a la hora de evaluar la expresión:
coleccion ->select(var | expresion-booleana-con-var)
La operación reject es similar a la operación select, solo que devuelve una colección que contiene aquellos elementos para los cuales la expresión booleana es falsa en lugar de cierta.
Veamos algunos ejemplos. Tened presente que en OCL, “--” indica un comentario de línea.
-- Reservas de un viaje tales que la fecha de inicio y la de fin coinciden:
context Viaje
def: reservasCortas:Set(Reserva) = self.reserva->select(fechaFin=fechaInicio)
En esta expresión, self.reserva es un Set(Reserva). Por lo tanto, self.reserva->select(fechaFin=fechaInicio) es también un Set(Reserva). En concreto, se trata del subconjunto de self.reserva formado por las reservas que cumplen la condición (que la fechaInicio y la fechaFin sean iguales).
-- Otra forma de definir la misma expresión:
context Viaje
def: reservasCortas:Set(Reserva) = self.reserva-> select(r|r.fechaFin=r.fechaInicio)
En este caso, se ha dado el nombre r a la reserva dentro de la condición. Esta expresión se puede leer como “La propiedad reservasCortas de tipo Set(Reserva) de la clase Viaje es el conjunto de reservas r del viaje tales que r.fechaFin es igual a r.fechaInicio”.
-- Todavía otra forma, esta vez con reject:
context Viaje
def: reservasCortas:Set(Reserva) = self.reserva->reject(fechaFin<>fechaInicio)
En este caso, se ha definido reservasCortas como el conjunto de reservas de un viaje excluyendo aquellas reservas en las que la fecha de inicio y la de finalización no coinciden.
collectNested y collect
La operación collectNested devuelve, a partir de una colección, otra colección con el mismo número de elementos. Más concretamente, la colección resultante se obtiene evaluando cada uno de los elementos de la colección original y poniendo el resultado de dicha evaluación (respetando el orden, si lo hubiera) en la colección resultante. Su sintaxis es:
coleccion ->collectNested(expresion)
coleccion ->collectNested(var | expresion-con-var)
Veamos un ejemplo.
-- Colección con todos los códigos de reserva de un viaje:
context Viaje
def: codigosReserva:Set(String) = self.reserva -> collectNested(r|r.codigo)
El resultado de collectNested es del de evaluar, para cada elemento r de self.reserva, la expresión r.codigo. Por lo tanto, se trata de un conjunto que contiene el código de cada una de las reservas que hay en self.reserva.
Puesto que el código es clave de reserva, el conjunto no tendrá repetidos y dado que, para una reserva, r.codigo es un String, el conjunto será de Strings. Por lo tanto, codigosReserva es de tipo Set(String).
La operación collect funciona de forma muy parecida a collectNested. Pero si el resultado de evaluar la expresión devuelve, para cada elemento, una colección, en lugar de devolver una colección de colecciones como haría collectNested, collect devuelve una única colección con los elementos de dentro de las colecciones resultantes.
Veamos un ejemplo sencillo.
Supongamos que tenemos un cliente cl con dos viajes, uno con una reserva de código a y el otro con dos reservas, de códigos b y c.
cl.viaje->collectNested(reserva) devuelve un conjunto de dos elementos: dos Set(Reserva) resultantes de evaluar reserva para cada uno de los dos viajes. El primer elemento contiene una reserva (a), mientras que el segundo elemento contiene dos (b y c).
Es decir, cl.viaje->collectNested(reserva)->collect(codigo) devuelve Set{Set{“a”},Set{“b”,”c”}}
En cambio, cl.viaje->collect(reserva) devuelve un único conjunto con las tres reservas. Por lo tanto, cl.viaje->collect(reserva)->collect(codigo) devolvería Set{“a”,“b”,”c”}.
En el caso de que collectNested aplique una expresión que no devuelva colecciones, puesto que sería equivalente a collect, es preferible utilizar la operación collect. Así, el ejemplo de collectNested mostrado al principio se podría reescribir de la siguiente forma:
context Viaje
def: codigosReserva:Set(String) = self.reserva->collect(r|r.codigo)
Dada la frecuencia con que se usan propiedades en las expresiones OCL, este lenguaje define una forma alternativa y más corta de usar la operación collect: el carácter del punto (que hemos ido utilizando a lo largo del presente capítulo) sirve para indicar que se está aplicando collect con la propiedad indicada como expresión:
Así, coleccion.propiedad es equivalente a coleccion->collect(v|v.propiedad)
-- Por lo tanto, el último ejemplo de los códigos de reserva se puede reescribir de la forma siguiente:
context Viaje
def: codigosReserva:Set(String) = self.reserva.codigo
forAll y exists
La operación forAll devuelve cierto si (y solo si) todos los elementos de una colección satisfacen cierta condición booleana. Del mismo modo, la operación exists devuelve cierto si (y solo si) alguno de los elementos de una colección satisface cierta expresión booleana. Sus sintaxis son:
coleccion->forAll(expresion- booleana)
coleccion->forAll(v | expresion- booleana- con-v)
coleccion->exists(expresion-booleana)
coleccion-> exists(v | expresion-booleana-con-v)
Veamos algunos ejemplos.
context Cliente
def: tieneViajesABcn:Boolean = self.viaje -> exists(v|v.destino.nombre=“Barcelona”)
La propiedad tieneViajesABcn (de tipo Boolean) de la clase Cliente se define como la existencia de al menos un viaje v entre los viajes del cliente tal que el nombre del destino de v es igual a “Barcelona”.
context Cliente def: soloViajaABcn:Boolean =
self.viaje->forAll(v|v.destino.nombre=”Barcelona”)
La propiedad soloViajaABcn, en cambio, es cierta si (y solo si) todos los viajes del cliente (self) son a destinos llamados “Barcelona”.
Otras operaciones básicas
Además de las operaciones ya estudiadas, las colecciones OCL ofrecen otro conjunto de operaciones que se consideran básicas.
Las pertenecientes al tipo Collection (y, por lo tanto, presentes en todas las colecciones) son:

Operación

Ejemplo

Detalles

size()

v.reserva->size()

Número de elementos.

isEmpty(), notEmpty()

v.reserva->isEmpty()

Indica si la colección está vacía o no.

includes(o), excludes(o)

d->includes(“Barcelona”)

Indica si la colección incluye el objeto o no.

count(o)

dests->count(“Barcelona”)

Devuelve el número de veces que aparece el objeto en la colección.

includesAll(c), excludesAll(c)

d->includesAll(prefDests)

Indica si una colección incluye o excluye todos los elementos de otra.

max, min, sum

v.reserva.fechaInicio->min()

Devuelve el máximo, mínimo o suma de los valores de una colección (tienen que ser valores numéricos).

Veamos algunos ejemplos:
context Cliente
def:numViajesABerlin:Integer =
self.viaje -> select(v|v.destino.nombre=”Berlin”) -> size()
La propiedad entera numViajesABerlin de un cliente es el número de viajes v que ha hecho el cliente tales que v.destino.nombre es “Berlin”. Para obtener este resultado se han cogido todos los viajes de self, se han seleccionado los que son a un destino llamada “Berlin” y se ha comprobado cuántos quedaban en el conjunto seleccionado.
context Cliente
def: numViajesATokio:Integer = self.viaje.destino.nombre->count(“Tokio”)
Este ejemplo se parece al anterior, pero utiliza otra operación para obtener el mismo resultado. Para calcular el resultado, se obtiene el Bag(String) de todos los nombres de destinos a los que el cliente ha viajado (donde cada nombre aparece tantas veces como viajes ha hecho el cliente) y se cuenta cuántas veces aparece el nombre “Tokio”.
context Cliente
def: conoceNuevaYork:Boolean = self.viaje.destino.nombre->includes(“Nueva York”)
La propiedad conoceNuevaYork es cierta si (y solo si) el conjunto de nombres de destino de los viajes de self incluye “Nueva York”.
Para obtener este resultado, se hace un collect de los nombres de los destinos de los viajes de self (recordad que self.viaje.destino.nombre es equivalente a self.viaje- >collect(destino)->collect(nombre)). Puesto que cada viaje tiene un destino pero dos viajes pueden tener el mismo destino, self.viaje.destino será un Bag(Destino). Sobre este Bag hacemos un collect de los nombres y, por lo tanto, tenemos un Bag(String) donde cada nombre aparece tantas veces como viajes se han hecho a un destino con ese nombre. Finalmente, si este Bag contiene el nombre “Nueva York”, es que alguno de los viajes del cliente ha sido a un destino llamado así.
context Cliente
def: primerViaje:Fecha = self.viaje.fechaInicio->min()
En este caso, primerViaje se define como la mínima de las fechas de inicio de los viajes de self; es decir, la fecha anterior, la del primer viaje (en orden cronológico).
Existen ciertas operaciones que nos permiten transformar las colecciones de un tipo a otro. Hay que tener en cuenta que:
  • Cuando pasamos de una colección con repetidos a una que no los tiene, se eliminan los repetidos.

  • Cuando pasamos de una colección sin orden a una con orden, el orden no está definido. En el caso contrario, sencillamente se pierde el orden de los elementos.

Operación

asSet()

asOrderedSet()

asSequence()

asBag()

flatten()

Veamos un ejemplo:
context Cliente
def destinosDiferentes:Set(String) = self.viaje.destino.nombre -> asSet()
Como hemos argumentado anteriormente, self.viaje.destino.nombre es un Bag(String) con tantos nombres de destino como viajes ha hecho el cliente. La operación asSet() devuelve un conjunto sin repetidos, donde cada nombre de destino solo aparece una vez, aunque estuviera repetido en el Bag(String) original.
Por otro lado, flatten() toma una colección que contiene otras colecciones y devuelve una colección con los elementos de dentro de dichas colecciones. En el caso de haber invocado la operación collect, dado que esta ya allana las colecciones, aquellas colecciones que obtengamos haciendo collect no habrá que allanarlas a mano mediante flatten. Veamos un ejemplo:
Una posible expresión para obtener los códigos de reserva de un cliente puede ser:
context Cliente
def codigosReserva:Set(String) = self.viaje.reserva.codigo ->asSet()
La expresión self.viaje.reserva.codigo es equivalente a self.viaje->collect(reserva.codigo) y obtiene un conjunto de todos los códigos de las reservas de todos los viajes del cliente. Este conjunto ya está allanado, en el sentido de que no obtiene un conjunto donde cada elemento sea otro conjunto de códigos de las reservas del viaje.
El equivalente usando flatten es:
context Cliente
def codigosReserva:Set(String) =
self.viaje -> collectNested(reserva.codigo) -> flatten() -> asSet()
Operaciones sin orden
Los Set tienen las siguientes operaciones básicas adicionales:

Operación

Detalles

union(s:Set(T)): Set(T)

Unión de self y s.

union(b:Bag(T)): Bag(T)

Unión de self y b.

intersection(s:Set(T)): Set(T)

Intersección de self y s.

intersection(b:Bag(T)): Bag(T)

Intersección de self y b.

- (s:Set(T)): Set(T)

Elementos de self que no están en s.

symmetricDifference(s:Set(T)): Set(T)

Elementos que están en self o en s pero no en los dos.

including(o:T): Set(T)

Conjunto de los elementos de self y, además, o.

excluding(o:T): Set(T)

Conjunto de todos los elementos de self excepto o.

Los Bag definen casi las mismas operaciones básicas adicionales que los Set:

Operación

Detalles

union(s:Set(T)): Bag(T)

Unión de self y s.

union(b:Bag(T)): Bag(T)

Unión de self y b.

intersection(s:Set(T)): Bag(T)

Intersección de self y s.

intersection(b:Bag(T)): Bag(T)

Intersección de self y b.

including(o:T): Set(T)

Conjunto de los elementos de self y, además, o.

excluding(o:T): Set(T)

Conjunto de todos los elementos de self excepto todas las ocurrencias de o.

Operaciones con orden
Los OrderedSet definen las operaciones básicas adicionales siguientes:

Operación

Detalles

append(o:T):OrderedSet(T)

Conjunto de los elementos de self seguidos de o.

prepend(o:T): OrderedSet(T)

Conjunto formato por o seguido de los elementos de self.

insertAt(i:Integer,o:T): OrderedSet(T)

Conjunto de los elementos de self con o insertado en la posición i.

subOrderedSet(i:Integer, f:Integer):OrderedSet(T)

Subconjunto de self entre las posiciones i y f.

at(i:Integer): T

Elemento que ocupa la posición i de self.

indexOf(o:T): Integer

Posición que ocupa el elemento o en self.

first():T, last():T

Primer/Último elemento de self.

reverse(): OrderedSet(T)

Los elementos de self en el orden opuesto.

Las Sequence tienen las operaciones básicas adicionales listadas a continuación:

Operación

Detalles

union(s:Sequence(T)): Sequence(T)

La secuencia formada por self seguida de los elementos de s.

append(o:T): Sequence (T)

Secuencia de los elementos de self seguidos de o.

prepend(o:T): Sequence (T)

Secuencia formada por o seguido de los elementos de self.

insertAt(i:Integer,o:T): Sequence (T)

Secuencia de los elementos de self con o insertado en la posición i.

including(o:T):Sequence(T)

Sinónimo de la operación append.

excluding(o:T): Sequence(T)

Secuencia de todos los elementos de self salvo todas las ocurrencias de o.

subSequence(i:Integer, f:Integer): Sequence (T)

Subsecuencia de self entre las posiciones i y f.

at(i:Integer): T

Elemento que ocupa la posición i de self.

indexOf(o:T): Integer

Posición que ocupa el elemento o en self.

first():T, last():T

Primer/Último elemento de self.

reverse(): Sequence(T)

Los elementos de self en el orden opuesto.

Operaciones de iteración
Además de las operaciones básicas indicadas, las colecciones OCL tienen unas operaciones que denominamos de iteración, puesto que todas ellas se pueden expresar como una iteración sobre los elementos de la colección original buscando, contando o acumulando cierto resultado.
Como select y reject, todas estas operaciones toman una expresión y, opcionalmente, un nombre de variable para el elemento que se evalúa:
coleccion-> operacion(e | expresion-con-e)

Operación

Detalles

isUnique

Devuelve cierto si la expresión se evalúa con un valor diferente para cada elemento de la colección.

any

Devuelve un elemento cualquiera para el cual la expresión se evalúe a cierta, o a null si no hay ninguno.

one

Devuelve cierto si y solo si exactamente uno de los elementos de self evalúa la expresión a cierta.

sortedBy

Devuelve una colección ordenada donde los elementos de self están ordenados según el resultado de evaluar la expresión.

Veamos algunos ejemplos:
context Destino inv: self.hotel->isUnique(nombre)
En este caso, se ha definido un invariante. Se indica que, para todo destino, en su conjunto de hoteles, el nombre tiene que ser único; es decir, que no puede haber más de un hotel con el mismo nombre en un mismo destino.
context Persona inv: Persona.allInstances()->isUnique(email)
Este otro invariante nos indica la clave de persona: del conjunto de todas las instancias de persona, la dirección de correo debe ser única.
context Cliente
def: unViajeARoma:Viaje = self.viaje->any(v | v.destino.nombre=”Roma”)
La expresión devuelve un viaje cualquiera del conjunto self.viaje tal que el nombre del destino es “Roma”. En caso de que no haya ninguno, devuelve null.
context Cliente
def: soloUnaVezATarragona:Boolean = self.viaje.destino.one(d | d.nombre=”Tarragona”)
La expresión self.viaje.destino contiene el conjunto allanado de destinos de los viajes de self, posiblemente con repetidos. La propiedad soloUnaVezATarragona es cierta si y solo si solo uno de los destinos de este conjunto tiene por nombre “Tarragona”.
context Cliente
def: viajesPorFechaInicio:OrderedSet(Viaje) = self.viaje->sortedBy(v|v.fechaInicio)
La propiedad viajesPorFechaInicio es un conjunto ordenado con los mismos viajes que self.viaje, pero ordenados por fechaInicio.

5.Caso práctico

A lo largo del módulo hemos presentado cinco escenarios de documentación: lista de requisitos estilo IEEE-830, UML y OCL, historias de usuario, pruebas automatizadas de aceptación y casos de uso.
El estándar IEEE-830 define un modelo de documento mucho más amplio de lo que hemos tratado en este módulo, por lo que no lo veremos aplicado al caso práctico.
Así pues, a continuación veremos la documentación de requisitos del caso práctico en dos escenarios: el modelo del dominio en UML con restricciones en OCL y las historias de usuario.

5.1.Documentación formal y exhaustiva: modelo del dominio en UML y OCL

Supongamos que queremos hacer una documentación exhaustiva y formal del modelo del dominio de Viajes UOC.
Para elaborar un modelo del dominio, es preciso obtener información muy detallada, lo cual requerirá más reuniones con las personas que conocen bien el dominio. Sin embargo, en ocasiones podemos utilizar otras fuentes a partir de las cuales recoger información para elaborar el modelo del dominio.
En el caso de Viajes UOC, el estudio del plan de viaje que nos facilita Joan Salvat, junto con las notas que tomemos cuando lo entrevistemos, nos ayudarán a elaborar buena parte del modelo del dominio:
75593m4011z.gif
5.1.1.Modelo del dominio en UML
Para no incluir información cuya fuente no esté clara, a continuación se muestra el modelo del dominio resultante de incluir solo aquello que aparece en el plan de viaje y la información referente a las recomendaciones, que es la otra parte del sistema que ya hemos analizado con detalle.
Debemos tener en cuenta que los agentes de viajes pueden hacer recomendaciones sobre los diferentes hoteles y destinos, y que estas recomendaciones tienen un sentido (los recomiendan o los desaconsejan) y, opcionalmente, un comentario (que es lo que se muestra en el plan de viajes si no se hace un comentario específico para esta reserva). Por otro lado, dado que el medio de comunicación principal será el correo electrónico, no se desea que dos personas (sean clientes o agentes) puedan tener el mismo e-mail. Por último, los hoteles pertenecen a un destino (que, obviamente, deberá coincidir con el destino del viaje).
En el plan de viaje vemos que conocemos una serie de datos comunes tanto de clientes como de agentes, que son el nombre y una dirección de correo. Por lo tanto, generalizamos Agente y Cliente en Persona, donde ponemos estos dos atributos. A pesar de que parezca que también tienen dirección y teléfono comunes, una nota en el plan de viaje nos indica que la dirección y el teléfono mostrados en el recuadro “Agente” son, en realidad, los de la agencia. Por lo tanto, los atributos dirección y teléfono los ponemos en la clase Cliente.
75593m4012z.gif
De esta misma nota se desprende que un agente pertenece a una agencia (y solo a una), que tiene dirección y teléfono. Puesto que no se dice lo contrario en ninguna parte, no ponemos ninguna restricción al número de agentes que puede haber en una agencia y, por lo tanto, consideramos que puede ser cualquier número (tal y como indica la multiplicidad en el lado Agente).
El plan de viajes del que disponemos muestra claramente los datos de un viaje, que representamos con la clase Viaje. Cada viaje tiene un destino que tiene un nombre, en el ejemplo, “Santiago de Chile”. También tiene un cliente y un agente, que representamos como asociaciones hacia las clases correspondientes con multiplicidad 1 (y multiplicidad * en el lado Viaje, puesto que no se nos dice que haya ninguna restricción al respeto). El viaje también tiene unas fechas de inicio y de fin, pero, según las notas que tenemos en el plan de viajes, son derivadas de las fechas de las reservas que lo forman.
Finalmente, un viaje tiene un conjunto de reservas. Dado que una reserva solo tiene sentido dentro de un viaje y no tiene sentido pensar en mover una reserva de un viaje a otro, consideramos que la asociación entre Viaje y Reserva es una composición. No se nos dice nada del número de reservas que pueden formar parte de un viaje (y, por lo tanto, ponemos multiplicidad *), a pesar de que también habríamos podido presuponer que como mínimo tiene que haber una (o dos si tenemos en cuenta un billete de ida y otro de vuelta); de cualquier modo, en caso de duda, mejor no introducir restricciones de las que no estemos seguros.
Las reservas de un viaje pueden ser vuelos u hoteles. En ambos casos, conocemos las fechas de inicio y de fin (a pesar de que en el caso de los vuelos se denominen fechas de salida y de llegada) y un código.
Las reservas de hoteles tienen una hora de check-in y de check-out, un tipo de habitación (que solo puede ser “individual” o “doble”), unas observaciones y una opinión del agente.
En el plan de viaje hay una nota que nos indica que el agente puede escribir una opinión para un viaje específico (lo que reflejamos en el atributo observacionesAgente de la clase ReservaHotel), así como una opinión sobre el hotel. Sin embargo, tal como hemos indicado, cuando se habla de recomendaciones, el agente puede emitir una opinión positiva o negativa y con un comentario no solo de los hoteles, sino también de los destinos. Por lo tanto, hacemos una generalización de las clases Hotel y Destino en una clase Recomendable, y creamos la clase Recomendación, cuyas instancias se asocian a un agente y a un Recomendable. Esta clase tiene un atributo que indica el sentido (positivo o no) de la recomendación y otro con el comentario.
Por lo que respecta a las reservas de vuelo, además de las características en común con el resto de las reservas, tienen varios trayectos. En el ejemplo, el primero vuelo tiene dos trayectos: uno Barcelona-Madrid y otro Madrid-Santiago. Cada trayecto tiene un número de vuelo, una fecha y hora de salida, una ciudad, aeropuerto y terminal de salida, una fecha y hora de llegada y una ciudad, aeropuerto y terminal de llegada. Para evitar duplicar el número de atributos, dado que tenemos el mismo conjunto de atributos (fecha y hora, ciudad, aeropuerto, terminal) tanto para la salida como para la llegada, creamos un tipo de datos (ElementoTrayecto) que agrupa estos atributos.
Cuando una reserva tiene más de un vuelo, entre un vuelo y el siguiente puede ser que haya enlace (presupondremos que no siempre tiene que ser así). Por lo tanto, un vuelo puede enlazar con un vuelo o con ninguno y puede tener un vuelo enlazado previo o ninguno. Cuando hay un enlace, queremos saber el tiempo de enlace, que es la diferencia entre la fecha y hora de llegada del vuelo previo y la de salida del vuelo de enlace (y, por lo tanto, será información derivada).
5.1.2.Restricciones de integridad e información derivada
Restricciones de integridad e información derivada en lenguaje natural
Para documentar las claves, otras restricciones de integridad y la información derivada, haremos ciertas presuposiciones, puesto que no toda la información necesaria está disponible en las entrevistas, en el plan de vuelo o en los casos de uso detallados. Deberemos confirmar estas suposiciones lo antes posible con las personas expertas en el dominio.
Claves:
- Agencia: dirección y teléfono (tiene dos claves)
Hemos supuesto que no puede haber dos agencias diferentes en la misma dirección y que dos agencias diferentes no pueden tener el mismo teléfono.
- Persona: email
- Destino: nombre
Hemos supuesto que todo el mundo se refiere a los destinos por el nombre y que, por lo tanto, no puede haber dos destinos con el mismo nombre.
- Hotel: destino + nombre
Del mismo modo, hemos supuesto que no puede haber dos hoteles con el mismo nombre en el mismo destino.
- Viaje: cliente + fechaInicio
No está demasiado claro cuál puede ser la clave del viaje. Hemos supuesto que un mismo cliente no puede tener dos viajes en la misma fecha.
- Reserva: viaje + código
A pesar de que no se indica en ninguna parte, hemos supuesto que no puede haber dos reservas con el mismo código en un mismo viaje.
- Trayecto: reserva + fecha del elemento de salida
Otras restricciones de integridad:
- La fecha de fin de una reserva debe ser posterior a su fecha de inicio.
- La salida de un trayecto tiene que ser posterior a la llegada
- Las reservas de un mismo viaje tienen que ser consecutivas, de modo que si las ordenamos por fecha, cada reserva tiene fecha de inicio posterior o igual a la fecha de fin de la reserva anterior.
- Los trayectos de una misma reserva deben ser consecutivos, de modo que, si los ordenamos por fecha, cada trayecto tiene la salida más tarde que la llegada del trayecto anterior.
- La asociación de un trayecto con sus posterior y previo es simétrica; es decir, si un trayecto t1 tiene como posterior un trayecto t2, entonces t2 tiene que tener como previo el trayecto t1.
- Los billetes que se enlazan deben ser de la misma reserva; la salida del vuelo de enlace tiene que ser desde la misma ciudad y aeropuerto que la llegada del vuelo previo y tiene que salir más tarde que el vuelo previo.
- Para las reservas de vuelo, la fecha de inicio debe ser la fecha de salida del primer vuelo y la fecha de fin tiene que ser la fecha de llegada del último vuelo.
- Los hoteles reservados en un viaje tienen que ser del mismo destino que el viaje.
Información derivada:
- La fecha de inicio de un viaje es la fecha de inicio de la primera reserva del viaje.
- La fecha de fin de un viaje es la fecha de finalización de la última reserva del viaje.
Restricciones de integridad e información derivada en OCL
Si necesitamos expresar las restricciones de integridad y las reglas de derivación de Viajes UOC de forma estándar y más formal, en lugar de usar el lenguaje natural, podemos utilizar el lenguaje OCL.
Como ya hemos indicado, debemos tener en cuenta que una misma restricción o regla de derivación se puede expresar con expresiones OCL diferentes y que, por lo tanto, la que presentamos a continuación no es la única manera válida de expresar las restricciones del ejemplo.
Claves
-- Agencia: dirección y teléfono (tiene dos claves)
context Agencia inv: Agencia.allInstances()->isUnique(direccion)
context Agencia inv: Agencia.allInstances()->isUnique(telefono)
-- Persona: email
context Persona inv: Persona.allInstances()->isUnique(email)
-- Destino: nombre
context Destino inv: Destino.allInstances()->isUnique(nombre)
-- Hotel: destino + nombre
-- Es decir, para cada destino, si cogemos la colección de hoteles en aquel
-- destino, el nombre tiene que ser único.
context Destino inv: self.hotel->isUnique(nombre)
-- Viaje: cliente + fechaInicio
-- Del mismo modo, la fecha de inicio de un viaje tiene que ser única para todo cliente
-- entre la colección de los viajes de dicho cliente.
context Cliente inv: self.viaje->isUnique(fechaInicio)
-- Reserva: viaje + codigo
context Viaje inv: self.reserva->isUnique(codigo)
-- Trayecto: reserva + fecha del elemento de salida
-- En este caso, si de cada trayecto de cualquier reserva hacemos una tupla
-- formada por la fecha de salida, no puede ser que encontremos dos tuplas iguales.
context ReservaVuelo inv:
self.trayecto->isUnique(t | t.salida.fecha)
Otras restricciones de integridad
A menudo, para definir restricciones de integridad sobre trayectos, necesitamos comparar dos elementos de trayecto para saber cuál es antes de qué otro. Para evitar complicar las expresiones que usaremos, definimos una nueva operación:
context ElementoTrayecto def: anteriorA(e:ElementoTrayecto) =
self.fecha < e.fecha
También nos irá bien expresar fácilmente cuando dos trayectos son consecutivos sin solapamientos:
context Trayecto def: noSolapadoCon(t:Trayecto) =
self.llegada.anteriorA(t.salida) or t.llegada.anteriorA(self.salida)
Con las nuevas operaciones definidas, ya podemos expresar las restricciones:
-- La fecha de finalización de una reserva debe ser posterior a su fecha de inicio
context Reserva inv: self.fechaFin> self.fechaInicio
-- La salida de un trayecto tiene que ser posterior a la llegada
context Trayecto inv: self.salida.anteriorA(self.llegada)
-- Las reservas de un mismo viaje deben ser consecutivas, de modo que si las
-- ordenamos por fecha, cada reserva tiene fecha de inicio posterior o igual a la fecha de
-- finalización de la reserva anterior.
context Viaje inv:
self.reserva->forAll(r1,r2 | r1<>r2 implies
r1.fechaFin< r2.fechaInicio or r2.fechaFin< r1.fechaInicio)
-- Los trayectos de una misma reserva tienen que ser consecutivos, de modo que si
-- los ordenamos por fecha, cada trayecto tiene la salida más tarde que la llegada del
-- trayecto anterior.
context ReservaVuelo inv:
self.trayecto -> forAll(t1,t2 | t1<>t2 implies
t1.llegada.anteriorA(t2.salida) or t2.llegada.anteriorA(t1.salida))
-- La asociación recursiva de la clase Trayecto es simétrica + Los billetes que
-- se enlazan tienen que ser de la misma reserva +
-- En los billetes que se enlazan, la salida del vuelo de enlace debe ser desde la
-- misma ciudad y aeropuerto que la llegada del vuelo previo + en los billetes que
-- se enlazan, el vuelo de enlace tiene que salir más tarde que el vuelo previo.
context Trayecto inv:
let l: ElementoTrayecto = self.llegada in
let s: ElementoTrayecto = self.posterior.salida in
self.posterior->notEmpty() implies
(self. posterior.previo = self and
self.reservaVuelo = self.posterior.reservaVuelo and
l.ciudad = s.ciudad and l.aeropuerto = s.aeropuerto and
l.anteriorA(s)
)
-- Para las reservas de vuelo, la fecha de inicio tiene que ser la fecha de salida del primer vuelo
-- y la fecha de finalización tiene que ser la fecha de llegada del último vuelo.
-- (Supondremos que el tipo Fecha se puede comparar con el tipo FechaHora)
context ReservaVuelo inv:
let trayectosOrdenados:OrderedSet(Trayecto)
= self.trayecto->sortedBy(t|t.salida.fecha) in
self.fechaInicio = trayectosOrdenados->first().salida.fecha and
self.fechaFin = trayectosOrdenados->last().llegada.fecha
-- Los hoteles reservados en un viaje tienen que ser del mismo destino que el viaje
context ReservaHotel inv:
self.hotel.destino = self.viaje.destino
Información derivada
-- La fecha de inicio de un viaje es la fecha de inicio de la primera reserva del viaje
context Viaje::fechaInicio:Fecha derive:
self.reserva.fechaInicio->min()
-- La fecha de finalización de un viaje es la fecha de finalización de la última reserva del viaje
context Viaje::fechaFin:Fecha derive:
self.reserva.fechaFin ->max()

5.2.Historias de usuario

Las historias de usuario están formadas por tres componentes: nombre, conversaciones y criterios de aceptación. En el caso de Viajes UOC, ya hemos visto el nombre de las historias de usuario y algunas de las conversaciones que se llevan a cabo para aclarar los detalles y proceder a su estimación. Así pues, solo queda documentar los criterios de aceptación de cada historia de usuario.
A continuación tenemos las tres historias de usuario que fueron escogidas como las mas prioritarias y proponemos criterios de aceptación para ellas.
1) Como agente de viajes, quiero añadir una opinión sobre un destino.
  • El agente puede escoger el destino de una lista predefinida.

  • La opinión se publica en cuanto se envía.

  • La opinión queda asociada al agente y al destino.

2) Como comercial, quiero hacer una oferta de viaje a un destino concreto.
  • El comercial puede escoger el destino de una lista predefinida.

  • Una vez hecha la oferta, esta queda asociada al destino y se registran las fechas de vigencia de la oferta.

3) Como propietario de una agencia, quiero hacer ofertas a usuarios concretos.
  • El propietario puede escoger al cliente de una lista de clientes de la agencia o elegir su nombre de usuario.

  • La oferta consiste en un tanto por ciento de descuento y es acumulable a otras ofertas.

  • La oferta solo está disponible si nos identificamos como el usuario en concreto para quien se ha hecho la oferta.

Resumen

En este módulo hemos estudiado que una buena especificación de requisitos debe ser correcta, inambigua, completa, consistente, ordenada, verificable, modificable y trazable.
En este sentido, el uso de lenguajes formales de especificación puede ayudar, ya que es más fácil evitar la ambigüedad, inconsistencia o carencia de verificabilidad si utilizamos un lenguaje que una máquina pueda interpretar. Como contrapunto, perderemos la expresividad del lenguaje natural.
También hemos visto que, en ocasiones, la documentación escrita dificulta la comunicación, por lo que nos interesa poder elegir entre diferentes estilos de documentación. Hemos visto varios escenarios de documentación según si se trata de documentación exhaustiva o no exhaustiva y de si se usa un lenguaje formal o no.
Hemos profundizado, también, en el estudio de los casos de uso, analizando cómo podemos adaptarlos a los diferentes escenarios antes mencionados (utilizando lenguaje natural, o lenguajes formales y detallándolos con más o menos exhaustividad). Sin embargo, esta gran flexibilidad hace que a veces se abuse de la técnica de los casos de uso, por lo que hemos estudiado los errores más habituales en la utilización de los casos de uso.
Finalmente, hemos visto el lenguaje OCL (object constraint language) y cómo nos puede ayudar a expresar formalmente algunas de las restricciones de integridad de un modelo del dominio.

Actividades

1. Suponed que queremos modelar con la orientación a objetos un servicio de música por Internet que ofrece a sus suscriptores la posibilidad de escuchar música desde cualquier lugar.
El servicio dispone de una serie de canciones, como por ejemplo:
  • Una canción titulada Sing, sing, sing, de Benny Goodman, con una duración de 8:40, perteneciente al álbum The Essential Benny Goodman.

  • Una canción titulada Shout, sister, shout, de Sister Rosetta Tharpe y Lucky Millinder and his Orchestra, de 2:44, perteneciente al álbum The A-Z of Jazz.

El servicio dispone de información adicional tanto de los artistas como de los álbumes. Así, por ejemplo:
  • El artista de nombre Benny Goodman tiene una pequeña biografía que nos explica que debutó en la era del swing y aparece una fotografía suya. Está relacionado con los artistas Glenn Miller, Duke Ellington y Ella Fitzgerald.

  • Hay una fotografía del artista Lucky Millinder and his Orchestra, pero no hay ninguna biografía en el sistema ni artistas relacionados.

  • La artista Sister Rosetta Tharpe tiene una fotografía y una pequeña biografía que la sitúa entre los cantantes de gospel de su generación. Está relacionada con los artistas Bassie Smith y Fats Waller.

  • El álbum The Essential Benny Goodman tiene una fotografía de la portada, una reseña de la crítica y una lista ordenada de cuarenta canciones, donde Sing, sing, sing ocupa la sexta posición.

Algunas canciones del sistema se pueden comprar en algunos países para poder escucharlas incluso sin conexión a Internet. Para estas, el sistema tiene un precio, que es diferente en cada país, y una URL de descarga. Así, por ejemplo:
  • Sing, sing, sing está a la venta en Estados Unidos y en Francia. En Estados Unidos cuesta 0,99 $, mientras que en Francia cuesta 0,79 €. También tiene un enlace de descarga.

Se pide lo siguiente:
a) El diagrama de clases que represente el dominio descrito.
b) Las restricciones de integridad que no se hayan podido expresar gráficamente en lenguaje natural y en OCL.
2. Suponed que queremos modelar en UML el dominio de los diccionarios. Utilizad esta captura de pantalla como guía:
75593m4013z.gif
Suponed que no es necesario descomponer en partes más pequeñas lo que no está marcado. Por ejemplo, se puede considerar que la definición va desde “Alguien, la gente...” hasta “... a muerte”.
Tened en cuenta que las áreas temáticas y las categorías gramaticales solo pueden ser las de unas determinadas listas. Utilizad el lenguaje OCL para documentar las restricciones de integridad que no podáis expresar gráficamente.
3. Partiendo del modelo del dominio UML que se muestra a continuación, definid las expresiones OCL que se piden:
75593m4014z.gif
Suponed que los tipos Dinero, Porcentaje, etc. se comportan como enteros.
a) La regla de derivación que dice que el total de una línea de factura se calcula como el importe más el porcentaje de IVA.
b) Una restricción de integridad que dice que el importe de una línea de factura debe ser mayor que cero.
c) Una nueva propiedad booleana para los proyectos llamada pendienteFacturar, que es cierta si y solo si la fecha de la última entrega del proyecto es posterior a la fecha de la última factura.
d) Una nueva propiedad booleana que dice que una factura está pagada si y solo si la fecha de pago no es null.
e) Una nueva propiedad en Proyecto que sea el conjunto de facturas no pagadas.
f) Una nueva propiedad listadoProyectos, que, para los clientes, tenga el conjunto de nombres de proyectos del cliente.
g) La regla de derivación de facturasPagadas, que es cierto si y solo si todas las facturas del proyecto tienen pagada a cierto.
4. Haced la descripción detallada del caso o casos de uso para el ámbito general y para el ámbito de organización que identifiquéis en las entrevistas. Utilizad la misma plantilla utilizada en los materiales: identificador del caso de uso, actor principal, actores de apoyo, nivel, ámbito, escenario principal de éxito y escenarios alternativos.
5. En la entrevista a Imma, se le pide que explique con más detalle cómo funcionará el crowdfunding en CrowdArt. Haced una lista de casos de uso a nivel de usuario que extraigáis de la entrevista a Imma a partir de ese punto. Indicad qué actor inicia cada caso de uso. No incluyáis las funcionalidades mencionadas en el último párrafo (sobre comunicación), puesto que no están lo bastante detalladas.
6. Para continuar con el análisis de la financiación de proyectos artísticos, fijaos ahora en unos esbozos de pantallas de los que disponemos, que nos proporcionan más información sobre los detalles de los datos que hay que tener en cuenta.
A continuación podemos ver el esbozo de una pantalla de detalle de un proyecto:
1) Título del proyecto.
2) Imagen.
3) Avatar y nombre del artista que lo ha propuesto.
4) Solo en caso de que el proyecto sea escénico, lugar, aforo y sesiones programadas.
5) Descripción del proyecto (texto con formato).
6) Para cada duda, se muestran el avatar y nombre de usuario que hizo la pregunta, fecha y texto de la pregunta y fecha y texto de la respuesta (si la hay). Podemos ver la página de perfil de un usuario haciendo clic sobre su nombre.
7) Estado del proyecto y datos sobre el capital conseguido (11.425 €), el objetivo (17.500 €), los días restantes para financiarlo, el día en que se creó, un resumen del proyecto y el número de artículos y comentarios que enlazan con el bloque del proyecto. Los posibles estados del proyecto pueden ser: propuesta, buscando financiación, éxito, fallido, no aceptado.
8) Vídeo del proyecto (opcional).
9) Importe y descripción de cada aportación. Algunas tienen un límite en el número de aportaciones, y se indica cuántas quedan. No puede haber dos aportaciones por el mismo importe en el proyecto.
75593m4015.gif
a) Haced el diagrama de clases UML correspondiente al modelo del dominio de la información mostrada en el esbozo de pantalla “Detalle de proyecto”.
  • No modeléis ninguna pieza de información que no aparezca en las pantallas.

  • Si pensáis que alguna información de la que aparece en el esbozo es derivada de información que se tiene que conocer pero que no aparece, indicadla igualmente como derivada. Por ejemplo, el número de artículos asociados a un proyecto se calcula a partir de los artículos que el proyecto tiene asociados, que no aparecen aquí; por lo tanto, en el diagrama UML indicaremos que este número de artículos es derivado.

b) Indicad las claves de las clases del dominio y otras restricciones de integridad textuales que pueda haber, así como las reglas de derivación de la información derivada.
c) Reescribid las restricciones de integridad y reglas de derivación usando el lenguaje OCL.
  • Haced las suposiciones que creáis convenientes para tratar las fechas y otros tipos de datos como si fueran numéricos en OCL.

  • En particular, suponed que restando dos fechas obtenéis el número de días de diferencia y que podéis obtener fechas con N días de diferenciasumando o restando enteros de fechas.

7. Fijaos ahora en el esbozo de la pantalla “Blog del proyecto”, donde vemos los artículos asociados a un proyecto.
1) Título y fecha del artículo.
2) Texto del artículo (con formato).
3) Comentarios. De cada uno, nombre de usuario de quien lo hace, avatar, fecha y comentario. Los comentarios no se relacionan unos con otros como si fueran preguntas y respuestas. Al hacer clic sobre un nombre de usuario, podemos ver su página de perfil.
75593m4016.gif
a) Haced el diagrama de clases UML de la información mostrada en el esbozo de pantalla “Blog del proyecto”.
  • No volváis a modelar lo ya hecho en la actividad anterior. Centraos en los artículos y comentarios. Si necesitáis una clase que ya habíais modelado, ponedla en el diagrama, pero no pongáis más atributos que los que son relevantes para los artículos y comentarios.

b) Indicad las claves de las clases del dominio y otras restricciones de integridad textuales que pueda haber, así como las reglas de derivación de la información derivada.
c) Reescribid las restricciones de integridad y reglas de derivación con lenguaje OCL.
  • Haced las suposiciones que creáis convenientes para tratar las fechas y otros tipos de datos como si fueran numéricos en OCL.

  • En particular, suponed que restando dos fechas obtenéis el número de días de diferencia y que podéis obtener fechas con N días de diferenciasumando o restando enteros de fechas.

8. Para modelar el resto del dominio, partiremos de otro esbozo de pantalla, el del perfil de un usuario.
1) Las aportaciones se listan agrupadas por proyecto; de cada una se lista fecha e importe.
75593m4017.gif
Nota: los usuarios también tienen una contraseña pero esta, evidentemente, no se muestra en el perfil.
a) Haced el diagrama de clases UML.
  • Como ya hemos dicho, no volváis a modelar lo ya modelado en la actividad anterior. Centraos en los datos mostrados en este esbozo. Si necesitáis una clase que ya habíais modelado, ponedla en el diagrama pero no pongáis más atributos que los que son relevantes a los datos mostrados en el esbozo.

b) Indicad las claves de las clases del dominio y otras restricciones de integridad textuales que pueda haber, así como las reglas de derivación de la información derivada.
c) Reescribid las restricciones de integridad y reglas de derivación identificadas en el apartado anterior con lenguaje OCL.
  • Haced las suposiciones que creáis convenientes para tratar las fechas y otros tipos de datos como si fueran numéricos en OCL.

  • En particular, suponed que restando dos fechas obtenéis el número de días de diferencia y que podéis obtener fechas con N días de diferencia sumando o restando enteros de fechas.

Ejercicios de autoevaluación

1. ¿Cuáles son las características deseables de una buena documentación de requisitos según el estándar IEEE-830?

2. ¿Qué dicen las buenas prácticas de documentación de requisitos sobre las interfaces gráficas de usuario?

3. ¿Qué queremos decir cuando decimos que la documentación de requisitos puede ser formal? ¿Qué ventajas e inconvenientes tiene que lo sea?

4. ¿Es posible hacer una documentación de requisitos ágil y formal a la vez? Si es posible, ¿cómo? En caso contrario, ¿por qué?

5. ¿Cuáles son los componentes básicos de una historia de usuario?

6. ¿Dónde podemos clasificar los casos de uso según los ejes formal - no formal y ágil-exhaustivo?

7. ¿Es cierto que los casos de uso son un mecanismo de documentación exhaustiva de requisitos? ¿Por qué?

8. ¿Qué es una restricción de integridad del modelo del dominio?

9. ¿Es cierto que OCL, al ser un lenguaje formal, solo permite una forma única de expresar una misma restricción de integridad?

10. ¿Qué tipos de propiedades puede tener un objeto en OCL?

11. ¿Cuáles de las colecciones de OCL admiten repetidos?

Solucionario

Actividades
1.
a)
75593m4018.gif
b)
Claves:
Album: titulo
context Album inv:
Album.allInstances()->isUnique(titulo)
Cancion: album + titulo
context Cancion inv:
Cancion.allInstances()->forAll(c1,c2 | c1 <> c2 implies
c1.album <> c2.album or c1.titulo <> c2.titulo)
Artista: nombre
context Artista inv:
Artista.allInstances()->isUnique(nombre)
Pais: nombre
context Pais inv:
Pais.allInstances()->isUnique(nombre)
Restricciones de integridad adicionales:
Un artista no puede estar relacionado consigo mismo
context Artista inv:
self.tieneRelacionados->excludes(self) and self.esRelacionadoDe->excludes(self)
Hemos supuesto que la asociación de un artista con sus relacionados no es simétrica y, por lo tanto, hemos indicado nombres de roles diferentes a cada extremo de asociación; en este caso, que un artista A1 esté relacionado con un artista A2 no implica que A2 esté relacionado con A1. Si la asociación fuera simétrica, no se indicarían nombres de rol (podríamos dar un nombre a la asociación) e indicaríamos el hecho de que la asociación es simétrica como una restricción de integridad adicional.
2.
75593m4019.gif
Claves:
Registro: termino
context Registro inv:
Registro.allInstances()->isUnique(termino)
Acepcion: registro + num
context Acepcion inv:
Acepcion.allInstances()->forAll(a1,a2 | a1 <> a2 implies
a1.registro <> a2.registro or a1.num <> a2.num)
Otras restricciones de integridad
Para todo registro, sus acepciones tienen números consecutivos empezando por el 1
context Registro inv:
self.acepcion->exists(a | a.num = 1) and
self.acepcion->forAll(a | a.num = 1 or self.acepcion->exists(a’ | a’.num = a.num - 1))
Notas:
  • La CategoriaGramatical y el AreaTematica son enumerations. Puesto que no nos han proporcionado la lista válida de valores, no se han modelado como parte de la solución.

  • El atributo num de la clase Acepcion es el número que se puede ver delante de cada acepción, antes de la categoría gramatical. Dado que indica un orden, se habría podido hacer la composición entre Registro y Acepcion {ordered} (al lado de Acepcion) y omitir el atributo num, o bien hacerlo derivado a partir de la posición que la acepción ocupe dentro de su registro.

3.
a) La regla de derivación que dice que el total de una línea de factura se calcula como el importe más el porcentaje de IVA.
context LineaFactura.total:Dinero derive:
self.importe + self.importe*self.iva/100
b) Una restricción de integridad que dice que el importe de una línea de factura debe ser mayor que cero.
context LineaFactura inv:
self.importe > 0
c) Una nueva propiedad booleana para los proyectos llamada pendienteFacturar que es cierta si y solo si la fecha de la última entrega del proyecto es posterior a la fecha de la última factura.
context Proyecto
def: pendienteFacturar:Boolean = self.fechaUltimaEntrega > self.fechaUltimaFactura
d) Una nueva propiedad booleana que dice que una factura está pagada si y solo si la fecha de pago no es null.
context Factura:pagada:Boolean derive:
not(self.fechaPago.oclIsUndefined())
e) Una nueva propiedad en Proyecto que sea el conjunto de facturas no pagadas.
context Proyecto def:
facturasPendientes = self.factura->select(f|not(f.pagada))
Otra forma de crear la misma propiedad podría ser:
context Proyecto def:
facturasPendientes = self.factura->reject(f|f.pagada)
f) Una nueva propiedad listadoProyectos, que, para los clientes, tenga el conjunto de nombres de proyectos del cliente.
context Cliente def:
listadoProyectos:Set(String) = self.proyecto.nombre
g) La regla de derivación de facturasPagadas, que devuelvo cierto si y solo si todas las facturas del proyecto tienen pagada como cierto.
context Proyecto:facturasPagadas:Boolean derive:
self.factura->forAll(f|f.pagada)
4. Aunque se podrían haber utilizado varios niveles de abstracción dentro del nivel general, se ha elegido el nivel más alto posible. Se considera que hay un único caso de uso de organización que satisface el objetivo fundamental de CrowdArt: financiar proyectos artísticos:
Financiar un proyecto artístico
Actor principal: Artista
Actores de apoyo: Administrador, Mecenas
Nivel: General
Ámbito: Organización
Escenario principal de éxito:
1) El artista propone un proyecto, indicando un título, una descripción y proporcionando material audiovisual para hacerlo atractivo, y define las aportaciones posibles y las recompensas asociadas. También indica la financiación total que necesita.
2) Un administrador de CrowdArt revisa el proyecto y lo da por bueno.
3) El proyecto se publica en la web.
4) Un mecenas busca proyectos según varios criterios y consulta los proyectos que le interesan.
5) El mecenas escoge el proyecto publicado (en el paso 3) y decide hacer una promesa de financiación.
6) Los pasos 4 y 5 se producen para varios mecenas hasta que pasan cuarenta días. El proyecto ha reunido suficientes promesas de financiación. Se cobra a los mecenas los importes apalabrados y el proyecto se considera exitoso.
7) El artista cobra la financiación reunida, salvo la comisión para CrowdArt.
Escenarios alternativos:
2b. El administrador decide que el proyecto no es válido e informa al artista.
1. El caso de uso acaba (a pesar de que el artista puede volver a proponer el proyecto).
5b. El mecenas tiene dudas y se comunica con el artista a través del sistema para resolverlos.
1. El caso de uso vuelve al punto 5.
5c. El mecenas no se decide a hacer promesa de financiación.
1. El caso de uso continúa en el punto 6 (otros mecenas).
6b. Pasados los cuarenta días, el proyecto no ha recibido suficientes promesas de financiación.
1. El proyecto no se considera exitoso y el caso de uso acaba.
Dado que el nivel es general, no se han proporcionado detalles de cómo se haría la busca, la consulta de detalle o la comunicación con el artista, puesto que son elementos secundarios que ya se describirán si se hacen casos de uso con un nivel de abstracción más concreto.
Tampoco hemos incluido la consulta del perfil de un usuario, puesto que no encaja demasiado bien en ninguna parte del caso de uso y, al mismo tiempo, no se puede considerar un caso de uso de organización. En todo caso, se habría podido poner como paso alternativo en el 5 (5d. El mecenas quiere consultar el perfil del artista o de un usuario).
5. Artista: proponer un proyecto.
  • Administrador: consultar proyectos pendientes de moderación.

  • Administrador: moderar un proyecto (decidir si lo acepta o lo rechaza).

  • Usuario: consultar proyectos por varios criterios.

  • Usuario: consultar los detalles de un proyecto.

  • Mecenas: hacer una promesa de aportación a un proyecto.

  • Usuario: consultar un perfil de usuario.

  • Calendario: finalizar la financiación de un proyecto.

  • Artista: cobrar un proyecto financiado con éxito.

Notas:
  • Los casos de uso en los que el actor es “Usuario” son aquellos que normalmente usará un posible mecenas pero que también pueden usar los artistas.

  • El penúltimo caso de uso se inicia cuando pasa cierto número de días desde que se propuso un proyecto. El actor Reloj o Calendario representa todos aquellos casos de uso que se inician en una fecha o en un momento en el tiempo en lugar de ser iniciados por un usuario.

6.
a)
75593m4020z.gif
Notas:
  • La clase Usuario tiene otros atributos que no aparecen en esta pantalla.

b)
Claves:
  • Proyecto: titulo

  • Usuario: nombreUsuario

  • Sesion: proyectoEscenico + fechaHora

  • Pregunta: proyecto + pregunta

  • Respuesta: pregunta (las respuestas se identifican por la pregunta a la que responden, puesto que cada pregunta solo puede tener una respuesta o ninguna).

  • Recompensa: proyecto + cantidad

Otras restricciones de integridad:
  • La fecha de cada pregunta de un proyecto tiene que ser posterior o igual a su fecha de creación.

  • La fecha de cada respuesta tiene que ser posterior o igual a la fecha de la pregunta que responde.

  • Un proyecto con 0 días restantes está, obligatoriamente, en estado éxito o fallido según si el importe conseguido alcanza el objetivo o no. Un proyecto con algún día restante solo puede estar en estado buscandoFinanciacion, propuesta o noAceptado.

  • La fechaHora de cada sesión de un proyecto escénico tiene que ser, como mínimo, cuarenta días posterior a la fecha de creación del proyecto.

Información derivada:
  • Recompensa::aportacionesRestantes, Proyecto::conseguido, Proyecto::numArticulos y Proyecto::numComentarios no se pueden derivar con los datos de este diagrama, pero se derivan de otros datos que no aparecen aquí.

  • Proyecto::diasRestantes es la diferencia entre cuarenta y el número de días desde que el proyecto se creó hasta la fecha actual, o cero si ya han pasado más de cuarenta días.

c)
-- Claves de las clases:
context Proyecto inv:
Proyecto.allInstances()->isUnique(titulo)
context Usuario inv:
Usuario.allInstances()->isUnique(nombreUsuario)
context Sesion inv:
Sesion.allInstances()->forAll(s1, s2 | s1 <> s2 implies
s1.proyectoEscenico <> s2.proyectoEscenico or
s1.fechaHora <> s2.fechaHora)
context Pregunta inv:
Pregunta.allInstances()->forAll(p1, p2 | p1 <> p2 implies
p1.proyecto <> p2.proyecto or p1.pregunta <> p2.pregunta)
context Respuesta inv:
Respuesta.allInstances()->isUnique(pregunta)
-- En este caso, dado que una Pregunta solo puede tener una respuesta y una Respuesta solo puede tener una pregunta, esta restricción no es necesaria.
context Recompensa inv:
Recompensa.allInstances()->forAll(r1, r2 | r1 <> r2 implies
r1.proyecto <> r2.proyecto or r1.cantidad <> r2.cantidad)
-- Otras restricciones de integridad:
context Pregunta inv:
self.fecha >= self.proyecto.fechaCreacion
context Respuesta inv:
self.fecha >= self.pregunta.fecha
context Proyecto inv:
self.diasRestantes = 0 implies
((self.conseguido >= self.objetivo and self.estado = EstadoProyecto.exito) or
(self.estado = EstadoProyecto.fallido))
context Proyecto inv:
self.diasRestantes > 0 implies
self.estado <> EstadoProyecto.exito and self.estado <> EstadoProyecto.fallido
context Sesion inv:
self.fechaHora >= self.proyecto.fechaCreacion + 40
-- Información derivada:
context Proyecto.diasRestantes:int derive:
let antiguedad:int = now() – self.fechaCreacion in min(0, 40 – antiguedad)
-- Nota: La operación now() no existe en OCL, pero la necesitamos para poder hacer referencia a la fecha actual.
7.
a)
75593m4021z.gif
b)
Claves:
  • Artículo: proyecto + fecha

  • Comentario: artículo + posición que ocupa en la asociación

Otras restricciones de integridad:
  • La fecha de un artículo tiene que ser posterior a la fecha de creación del proyecto.

  • La fecha de un comentario tiene que ser posterior a la fecha del artículo que comenta.

Información derivada:
  • Proyecto::numArticulos es el número de artículos que el proyecto tiene asociados.

  • Proyecto::numComentarios es el número total de comentarios que los artículos del proyecto tienen asociados.

c)
-- Claves:
context Proyecto inv:
self.articulo->isUnique(fecha)
-- No hay que escribir la clave de comentario porque está implícita
-- en el hecho de que la asociación es {ordered}
-- Otras restricciones de integridad:
context Articulo inv:
self.fecha >= self.proyecto.fechaCreacion
context Comentario inv:
self.fecha >= self.articulo.fecha
-- Información derivada:
context Proyecto::numArticulos:int derive:
self.articulo->size()
context Proyecto::numComentarios:int derive:
self.articulo.comentario->size()
-- Nota: self.articulo.comentario es un Set(Comentario) que contiene los comentarios de
-- todos los artículos del proyecto.
8.
a)
75593m4022.gif
b)
Claves: no hay claves nuevas.
Otras restricciones de integridad:
  • La fecha de una aportación tiene que ser posterior a la fecha de creación del proyecto.

  • Si una recompensa tiene maximoAportaciones, entonces no puede tener más de maximoAportaciones aportaciones asociadas.

Información derivada:
  • Recompensa::aportacionesRestantes es el máximo de aportaciones de la recompensa menos el número de aportaciones ya asociadas a la recompensa o nulo si no hay un máximo de aportaciones.

  • Proyecto::conseguido es la suma de la cantidad de cada aportación al proyecto.

c)
-- Restricciones de integridad:
context Aportacion inv:
self.fecha >= self.recompensa.proyecto.fecha
context Recompensa inv:
not(self.maximoAportaciones.oclIsUndefined()) implies
self.aportacion->size() <= self.maximoAportaciones
-- Información derivada:
context Recompensa.aportacionesRestantes:int derive:
if self.maximoAportaciones.oclIsUndefined() then null
else self.maximoAportaciones – self.aportaciones->size()
context Proyecto.conseguido:Dinero derive:
self.recompensa->collect(r|r.cantidad * r.aportacion->size())->sum()
-- Nota: El collect devuelve una colección donde hay, para cada recompensa, el importe
-- de todas las aportaciones de dicha recompensa. Por lo tanto, al sumar se obtiene el importe
-- total.
Ejercicios de autoevaluación
1. Según el estándar IEEE-830, una buena documentación de requisitos debería ser correcta, inambigua, completa, consistente, ordenada, verificable, modificable y trazable. Véase el subapartado 1.1.
2. Conviene no dar por supuesta una determinada solución, como por ejemplo no dar por supuesta una interfaz gráfica de usuario. Véase el subapartado 1.2.
3. Decimos que la documentación de requisitos es formal cuando se usa un lenguaje formal a la hora de describir los requisitos. Por lo tanto, se trata de utilizar un lenguaje definido formalmente y creado para no admitir ambigüedades y, a la vez, facilitar la verificación y la comprobación de la consistencia de la documentación de requisitos. Véase subapartado 2.1.2.
4. Una aproximación ágil y formal a la documentación de los requisitos consiste en escribir los criterios de aceptación en forma de pruebas automatizadas. Véase el subapartado 2.5.
5. Los componentes básicos de una historia de usuario son una descripción, una serie de conversaciones y un conjunto de criterios de aceptación. Véase el subapartado 2.4.
6. Una de las ventajas de los casos de uso es que son muy flexibles por lo que respecta nivel de formalismo y la exhaustividad de su descripción. La misma técnica se puede aplicar en los cuatro estilos de documentación vistos. Véase el subapartado 3.1.
7. No, puesto que, en realidad, los casos de uso es pueden utilizar para documentar requisitos de forma exhaustiva, pero también para documentarlos de forma mucho más ágil. En todo caso, es cierto que las metodologías ágiles suelen preferir las historias de usuario. Véase el apartado 3.
8. Una restricción de integridad es una regla o condición que los datos del software tienen que cumplir para que podamos decir que los datos son consistentes. Véase el subapartado 4.1.1.
9. No, OCL permite escribir una misma restricción de formas diferentes. Véase el subapartado 4.2.2.
10. Atributos, extremos de asociación y propiedades definidas en OCL. Véase el subapartado 4.2.4.
11. Bag y Sequence. Se diferencian en que en Bag no hay ningún orden definido, mientras que en Sequence el orden es relevante. Véase el subapartado 4.5.

Glosario

actor m
Ente con capacidad de interactuar con el sistema y que tiene un comportamiento propio. Puede ser una persona, pero también una organización o un sistema (de software o de otro tipo) externos al propio sistema.
actor de apoyo m
Actor externo al sistema que proporciona un servicio al sistema.
actor iniciador m
Actor que comienza la interacción con el sistema. Puede ser el actor principal, pero también puede ser un intermediario entre este y el sistema. También puede ser que el caso de uso se inicie de forma automática o en respuesta a un acontecimiento externo, en cuyo caso el actor iniciador sería el reloj o dicho acontecimiento, respectivamente.
actor principal m
Actor que hace una petición al sistema para recibir uno de los servicios y así satisfacer un objetivo.
atributo derivado m
Véase información derivada.
backlog m
Lista de historias de usuario, ya sea de todas las historias de un proyecto en desarrollo (product backlog) o de las que se incluyen en una iteración.
cardinalidad de un atributo f
Número de valores que una determinada instancia tiene en un atributo.
cardinalidad de una asociación f
Número de instancias que una determinada instancia tiene asociadas por medio de una asociación concreta.
caso de uso m
Documento que recoge el contrato entre el sistema y sus stakeholders. Describe el comportamiento del sistema y las interacciones en varios escenarios mostrando la forma en que el sistema responde a las peticiones y objetivos de los stakeholders.
clave de una clase de dominio f
Atributo o conjunto de atributos que identifica las instancias de la clase de forma única, de modo que no puede haber más de una instancia con los mismos valores en aquel o aquellos atributos.
contrato de una operación m
Documentación detallada del comportamiento de una operación que incluye su firma y las precondiciones y poscondiciones.
criterio de aceptación m
Descripción de las condiciones mediante las cuales se decidirá si determinado requisito, por lo general una historia de usuario, es considerado satisfecho o no. Véase también prueba de aceptación.
diagrama de clases m
Diagrama estándar de UML utilizado para modelizar las clases de objetos y sus relaciones.
enumeración f
Tipo de datos en el que el conjunto de valores es una lista finita de valores que se enumera en la definición del tipo de datos.
escenario de un caso de uso m
Secuencia de acciones e interacciones que se producen en un caso de uso bajo ciertas condiciones, expresadas sin ramificaciones condicionales o con muy pocas.
escenario principal de un caso de uso m
Escenario descrito completamente por un caso de uso desde el inicio del caso de uso hasta su finalización y que lleva al actor principal a cumplir su objetivo. A pesar de que es un escenario de éxito, no tiene que ser necesariamente el único del caso de uso.
especificación de software f
Documentación del conjunto de requisitos que debe satisfacer el software.
garantía mínima de un caso de uso f
Aquello que podemos asegurar que sucederá al finalizar el caso de uso, sea cual sea el escenario que se produzca.
historia de usuario f
Técnica de documentación de requisitos que pone énfasis en la comunicación verbal y minimiza la documentación escrita. Se compone de tres elementos: el nombre, la conversación entre los desarrolladores y los clientes y la lista de pruebas que hay que hacer para verificar que se ha implementado correctamente.
información derivada f
Atributo o asociación presente en un modelo, por lo general un modelo del dominio en UML, cuyo valor se calcula a partir de otros elementos del modelo.
invariante m
Predicado que es cierto en todo momento. En un modelo de clases UML, un invariante es un predicado, una afirmación sobre el modelo, que siempre es cierta. Las restricciones de integridad y las reglas de derivación de la información derivada son, por lo tanto, invariantes.
multiplicidad f
Restricción que indica qué cardinalidades son válidas para un atributo o asociación.
object constraint language m
Lenguaje formal estándar de restricciones: lenguaje estándar de UML para la expresión de restricciones que se puede utilizar, entre otras cosas, para escribir restricciones de integridad y reglas de derivación.
sigla OCL
Object Management Group m
Consorcio americano sin ánimo de lucro, creado el 1989, que tiene por objetivo la estandarización y promoción de la modelización orientada a objetos.
sigla OMG
OCL m
Véase object constraint language.
OMG m
Véase Object Management Group.
panel Kanban m
Véase panel de tareas.
panel Scrum m
Véase panel de tareas.
panel de tareas m
Radiador de información que suele mostrar las funcionalidades que hay que desarrollar durante una iteración, su descomposición en tareas y el progreso de cada una.
sin compl. según la metodología de desarrollo utilizada, panel Kanban o panel Scrum
propiedad de una clase f
Elemento genérico que hace referencia a atributos y a extremos de asociación de la clase.
prueba de aceptación f
Prueba que tiene por objetivo determinar si el sistema se adecua a los requisitos de los clientes y, por lo tanto, si estos aceptarán o no el sistema desarrollado. Por definición, pues, es una prueba de sistema.
radiador de información m
Herramienta utilizada en entornos de desarrollo ágil para representar información relevante para el desarrollo, como por ejemplo los requisitos. Se trata de un elemento visual ubicado de tal manera que las personas lo tienen visible mientras trabajan o cuando pasar por delante de él.
requisito m
Condición exigida o necesaria para algo. En el campo de la ingeniería del software, necesidad o restricción que afecta a un producto de software definiendo qué soluciones son adecuadas (lo cumplen) y cuáles no (no lo cumplen) desde el punto de vista de dicha necesidad o restricción. Una solución será adecuada si satisface todos los requisitos del sistema.
requisito candidato m
Necesidades o restricciones obtenidas en una primera etapa de obtención de requisitos y que se convertirán en requisitos solo si son seleccionadas como tales a la hora de definir el alcance del proyecto por desarrollar.
requisito funcional m
Requisito que describe la funcionalidad esperada del sistema; es decir, que describe el comportamiento del sistema ante los estímulos que le llegan del exterior.
requisito no funcional m
Requisito que restringe el conjunto de soluciones posible, por lo general en forma de restricción, sin hablar de la funcionalidad.
restricción de integridad f
Limitación que se aplica sobre un modelo del dominio en el sentido de que no puede ser que el predicado no se satisfaga (es decir, se tiene que evaluar a cierto en cualquier momento).
stakeholder m y f
Persona o entidad con un interés sobre el producto que estamos desarrollando.

Bibliografía

Bibliografía principal
Cockburn, A. (2001). Writing effective use cases. Addison-Wesley.
Este libro nos ofrece indicaciones sobre cómo aplicar de forma eficaz los casos de uso a la gestión de requisitos. A pesar de centrarse en la utilización de casos de uso para la documentación de requisitos, gran parte del discurso es aplicable independientemente del estilo de documentación.
Cohn, M. (2004). User stories applied. Addison-Wesley.
Este libro trata, principalmente, de la técnica de las historias de usuario pero, igual que el de Cockburn, gran parte del discurso es aplicable a la obtención, gestión y documentación de requisitos con independencia del estilo de documentación.
Object Management Group (2012). OMG object constraint language. Version 2.3.1. OMG.
Bibliografía complementaria
Davis, Al. M. (2005). Just enough requirements management. Dorset House.
En esta obra, Alan Davis propone un enfoque pragmático para la obtención y la gestión de requisitos.
Pradel, J.; Raya, J. Ingeniería del software. Editorial UOC.
Los materiales de la asignatura de Ingeniería del software, especialmente el módulo 3, tratan los requisitos de forma introductoria. Por otro lado, también tratan la documentación de requisitos mediante casos de uso y UML.
Cockburn, A. (2005). Crystal clear: A human-powered methodology for small teams. Addison-Wesley.
Referencias bibliográficas
Cockburn, A. <https://alistair.cockburn.us/Use+case+questions> [En línea, última visita: febrero 2012].
Cockburn, A. (2002). Use cases, ten years later. [En línea, última visita: febrero 2012]. <https://alistair.cockburn.us/Use+cases%2c+ten+years+later>
Cohn, M. Training for scrum task board use. [En línea, última visita: febrero 2012]. <https://www.mountaingoatsoftware.com/scrum/task-boards>
Constantine, L. Users, roles and personas. [En línea, última visita: febrero 2012]. <https://www.foruse.com/articles/rolespersonas.pdf>
Gottesdiener, E. (2002). Top ten ways project teams misuse use cases and how to correct them. [En línea, última visita: febrero 2012]. <https://www.ibm.com/developerworks/rational/library/content/RationalEdge/jun02/MisuseUseCasesJun02.pdf>
IEEE-SA Standards Board (1998). IEEE Std 830-1998 - IEEE Recommended practice for software requirements specifications. IEEE Computer Society.
Jacobson, I. (2003). Use cases – yesterday, today and tomorrow. [En línea, última visita: febrero 2012]. <https://www.bus.iastate.edu/nilakant/MIS538/Readings/UseCases_TheRationalEdge_Mar2003.pdf>
Korson, T. (n.d.). The misuse of use cases. [En línea, última visita: febrero 2012]. <https://robertwoodley.co.uk/Tecademy/52%20The%20Misuse%20of%20Use%20Cases.pdf>
Lilly, S. (1999). Use case pitfalls; Top 10 problems from real projects using use cases. [En línea, última visita: febrero 2012]. <https://gul.gu.se/public/pp/public_courses/course41505/published/1288203577450/resourceId/15876915/content/UseCasePitfalls.pdf>
Pichler, R. (2011). All things product owner. [En línea, última visita: febrero 2012]. <https://www.romanpichler.com/blog/product-backlog/product-backlog-board/>
Varios autores (2001). Manifiesto por el Desarrollo Ágil de Software. [En línea, última visita: febrero 2012]. <https://agilemanifesto.org/iso/es>
Varios autores (2010). Unified modeling language. Object Management Group.