Buscar en Gazafatonario IT

martes, octubre 23, 2007

Lecturas Fundamentales 10

Lectura Fundamental Anterior: “RUP: Fase de Concepción Parte 2”

Lectura # 10: Realización de Casos de Uso: De los Objetos y sus Interacciones

Presentación

¿Es familiar para alguno de ustedes esta imagen?

Figura 1: Meta-Modelo de un proceso de desarrollo de software mal-ejecutado

Este meta-modelo muestra un paradigma muy común en los equipos de desarrollo de software en el que la substancia de los emisores (los Analistas) no se coordina con la idiosincrasia de los receptores (los Programadores y los Probadores). Lo que está sucediendo es que el formato en el que se emite y el formato en el que se recibe (para usar términos reconocibles por nosotros) no son compatibles en el sentido en que unos y otros ven las cosas de distinta forma: mientras los Analistas observamos el universo del problema desde una perspectiva de usuario final, los Programadores lo hacemos con una visión técnica y tecnológica de las cosas. Mientras el Analista habla (escribe) en términos de necesidades, causas raíz, características, requisitos y solicitudes, el Programador espera recibir expresiones orientadas al método (orientado-a-objetos, orientado-a-aspectos o a algún otro similar): clases, tablas, objetos, controles visuales, entre otros.

Esta cacofonía por antonomasia, esta genuina desproporción de los formatos de emisión y recepción que nacen (durante la Concepción del sistema) y evolucionan (a lo largo de la Elaboración y Construcción) de manera asimétrica por la simple causa de que una se apoya en lo biológico (las necesidades y los requisitos vienen de seres humanos) mientras que la otra se soporta en la mecánica y en la electrónica (las tablas, los índices y los objetos necesitan de una máquina para vivir), esta situación en la que la comunicación (entre Analistas y Programadores) se torna en un procedimiento irregular y embrollado, esta disyuntiva es la que me empieza a ocupar a partir de esta lectura fundamental.

Ahora bien, puesto que el análisis y diseño orientado a objetos es ciertamente un proceso dinámico, debo elegir desde que ángulo presentarlo. Como con el mismo proceso de desarrollo de software, mostrar un estado “final” muchas veces da al lector falsas expectativas de estar en lo correcto desde el primer intento, mientras que con un primer acercamiento se puede exhibir un sistema incompleto o peor aún, erróneamente entendido. En este artículo elegí compartir mi trabajo a partir de un enfoque temprano, corriendo el riesgo de darle al lector inexperto algunas falsas concepciones acerca de lo que son el análisis y el diseño orientados a objeto. Es un buen reto, sobre todo porque me da la oportunidad de reducir ese riesgo al mínimo, al punto de hacerlo desaparecer a lo largo de la explicación que tengo planeada en esta exposición; al final de la misma, espero, el Diseñador orientado a objetos habrá entendido las cuestiones básicas y algunas no tan básicas de la realización de casos de uso desde una perspectiva sistémica. Si eso sucede, al menos para un pequeño grupo de lectores, entonces habré cumplido mi misión.

Aquí vamos otra vez.

Sobre el Lenguaje de Modelado Unificado

Definición 19: “El Lenguaje de Modelado Unificado es un lenguaje visual para especificar, construir y documentar los artefactos de los sistemas. UML es un lenguaje de modelado de propósito general que puede ser usado con todos los métodos orientados a objetos y a componentes y puede ser aplicado a todos los dominios de aplicación (por ejemplo, salud, financiero, telecomunicaciones, aeroespacial) y a distintas plataformas de implementación (por ejemplo, J2EE y .NET).” 1

En este contexto, Especificar significa enumerar y construir modelos que son precisos, no ambiguos y completos. UML dirige la especificación de todas las decisiones importantes de análisis, diseño e implementación2. Mientras tanto, Construir quiere decir crear o modificar diagramas y modelos, usualmente basado en la existencia y estado de otros símbolos base o de otros diagramas. Los modelos que se construyen con UML están relacionados con los lenguajes de programación orientados a objetos. UML se usa para hacer Ingeniería Hacía Adelante, esto es, un mapeo directo de un modelo UML con código fuente. UML también se usa para hacer Ingeniería en Reversa, o sea, una reconstrucción de un modelo UML desde una implementación específica, normalmente en un lenguaje OO3. UML además se usa para Documentar la arquitectura de los sistemas, los requisitos, las pruebas y actividades como planeación del proyecto y manejo de la entrega del producto4.

Como cualquier lenguaje, UML tiene una sintaxis y una semántica. La sintaxis de UML está compuesta por un conjunto de construcciones gráficas útiles para especificar diagramas y modelos de sistemas5. Cada uno de estos símbolos tiene un significado de manera independiente, como lo tienen las palabras reservadas Begin, End y While en Pascal o los términos private, void y string en C# (léase C Sharp)6.

La semántica de UML es la que nos dice, por ejemplo, que un Actor solo se puede “comunicar” con el sistema a través de los casos de uso mediante una relación de Asociación, o que un paquete puede estar compuesto de Clases, Casos de Uso, Componentes u otros Paquetes y que existen distintos tipos de diagramas, a manera de modelos, que pueden ser construidos a partir de uno o más símbolos de UML7. De esta manera, podemos construir “instrucciones válidas” en UML como las que ilustro en la figura 2, que resulta ser un Diagrama de Casos de Uso, un espécimen del que hablaré en alguna oportunidad.

Pueden encontrar más de UML en mis Prolegómenos Sobre el Lenguaje de Modelado Unificado (http://gazafatonarioit.blogspot.com/2005/06/prolegmenos-sobre-el-lenguaje-de.html).

El Caso de Estudio (aka, el Ejemplo)

Consideremos el siguiente caso de uso típico ingreso al sistema de reserva de vuelos:

Caso de Uso: Ingresar al Sistema

Actor: Pasajero

Descripción: Este caso de uso permite al Pasajero ingresar al sistema Web de Reservas usando su número de pasajero frecuente y la clave actual. A solicitud del usuario, el caso de uso además muestra los datos detallados del pasajero registrado.

Objetivo: Suministrar al usuario Web del sistema de Reservas una interfaz amigable que le permita acceder rápidamente a las principales opciones del sistema.

Precondiciones

Ninguna

Secuencia Básica:

1. El caso de uso inicia cuando el pasajero decide ingresar al sistema

2. El sistema solicita el código (número) de pasajero frecuente y su respectiva clave

3. El pasajero ingresa su número y clave

4. El sistema busca los datos del pasajero, valida que el número y la clave del mismo coincidan con los registrados previamente.

5. El sistema solicita confirmación para mostrar los datos completos del pasajero

6. El pasajero confirma que quiere ver sus datos

7. El sistema muestra los datos detallados del pasajero

8. El caso de uso termina

Secuencia Alternativa 1

4A. El número de pasajero o la clave no coinciden.

4A1. el sistema muestra el mensaje: “Los datos proporcionados del pasajero no coinciden con los registrados en el sistema. Verifique.”

4A2. El caso de uso continúa en el paso 2.

Poscondiciones

El pasajero puede realizar diversas transacciones en el sistema como Reservar Vuelos, Cancelar Reservas, Cambiar su Clave, Modificar su Perfil, Pagar una Reserva o Cambiar los datos de una reserva.

Requisitos Especiales

Por razones de seguridad, el sistema sólo muestra los cuatro últimos dígitos de la tarjeta de crédito del pasajero.

Por razones de seguridad, el sistema no indica cual de los datos (número o clave) está erróneo.

Advertencia: aunque éste es un caso de uso terminado, es de una situación simulada. Si lo usa en algún proyecto es bajo su propia responsabilidad. Todos los nombres y lugares han sido cambiados para proteger la identidad de los culpables.

Puesto que se trata de un enfoque holista, no puedo continuar sin poner en perspectiva este caso de uso. Hablo de ubicarlo dentro de un sistema, en este caso, de un sistema de reserva de vuelos en el que además se pueda cancelar una reserva, pagar por el vuelo (con tarjeta de crédito, por ejemplo), cambiar la reserva (algunos de sus datos, como la fecha de regreso, por ejemplo), confirmar una reserva, hacer apertura y cierre de vuelos por parte de las aerolíneas y mantener información de los aeropuertos, entre otras funcionalidades. Las cosas así, el diagrama de casos de uso de la figura 2 nos muestra una vista funcional del sistema.

Figura 2: Algunos casos de uso del Sistema de Reserva de Vuelos

Con este conato de modelo en mente, volvamos a nuestro caso de uso.

Interacciones

Las Interacciones son usadas en diversas situaciones: para tener un mejor control de una situación de interacción por parte de un diseñador individual o por un grupo de personas que necesita lograr una interpretación común de la situación. Las interacciones también son usadas durante la fase de diseño más detallada donde la precisa comunicación entre procesos debe ser establecida de acuerdo a protocolos formales. Cuando se ejecutan las pruebas, las secuencias de ocurrencias de eventos del sistema pueden ser descritas como interacciones y comparadas con las de las fases anteriores.8

En UML 2.x hay tres clases de diagramas de Interacción: Diagrama de Secuencia, Diagrama de Comunicación (anteriormente diagrama de Colaboración) y Diagrama de Revisión de Interacciones (nuevo a partir de la versión 2.0 de UML).

Los Diagramas de Comunicación muestran las interacciones a través de una vista arquitectónica de las líneas de vida de los objetos en donde la secuencia de Mensajes es dada vía un esquema de numeración secuencial. Mientras tanto, los Diagramas de Revisión de Interacciones definen Interacciones a través de una variante de los Diagramas de Actividad promoviendo la revisión del flujo de eventos. En estos diagramas de Revisión se omiten las Líneas de Vida y los Mensajes entre ellas.

En lo que sigue, me concentraré en los Diagramas de Secuencia, que se enfocan en el intercambio de mensajes entre las Líneas de Vida de los objetos.

Diagramas de Secuencia, Paso a Paso

Un Diagrama de Secuencia muestra una vista de las entrañas del sistema, cómo se van a suceder las funciones (expuestas normalmente en un caso de uso o en un conjunto de casos de uso) a partir de la activación de un Actor (una persona). En general, un diagrama de secuencia se usa para modelar la lógica del sistema (de uno o más casos de uso del sistema al tiempo, o de parte de un caso de uso). Este tipo de diagrama muestra precisamente el orden temporal en el que ocurren las acciones en un sistema. Estas acciones son especificadas en términos de Mensajes entre los objetos que interactúan en el modelo. Además, son modelados a nivel de objetos en vez de a nivel de clases para permitir escenarios que usan más de una instancia de la misma clase y para trabajar a nivel de hechos (reales), datos de prueba y ejemplos. Y más aún, el diagrama de secuencia muestra una vista dinámica del sistema (o de parte de él), en contraposición a lo que hace por ejemplo el diagrama de clases, que presenta un panorama estático del mismo sistema.

De acuerdo a Ivar Jacobson, la parte más importante del así llamado análisis dinámico es la realización del caso de uso, nombrada así puesto que con ellos hacemos real(idad) el caso uso mediante la demostración de cómo puede ser implementado a través de la colaboración entre objetos. La realización de casos de uso tiene tres pasos esenciales:

1. Ir a través de (caminar por) los casos de uso del sistema simulando los mensajes enviados entre objetos y almacenando los resultados en diagramas de interacción.

2. Agregar operaciones a los objetos que reciben los mensajes.

3. Adicionar clases para representar límites (interfaces del sistema) y controladores (un receptáculo para procesos del negocio complejos o para la creación y recuperación de objetos), cuando sea necesario.

En nuestro ejemplo, el caso de uso inicia cuando el Pasajero decide Ingresar al Sistema. Este es el punto de partida, la activación del proceso. La primera pregunta que tratamos de responder es: ¿mediante qué mecanismo (de interacción) el pasajero le indica al sistema que quiere autenticarse? Esta es una pregunta simple que tiene una respuesta simple: mediante un formulario llamado, coincidentemente, Ingresar al Sistema. Si asumimos que estamos desarrollando una aplicación para Internet, este formulario se encuentra en una página Web (no entraré en el detalle de la plataforma de implementación, del lenguaje de programación o de la base de datos a usar para la producción de este sistema). En este escenario, entonces, el actor le envía un mensaje (interactúa) con un formulario llamado Ingresar al Sistema (que no es más que una instancia –un objeto – de una página Web.

Anotación: estoy usando una definición amplia e indistinta de los términos Instancia, Objeto y Clase. En una futura Lectura Fundamental abordaré el tema específico de la programación orientada a objetos y especificaré los detalles de tales conceptos.

La figura 3 muestra entonces la interacción inicial entre el actor (el pasajero) y el sistema (la página Ingresar al Sistema). El diagrama de la figura muestra dos Líneas de Vida.

Definición 20: Una Línea de Vida representa un participante individual en la Interacción. Cada Línea de Vida se conoce como una entidad interactuante9. Simplificando, una Línea de Vida es una instancia de una clase (si nos encontramos a la altura del análisis del sistema, ésta es realmente una instancia potencial) que muestra su propio ciclo de vida, desde la creación (que puede ser implícita o explícita), pasando por diferentes estados, hasta su destrucción (que también puede ser implícita o explícita).

Definición 21: Un Mensaje define una comunicación específica entre las Líneas de Vida de una Interacción. La comunicación puede ser de diversos tipos: el envío de una señal, la invocación de una operación, la creación o destrucción de una instancia10, entre otras. Con el Mensaje se pueden identificar tanto el Emisor (el objeto que envía el mensaje) como el Receptor (el objeto que recibe el mensaje). Este último es el que finalmente implementará la funcionalidad (las operaciones) que se requiere para que el sistema procese el Mensaje.

En el diagrama, el mensaje es “Ingresar”. El mensaje tiene dos argumentos: Número y Clave del Pasajero.

Por ahora, esta interacción nos resuelve los pasos 1 a 3 del caso, en los que el sistema solicita el número y clave del pasajero y este los ingresa. Lo que sigue es que el sistema busca y valida los datos de ese pasajero en particular.

Puesto que normalmente estaríamos en una fase en la que ya se conoce al menos una arquitectura preliminar o candidata del sistema, como diseñadores seguimos los patrones o las restricciones impuestas por esa arquitectura. En este caso, asumamos que nos enfrentamos a un diseño de varias capas en el que la interfaz de usuario no se comunica por ningún motivo con la base de datos. En este caso, la página Ingresar al Sistema debe transferir la solicitud de acceso a otro elemento del modelo que bien podría ser un objeto, una instancia de un Controlador de Pasajeros, como lo muestra la figura 4.

Figura 3: Diagrama de Secuencia Inicial del Caso de Uso Ingresar al Sistema

Figura 4: Transferencia de la Solicitud de Ingreso al Sistema

Quiero detenerme unos instantes en este asunto de las restricciones impuestas por la arquitectura del software. Resulta que durante parte del proceso de desarrollo, los analistas, diseñadores, programadores y demás miembros del equipo nos encontramos en un proceso creativo, un proceso de descubrimiento constante, desde los objetos del dominio (del negocio) durante el modelado del negocio, pasando por los requisitos (en términos de casos de uso) y las clases y las tablas del sistema, hasta muchas de las líneas de código que escribimos. Eso es lo que hace que nuestra profesión siga siendo un “arte”, poco precisa y hasta llena de dilemas y ambigüedades. Pues bien, resulta que la Arquitectura es uno de esos “artilugios” que tenemos para guiar ese proceso de hallazgo al que nos enfrentamos. La arquitectura define o establece un conjunto de elementos estructurales que se nutren de decisiones de diseño, reglas o patrones que restringen ese diseño y la construcción misma del software.

Ahora bien, si nos ceñimos a las reglas arquitecturales (o arquitectónicas), el número de salidas que encontremos para un caso de uso o para un conjunto de ellos puede ser grande. La solución que estoy exponiendo es la de una transacción “tipo”, un patrón determinístico que se puede usar en la gran mayoría de los escenarios que nos ocupan día a día.

Volvamos al ejemplo. Este recién llegado Controlador de Pasajeros es quien puede comunicarse con algo así como una Relación de Pasajeros, un inventario que conozca los datos de todos los pasajeros registrados en el sistema. Este repertorio, bien podríamos llamarlo Catálogo de Pasajeros y dependiendo del momento por el que atravesemos en el proceso de desarrollo, este elemento bien podría permanecer en la abstracción conceptual de lo que ordinariamente conocemos como catálogo (durante el análisis) o bien podría ser una instancia de una Clase (realmente una Colección) de Pasajeros (más tarde en el diseño), hasta llegar a ser una Tabla de la base de datos (durante la implementación de la solución). Por ahora, sólo será el Catálogo de Pasajeros, simple.

Y es a este catálogo al que el Controlador de Pasajeros le puede indagar por los datos del pasajero que acaba de suscribir la solicitud de acceso, como lo dibujo en la figura 5.

Figura 5: Buscar Pasajero por Número

Sabemos que hay dos alternativas posibles: que el pasajero se encuentre registrado en el catálogo o que no se encuentre registrado. En este diagrama sólo ilustraré la primera. Una vez los datos del pasajero han sido localizados, el Catálogo de Pasajeros puede hacer dos cosas: informar a Control de Pasajeros y crear un nuevo objeto, finalmente, el Pasajero. Este nuevo objeto tiene todos los datos del pasajero y conoce todas las funciones que un pasajero (una persona) puede desempeñar en el sistema. ¡Es un Humanoide Virtual!

En la figura 6 podemos observar el instante preciso en el que se crea la instancia del Pasajero, justo después de que el Control de Pasajeros recibe la notificación de que el pasajero en proceso existe.

Figura 6: Crear la Entidad Pasajero

En esta versión del diagrama de secuencia aparecen dos nuevos tipos de mensaje: el mensaje 4: Existe Pasajero devuelve una confirmación del registro de pasajero al Controlador de Pasajeros; mientras tanto, el mensaje 5: Crear Pasajero crea un nuevo objeto del sistema, Pasajero. Aquí, el título del mensaje, nemotécnico por demás, no es el que advierte de la creación del objeto, es más bien el lugar en el diagrama donde aparece la instancia recién-creada, mucho más abajo de donde inician las demás líneas de vida de esta realización.

Bueno, al parecer ya tenemos las identidades de todos los seres interactuantes que nos permitirán terminar de implementar el caso de uso Ingresar al Sistema o, al menos, uno de sus escenarios. El paso 4 del caso de uso: El sistema busca los datos del pasajero, valida que el número y la clave del mismo coincidan con los registrados previamente, se puede implementar como lo muestra la figura 7.

Figura 7: Diagrama de Secuencia del Caso de Uso Ingresar al Sistema (versión 1 – Beta)

La imagen también muestra los demás pasos de la secuencia básica del caso de uso. Observamos que la Entidad Pasajero se convierte en una fuente de información para el resto de elementos del modelo ya que representa, como establecí antes, un pasajero real.

En esta versión del diagrama también aparece un nuevo tipo de mensaje (8: Validar Clave). Este es un mensaje Reflexivo (o Auto-Mensaje), en el que el origen y el destino del mensaje son el mismo objeto. No confundir con mensaje recursivo, que es un mensaje reflexivo que se ejecuta recursivamente (se llama a sí mismo durante su ejecución).

De otro lado, los mensajes 11 y 12 comunican directamente la interfaz de usuario con la entidad Pasajero. Si volvemos a los patrones arquitectónicos quizás deberíamos evitar este tipo de interacción y pasar siempre a través del Controlador de Pasajeros. Esta es apenas una primerísima versión, bien podrían existir algunas otras antes de lograr una que sea definitoria para el sistema.

De Estereotipos y Otros Menesteres

Un estereotipo es el mecanismo que proporciona UML para que podamos extender las construcciones útiles de diseño. Un estereotipo puede ser visto como un clasificador (o tipificador) de clases o de otros elementos del lenguaje, es decir, adicionan un nuevo nivel al árbol de jerarquías en un modelo. Los estereotipos también sirven para representar objetos (físicos o virtuales) de una manera más cercana a la realidad de los mismos. De esta forma, Página Web (o Página HTML o Página ASP), Formulario Windows, Controlador, Entidad del Dominio, Colección, Tabla y Procedimiento Almacenado son estereotipos válidos que aplican al elemento base Clase de UML, mientras que Reporte, Proceso por Lotes, Ingreso de Datos y Transacción bien podrían ser estereotipos aplicables al elemento base Caso de Uso de UML.

La sintaxis de estos clasificadores es: <<Nombre del Estereotipo>>, como en <<Colección>>.

Figura 8: Diagrama de Secuencia del Caso de Uso Ingresar al Sistema con Estereotipos

Algunos de estos estereotipos (usados en el ejemplo), cuyo uso se ha difundido ampliamente en la comunidad de desarrollo, ya tienen su propia sintaxis, como revelo en la figura 8. Ingresar al Sistema tiene un estereotipo boundary (límite), Control de Pasajeros y Catálogo de Pasajeros tienen un estereotipo control, mientras que Pasajero tiene un estereotipo entity. Cada uno de estos clasificadores tiene también su propia semántica: las clases límite (llamadas así porque tienen el estereotipo boundary) representan interfaces con los usuarios o con otros sistemas; las clases controladoras son las responsables de manejar el flujo o la lógica de un caso de uso y coordina la mayoría de sus acciones; por su parte, las clases entidad simbolizan las unidades de información operadas en el sistema, normalmente son elementos pasivos y persistentes en cuanto muchas veces la información que encapsulan en almacenada en algún tipo de repositorio de datos.

En esta figura, también aparece una Nota de UML, que es el equivalente de un comentario en un lenguaje de programación como C++ o Java. Las notas se pueden asociar a una línea de vida, a un mensaje, a un argumento del mensaje o a cualquier otro elemento sustancial del modelo.

Comentarios Finales

Los diagramas de secuencia pueden ser usados en distintas etapas del ciclo de vida, desde el análisis primitivo de los casos de uso, empleando abstracciones clave del sistema, hasta el diseño detallado, explotando muchos de los recursos del lenguaje de modelado unificado. De esta manera, constituyen un conducto natural entre la especificación de requisitos funcionales (casos de uso) y no funcionales por parte de los Analistas y Arquitectos del software y la implementación y prueba de los mismos por parte de los Programadores y Probadores del sistema.

Que cuántos diagramas son necesarios por caso de uso es una cuestión que dejo a las condiciones particulares de cada proyecto y dentro de este, a cada caso de uso. Primero es importante reconocer que no todos los casos de uso necesitan de un diagrama de secuencia para ser implementados (y en este subconjunto quizás entre el propio caso de uso que usé de ejemplo y que quizás solo sirve para ilustrar los principales conceptos de la realización de casos de uso). El paso a seguir es entonces decidir cuáles son los casos de uso que definen la arquitectura del sistema, es decir, los más complejos o riesgosos desde el punto de vista arquitectónico; de estos, los escenarios que abarquen más elementos del modelo (extensión) y que incluyan muchos mensajes entre ellos o mensajes que impliquen procesos delicados o laboriosos (profundidad), son los mejores candidatos, los más útiles, para diseñar con ellos el sistema a través de diagramas de secuencia. Habitualmente, estos casos de uso son los que se desarrollan durante la fase de Elaboración del proyecto y a menudo requieren de información suplementaria para su diseño: requisitos especiales, arquitectura, restricciones de diseño, longitud y tipos de datos y al final del diseño, restricciones de la plataforma de implementación. Los demás casos de uso, aquellos que son más fáciles o directos de implementar encontrarán, si hicimos bien el trabajo, un camino de ida hacía los elementos (clases, objetos, controles, tablas) que se usaron para realizar los primeros.

Los diagramas de secuencia constituyen así un medio de comunicación efectivo que cura esa fisura, a veces insondable, a veces abierta, que supone el paso de la especificación de requisitos a la programación de los sistemas de cómputo. Ahora bien, lograr comunicar un diagrama, y por extensión un modelo, demanda discernimiento y práctica, pues el diseño (orientado a objetos) de software es por ley un proceso formal y para ejecutarlo debemos adquirir los fundamentos (que rayan en lo estrictamente teórico) para que luego la práctica habitual nos permita perfeccionar la técnica y desarrollar mejores productos en cada nuevo intento.

¡Funciona para mí!

Referencias

1 Object Management Group –OMG. UML Infrastructure Specification, v2.1.1. Page 9.

2,3,4,5,6,7 Salazar Caraballo, Luis A. Prolegómenos Sobre el Lenguaje de Modelado Unificado

8 Object Management Group –OMG. UML Superstructure Specification, v2.1.1. Page 457.

9 Object Management Group –OMG. UML Superstructure Specification, v2.1.1. Page 489.

10 Object Management Group –OMG. UML Superstructure Specification, v2.1.1. Page 491.

Lectura Fundamental Siguiente: “Orientación a Objetos: Un Enfoque Teórico Moderno (Actualizado)”

2 comentarios:

  1. Anónimo4:05 p.m.

    Gracias por compartir tus conocimientos. Estoy haciendo mi tesis un sistema web con el proceso unificado, es una oportunidad de aprender. Benjamin Ecuador julio 2012.

    ResponderEliminar
  2. Anónimo7:08 a.m.

    Lástima que no estén disponibles las imágenes del documento. ¿Podrías adicionarlas de nuevo?. Federico. España. Octubre 2013

    ResponderEliminar