Tema IV. DISEÑO DETALLADO (diseño de bajo nivel) 21

Anuncio
Ingeniería del Software 2 – Curso 2009-2010
Tema IV. DISEÑO DETALLADO (diseño de bajo nivel)
21. Introducción al diseño detallado: diseño por contratos
Qué es el diseño detallado (“diseño de bajo nivel”)
El diseño detallado es la actividad técnica que sigue a la selección de la arquitectura. Es el
último paso en la descomposición orientada a objetos, en el que se llega a las unidades de
programación: las clases de implementación (representaciones simbólicas del código).
Su objetivo es dejar el proyecto preparado para la implementación/codificación:
o parte de los resultados de la fase de arquitectura,
o describe en detalle cada una de las partes de la solución,
o verifica que se satisfacen los requisitos,
o y produce un diseño completo y listo para ser programado.
Un diseño completo guía suficientemente la implementación, de modo que la hace
comprensible y fácil de mantener, pero no necesariamente suprime toda creatividad en la
implementación.
o Es decir, los programadores deben ser capaces de implementar un diseño detallado,
concentrándose en cuestiones de codificación y dependientes de la plataforma
tecnológica (lenguaje de programación, sistema operativo, hardware, etc.).
o Para que un diseño se pudiera implementar de forma no creativa, tendría que ser tan
detallado que no sería práctico.
Nivel de detale en el modelo de diseño
El modelo de diseño detallado requiere encontrar un equilibrio adecuado entre abstracción y
detalle: tiene que ser suficientemente...
o ... abstracto, para que los detalles innecesarios no oculten la esencia del diseño.
o ... detallado, para que los programadores sólo tomen decisiones de codificación.
Tanto los tipos de modelos utilizados como el nivel de detalle alcanzado en los mismos
dependen de la organización (por ejemplo, qué grado de responsabilidad tienen los
programadores) y del sistema desarrollado (complejidad, riesgo, etc.).
Dos tipos principales de modelos (o dos aspectos principales de todo modelo):
o modelo estático: describe la estructura de clases y las relaciones entre ellas.
o modelo dinámico: describe el comportamiento de las clases/objetos.
Conviene comenzar el diseño por los aspectos que presentan mayores riesgos (risk driven).
Un diseño elegante mejora la implementación y el mantenimiento de una aplicación.
Diseño por contratos (design by contract: DbC)
La productividad del software depende de su calidad, que a su vez depende de su fiabilidad.
o Fiabilidad = corrección + robustez.
o La búsqueda sistemática de la fiabilidad empieza por definir con precisión lo que se
espera de cada elemento del software.
o No es una condición suficiente para la fiabilidad, pero sí es una condición necesaria.
Beneficios de DbC:
o Mejor comprensión de la construcción de software (en particular, orientado a objetos).
o Construcción sistemática de sistemas libres de errores.
o Marco eficaz para depuración, pruebas, y aseguramiento de la calidad.
o Documentación de componentes: el contrato explica la finalidad del componente.
o Mejor comprensión del mecanismo de herencia.
o Técnica para tratar los casos anormales (excepciones).
45
Ingeniería del Software 2 – Curso 2009-2010
Noción de contrato
Diseñar contra una interfaz es como emplear un contrato: el proveedor se compromete a
proporcionar un servicio, de modo que el cliente puede ser diseñado sin saber cómo se
implementa este servicio.
Analogía con contratos en asuntos humanos: cliente y proveedor, obligación y beneficio.
Si ambos satisfacen su parte del contrato:
o El cliente puede confiar en que obtendrá lo que el proveedor promete.
o El proveedor puede confiar en que no se le pedirá más de lo que promete.
El uso de interfaces (o clases abstractas) facilita la incorporación de otros subtipos concretos
al diseño sin tener que cambiar el código cliente (menor acoplamiento).
Especificación formal del contrato
Todo contrato tiene dos partes igualmente esenciales: sintaxis y semántica.
La sintaxis del contrato (su interfaz) especifica el aspecto externo del contrato, la información
necesaria para proporcionar el servicio, dada por la firma o signatura de la operación, que
define los valores entregados y devueltos, con sus tipos.
La semántica del contrato (su significado) especifica el contenido propiamente dicho del
contrato, mediante pre- y post- condiciones (expresiones lógicas entre atributos y parámetros).
o Precondición:
El cliente debe asegurarse de que se cumple (es su obligación en el contrato).
El proveedor la comprueba, y no hace nada si no se cumple (su beneficio).
o Postcondición:
El proveedor debe asegurarse de que se cumple (es su obligación en el contrato).
Expresa lo que el cliente espera del servicio (su beneficio).
El nombre del servicio/operación debe estar relacionado con la semántica, pero no la expresa
completamente, ni menos aún la garantiza. La signatura del contrato también es insuficiente
para expresar y garantizar la semántica. Necesitamos pre- y post- condiciones (PPCs).
Ejemplo: cuenta corriente.
o Los nombres no expresan completamente la semántica del contrato: ¿qué pasa con
sacarDinero si la cuenta está vacía? ¿y con meterDinero si la cuenta está cerrada?
o Tampoco garantizan la semántica: ningún lenguaje de programación puede garantizar
que meterDinero y sacarDinero significan lo que el cliente espera que signifiquen.
Es un formalismo perfectamente aplicable en análisis (recordar casos de uso).
Uso de pre- y post- condiciones
La expresión de las PPCs es puramente declarativa, no dice lo que tiene que hacer una
operación, sino sólo el efecto que puede percibir su cliente.
Las PPCs se especifican en el diseño, y se comprueban en tiempo de ejecución.
o En determinadas circunstancias se puede omitir la comprobación de la postcondición:
por ejemplo, servicios proporcionados por componentes muy fiables.
o En cambio, en general conviene al proveedor comprobar siempre la precondición: es
más fácil que el código cliente se equivoque, y comprobarlo puede evitar desastres.
¿Quién las comprueba? Tanto el cliente como el proveedor pueden comprobarlas (ver lo dicho
más arriba sobre la semántica del contrato).
¿Qué ocurre si no se cumplen las PPCs?
o Precondición: indica un error en el cliente, que no debería haber invocado el servicio.
Diversas estrategias: devolver un código de error (forma abreviada de comprobar la
precondición y ejecutar la operación si se cumple), levantar una excepción, etc.
o Postcondición: indica un error en el proveedor, que no ha cumplido su parte del
contrato. Error grave que exige levantar una excepción u otro mecanismo equivalente.
Además de ser útiles para documentar el diseño, las PPCs tienen utilidad especialmente en la
depuración del software.
46
Ingeniería del Software 2 – Curso 2009-2010
Invariantes de clase
Además de las pre- y post- condiciones de cada operación, pueden especificarse también
invariantes de clase, que son aserciones que deber ser verdaderas “en todo momento”.
Las operaciones pueden violarlos temporalmente y de modo controlado durante su ejecución.
Obviamente, la expresión lógica del invariante sólo puede contener atributos (no parámetros).
La violación de un invariante es un error grave que también exige levantar una excepción.
o Igual que con las postcondiciones, a veces se puede omitir la comprobación.
Contratos y herencia: subcontratación
La superclase subcontrata a las subclases para las instancias directas de la subclase.
Pero existe el peligro de redefinir mal un servicio, es decir, modificar su semántica.
La especificación de contratos ayuda a entender y usar correctamente la herencia.
Las instancias de la subclase son también instancias de la superclase, por tanto todo invariante
aplicable a la superclase se aplica también a la subclase (principio de sustitución). El
invariante puede ser más restrictivo en la subclase, pero no menos restrictivo.
Regla o principio de subcontratación: la operación redefinida en una subclase debe…
o mantener o debilitar la precondición (no puede exigir más), y
o mantener o reforzar la postcondición (no puede ofrecer menos).
Por lo tanto, para que pueda existir una relación jerárquica entre dos clases, deben cumplirse
las siguientes implicaciones lógicas (“si A entonces B”):
o subclase.invariante → superclase.invariante
o Y para cada operación redefinida:
subclase.operación.Pre ← superclase.operación.Pre
subclase.operación.Post → superclase.operación.Post
Si esto no se cumple, entonces no debe existir una relación jerárquica entre las clases.
Ejemplo: cuenta corriente y cuenta de crédito.
o La operación sacarDinero está redefinida correctamente.
o Pero el invariante de clase redefinido es menos restrictivo: el saldo podría ser negativo.
o Conclusión: la generalización es incorrecta.
o Solución:
añadir una subclase CuentaDebito, hermana de CuentaCredito.
traladar el invariante (saldo ≥ 0) a esta subclase.
Ejemplo: distancia “oblicua” y distancia “esquinada”. La jerarquía es también incorrecta.
Especificación de algoritmos
Es necesario estudiar el algoritmo antes de programarlo, especialmente si hay ramificaciones,
iteraciones o interacciones (son cosas distintas) complejas, para que las complejidades del
lenguaje de programación no oculten u oscurezcan errores algorítmicos. Dos técnicas clásicas:
Diagrama de actividad (diagrama de flujo de control).
o Hay herramientas de ingeniería inversa, pero tiene más interés la ingeniería directa, es
decir, detectar los errores antes de programarlos.
o Utilidad para lograr un anidamiento correcto en las estructuras de control y evitar el
llamado código spaghetti.
o Hay que tener en cuenta que la orientación a objetos reduce en buena medida la
complejidad de la ramificación, al sustituirla por interacción entre objetos y
polimorfismo (ramificación implícita), lo que en cierta medida reduce la utilidad de este
tipo de diagramas (en el límite: ninguna ramificación).
Pseudocódigo.
o Especificación textual sin detalles del lenguaje de programación.
o Ventajas: un nivel más de estudio, inspección, seguridad, disciplina.
o Desventajas: más trabajo, nuevas posibilidades de error.
47
Ingeniería del Software 2 – Curso 2009-2010
22. Diseño detallado con UML (1): polimorfismo
Asignación polimórfica de “variables”
Salvo en el caso de los tipos elementales de datos, una “variable” es en realidad una referencia
(“enlace”) a un objeto del tipo especificado en la declaración de la variable.
La asignación polimórfica consiste en declarar que una variable es de un clase, y asignarle un
objeto que es instancia directa de una subclase.
Un caso análogo es el uso de un argumento polimórfico: se pasa un objeto que es instancia
directa de una subclase de la clase esperada por la operación.
Reduce dependencias: la clase origen conoce la superclase destino, no sus subclases.
Facilita la extensibilidad: la clase origen no se entera si se añaden nuevas subclases.
Invocación polimórfica de operaciones
En general, una operación se invoca mediante el envío de un mensaje reconocible por la clase
(o superclase) del objeto receptor.
La invocación es polimórfica si:
o la clase invocante conoce la clase donde se define la operación, pero no sus subclases
(invoca la operación tal como está definida en la clase que conoce);
o el objeto receptor es instancia indirecta de esta clase, es decir, instancia directa de una
subclase (no hace falta que la superclase sea abstracta); y
o la operación está redefinida al menos en una de las subclases.
La interfaz de la operación es la misma en la superclase y en todas las subclases, pero el
comportamiento será distinto en función de la subclase efectiva del objeto receptor.
Redefinición polimórfica de operaciones
No confundir sobreescritura (overwriting) con sobrecarga (overloading).
La sobreescritura o redefinición de una operación debe respetar su contrato.
Habitualmente, los lenguajes garantizan la sintaxis del contrato, pero no su semántica. Sólo
una programación disciplinada garantiza el respeto íntegro al contrato en la redefinición.
La sobrecarga implica definir un contrato distinto (sintaxis y, por supuesto, semántica).
Aunque el lenguaje de programación lo permita, no conviene abusar de la sobrecarga.
o Habitualmente es más tolerable el caso de la sobrecarga de los constructores.
Signatura de la operación
La signatura o firma define los elementos que identifican unívocamente una operación.
Se trata de una cuestión complicada, que depende del lenguaje de implementación.
o Por ejemplo, en UML2 el tipo del valor devuelto pertenece a la signatura.
o No obstante, en Java no podemos usar el tipo del valor devuelto para distinguir métodos
sobrecargados, por tanto no sería parte de la signatura.
Esta diferencia con Java se explica probablemente porque UML adopta el punto de vista de
identificar unívocamente una operación desde el punto de vista de su declaración en el
modelo, mientras que Java (y posiblemente otros lenguajes) adopta el punto de vista de
identificar unívocamente una operación desde el punto de vista de su invocación.
La selección del método en tiempo de ejecución en función de los tipos de los parámetros es
un tema complicado que depende del lenguaje concreto de implementación, como reconoce el
propio estándar UML.
En función del nombre de la operación, su signatura, y su clase, el resultado puede ser una
operación distinta, una operación sobrecargada (es decir, parecida pero distinta), una
operación sobreescrita, o un error (detectable por el compilador).
Por tanto, es importante saber si dos signaturas son iguales o no.
o Ejemplo: x(a: A), x(b: B), ¿qué ocurre si B es subclase de A?
48
Ingeniería del Software 2 – Curso 2009-2010
Visibilidad
Característica que indica si un atributo u operación puede ser visto desde otras clases.
Los “niveles” de visibilidad varían en UML y en los diversos lenguajes de programación.
La visibilidad es una propiedad de las clases, no de los objetos. Un objeto…
o puede acceder a operaciones y atributos privados de otros objetos de la misma clase.
o no puede acceder a “sus” operaciones y atributos privados definidos en una superclase.
En definitiva, es una propiedad analizada no en tiempo de ejecución (objetos) sino en tiempo
de compilación (clases): “qué parte del código puedo ver desde otra parte del código”.
Control de la visibilidad para garantizar los invariantes de la clase:
o Asignar visibilidad privada (o protegida) a los atributos.
o No basta proporcionar simples operaciones get y set para acceder a los atributos. Si no
incluyen la garantía de los invariantes, ¿qué mejora aportan respecto al acceso directo a
los atributos públicos?
o Solución: proporcionar operaciones públicas con contratos bien establecidos.
Interfaces
Clases y operaciones abstractas.
Interfaz: conjunto de operaciones que ofrecen un servicio coherente.
o Nota: la operación imprimir( ) de la transparencia significa ser impreso, por eso tiene
sentido que la clase Documento realice la interfaz Imprimible. Si imprimir( ) significase
imprimir algo, entonces la interfaz debería llamarse Imprimidor, y podría ser realizada
por la clase Impresora.
Semejanza con clases abstractas.
Relación con la visibilidad: interfaz natural.
Generalización vs. Realización.
49
Ingeniería del Software 2 – Curso 2009-2010
23. Diseño detallado con UML (2): interacciones
Modelado dinámico: diagramas de interacción
El modelado dinámico introduce un elemento nuevo: el tiempo.
Los diagramas de interacción representan objetos enlazados y mensajes intercambiados:
o Diagrama de colaboración (“diagrama de comunicación” en UML2):
objetos y enlaces,
mensajes numerados,
expresa mejor la estructura de la colaboración de objetos, cómo están enlazados.
o Diagrama de secuencia:
sólo objetos (enlaces implícitos),
mensajes en posición relativa sobre la línea de vida,
expresa mejor la secuencia de mensajes, sobre todo en interacciones complejas.
Equivalencia parcial:
o Ambos representan una colaboración de objetos y una secuencia de mensajes.
o Cada uno expresa mejor uno de los dos aspectos: estructura, cronología.
o Se debe escoger el que mejor convenga para representar cada interacción.
La descripción de interacciones se usa en análisis y en diseño (casos de uso, operaciones).
o ¿Quién activa la secuencia de mensajes?
La prestación de un servicio se modela mediante el intercambio de mensajes entre objetos:
o Un objeto recibe una petición de servicio (mensaje) y responde a ella.
o Este objeto puede requerir a su vez los servicios de otros objetos (delegación).
Elementos de los diagramas de interacción
Mensajes y operaciones.
Foco de control.
Números de secuencia.
Mensajes síncronos y asíncronos
No confundir el flujo de control con el flujo de información.
Mensaje síncrono: flujo de control bidireccional (ida y vuelta).
o El flujo de información puede ser unidireccional o bidireccional, en función de que el
mensaje tenga sólo argumentos de entrada/salida (uni-), o ambos (bi-).
Mensaje asíncrono: flujo de control y de información unidireccionales (sólo ida).
En ambos casos la dependencia es unidireccional (invocante invocado).
Los mensajes asíncronos posibilitan el modelado de hilos de ejecución concurrente.
Tipos de relaciones entre clases en UML
Cuatro tipos básicos: dependencia, asociación, generalización, realización.
Significado de la relación de dependencia en UML:
o El elemento dependiente requiere la presencia del elemento independiente.
o Los cambios en el elemento independiente pueden afectar al elemento dependiente.
La dependencia es una relación en sí misma, pero otras relaciones pueden inducirla.
El diseño debe minimizar las dependencias con vistas a facilitar el mantenimiento. Ahora
bien, ¿qué dependencias, de dónde salen, quién las ha puesto ahí?
o Algunas dependencias son explícitas: el diseñador las ha representado.
o Otras dependencias existen, aunque no estén representadas en el diseño.
La dependencia se estudia en “tiempo de compilación”, no en “tiempo de ejecución”. No
importa que un objeto necesite de otro para activarse, no importa en qué sentido fluya la
información o el control. Lo que importa es qué clases menciona o conoce una clase dada.
50
Ingeniería del Software 2 – Curso 2009-2010
Dependencias inducidas por las relaciones entre clases
Dependencia y generalización:
o Toda generalización induce una dependencia subclase superclase.
Dependencia y asociación:
o Toda asociación induce una dependencia en el sentido en que es navegable:
unidireccional: dependencia sólo en un sentido, menor acoplamiento.
bidireccional: dependencia mutua, mayor acoplamiento.
o Otras dependencias análogas, inducidas por asociaciones contextuales, denominadas así
porque sólo son válidas en el contexto de la ejecución de una operación de una clase:
dependencia respecto a las clases de los parámetros de sus operaciones.
dependencia respecto a las clases de las variables locales de sus operaciones.
o Reducir estas dependencias usando interfaces en lugar de clases para declarar los tipos:
convertir Color, Vector y Posicion en interfaces.
Resumen: hay dependencia A B cuando A menciona o conoce a B.
Tipos de enlaces de comunicación
Todo mensaje requiere un destinatario, que se especifica mediante un enlace que los conecta.
o ¿Es todo enlace de comunicación una instacia de una asociación?
Dos tipos de enlaces de comunicación: estructurales y contextuales.
o Enlaces estructurales: determinados por la estructura de asociaciones entre clases.
o Enlaces contextuales: determinados por el contexto de ejecución de las operaciones.
Correspondencia entre diagramas de interacción y diagramas de clases
Objeto-Clase
Enlace-Asociación
Mensaje-Operación/Señal
Creación y destrución de objetos y enlaces
Objetos: mensajes de creación y destrucción
Enlaces: creación y destrucción implícitas / explícitas
Requiere comprender la diferencia entre enlaces estructurales y contextuales.
Ley de Demetra (Law of Demeter): “No hable con desconocidos”
Es una “regla de estilo” que procura garantizar que, si dos objetos pueden comunicarse,
entonces el código de sus respectivas clases debe manifestarlo claramente, consiguiendo así
hacer explícitas las dependencias entre clases.
El nombre proviene de la diosa griega de la agricultura Demetra (o Deméter), que dio nombre
al Demeter Project (Northeastern University, Boston, 1987). “El software crece poco a poco”.
El objeto x, en respuesta al mensaje m(a, b, c...), puede enviar mensajes sólo a:
o El objeto x («self»).
o Los argumento del mensaje m: a, b, c... («parameter»).
o Objetos creados por x en el curso de su respuesta a m («local»).
o Objetos directamente enlazados con x («association»).
Formulada en términos de UML 1.x:
o Todo enlace es instancia de una asociación (estructural / contextual).
o Toda asociación debe ser declarada (dependencia explícita).
La Ley de Demetra no es una regla absoluta, pero sí conveniente, hace el código más seguro.
51
Ingeniería del Software 2 – Curso 2009-2010
¿Qué prohíbe la Ley de Demetra?
Dos violaciones típicas:
o Objeto global (clase pública con instancia única, acceso por atributo o método estático):
Sistema.reloj, Sistema.ahora()
Para algunos, la posibilidad de referirse a un objeto global (reloj), sin necesidad
de declarar una asociación, manifiesta la existencia de un nuevo tipo de enlace
contextual («global»).
Posiblemente menos problemático que el caso siguiente.
o Enlace compuesto o derivado (concatenar enlaces u operaciones):
titularDe.obtenerSaldo().escribir(), titularDe.numeroCuenta.escribir()
Consiste en enviar un mensaje al objeto devuelto por una operación, o por una
concatenación de atributos (menos frecuente porque suelen ser privados).
Ejemplo: la clase del objeto invocante (Persona) debe mencionar la clase de la
variable titularDe al declararla (CuentaCorriente, por tanto la conoce), y por tanto
sabe que puede invocar el método obtenerSaldo.
En cambio, no menciona la clase del objeto devuelto por obtenerSaldo, y por
tanto la dependencia respecto a esa clase (Moneda) queda oscurecida: ¿cómo sabe
que puede invocar la operación escribir? Aumentan las posibilidades de error.
Solución: declarar una variable «local» (haciendo explícita la dependencia
respecto a su clase) y asignarle el valor antes de invocar una operación en ella.
Conclusión: si no se respeta la letra de la ley, debe respetarse al menos su espíritu.
52
Ingeniería del Software 2 – Curso 2009-2010
24. Diseño detallado con UML (3): máquinas de estados
Modelado dinámico: máquinas de estados
Modelado dinámico, dos tipos complementarios de diagramas:
o Diagramas de interacción: ilustran o ejemplifican el comportamiento.
o Diagramas estados: especifican el comportamiento de modo abstracto.
o Las máquinas de estados determinan todas las interacciones que son posibles.
Una máquina de estados es una descripción abstracta del comportamiento:
o Está asociada a una clase (o elemento análogo: componente, subsistema...).
o Describe el comportamiento de los objetos en respuesta a los eventos externos.
o Contempla todas las posibles situaciones y reacciones, típicamente es cíclica.
o Representa el comportamiento de una clase, no de varios objetos en interacción.
Los objetos pertenecientes a una clase con máquina de estados asociada se comportan como
objetos reactivos con memoria:
o Esperan la llegada de eventos externos (típicamente mensajes).
o Reaccionan de forma distinta en función del estado que recuerdan.
o Son deterministas: un mismo evento no puede conducir a estados distintos.
El emisor de los mensajes no se representa en un diagrama de estados:
o No es necesario conocer el origen de los eventos para describir la reacción.
o Pero es muy útil conocerlo para comprender el comportamiento global del sistema.
Elementos del diagrama de estados
Esenciales: estados, eventos y transiciones:
o Los estados semánticamente relevantes de una clase no pueden determinarse
automáticamente (ej.: ¿un estado por cada valor de Persona.edad?).
Guardas (no confundir con evento de cambio):
o La guarda requiere la ocurrencia de un evento para ser evaluada.
o La condición asociada a un evento de cambio (cláusula when) es evaluada
continuamente, y es ella misma la que genera el evento.
Acciones y actividades:
o Acciones asociadas a transiciones.
o Acciones asociadas a un momento puntual en un estado.
o Actividades asociadas a toda la duración de un estado.
Correspondencia entre diagramas de estados y diagramas de clases.
o Tanto clases activas (con doble trazo) como pasivas pueden tener máquinas de estados.
o Sólo las clases activas pueden tener actividades interrumpibles por eventos externos.
Comunicación entre máquinas de estados.
Diseño e implementación de máquinas de estados.
¿Qué ocurre cuando se recibe un evento?
¿Es relevante para el estado actual?
o Si no hay ninguna transición disparada por ese evento, el evento se descarta.
¿Existe una guarda asociada al evento?
o Si la transición no es autorizada por la guarda, el evento no tiene efecto.
¿Existe una actividad asociada al estado origen? se interrumpe.
¿Existe una acción de salida? se ejecuta.
¿Existe una acción asociada al evento?
o Se ejecuta la acción o secuencia de acciones, en el propio objeto o en otros.
¿Existe una acción de entrada? se ejecuta.
¿Existe una actividad asociada al estado destino? empieza a ejecutarse.
Finalmente, se devuelve el control al emisor del mensaje.
53
Ingeniería del Software 2 – Curso 2009-2010
25. Patrones de diseño (1)
Introducción a los patrones de diseño
En lugar de reinventar la rueda, intentamos reutilizar diseños que hayan demostrado su
eficacia en previas aplicaciones. Los patrones de diseño son combinaciones de clases y
métodos interrelacionados que la experiencia ha demostrado que resuelven ciertos problemas
comunes de diseño.
Ejemplo: colección de clases que implementan un árbol de objetos.
Analogía: el patrón “rancho” (“caserío”, “cortijo”…) para casas aisladas con gran extensión
de terreno. El “rancho” es una idea de diseño que permite muchas realizaciones alternativas:
no es un conjunto inalterable de planos de construcción.
Gamma et al. difundieron los patrones de diseño entre el público con su ya clásico libro, en el
que introducen 23 patrones divididos en patrones de estructura, de creación y de
comportamiento. Naturalmente, puede haber más, inventados por otros autores.
o Patrones de estructura: formas de representar conjuntos de objetos, donde el conjunto
puede ser tratado de manera elegante como una única entidad. Ejemplo: árboles y listas
enlazadas.
o Patrones de creación: formas de crear objetos complejos como árboles o redes.
o Patrones de comportamiento: formas de capturar el comportamiento de objetos, por
ejemplo, recorriendo colecciones de objetos en determinado orden.
Son aplicables tanto a nivel de diseño arquitectónico como a nivel de diseño detallado.
No conviene usarlos en los modelos de análisis, ya que estos deben ajustarse lo más posible a
los requisitos, sin incluir “soluciones” (diseño).
Funcionamiento:
o Cada patrón de diseño presta un servicio específico requerido por un código cliente: el
patrón ejecuta el código necesario sin que el cliente tenga que conocerlo, y este código
utiliza la estructura definida en el patrón.
o El patrón de diseño define un punto de entrada para el cliente (habitualmente uno o
varios métodos de una clase dentro del patrón).
o También existe el “código de puesta a punto”, que inicializa el estado del patrón, y hace
posible que los clientes conozcan lo menos posible de la estructura interna del patrón de
diseño.
Relaciones entre marcos y patrones
Marco: conjunto de clases reutilizable, librería (reutilizar desarrollos).
Patrón: estructura que proporciona una determinada función (reutilizar ideas).
Relación entre ambos:
o Los marcos implementan patrones
o Los marcos pueden implementar otras ideas que no sean “patrones”
o Los patrones pueden implementarse directamente sin usar marcos
Relaciones entre estilos arquitectónicos y patrones de diseño
Diferencia: nivel de abstracción.
Semejanzas:
o Los patrones de diseño puede aplicarse en el diseño de la aquitectura.
o Los estilos arquitectónicos suelen incluir patrones de diseño.
o Los patrones de diseño se basan en ideas que también son aplicadas a menudo en los
estilos arquitectónicos (ejemplo: delegación, fachada, Proxy o agente).
54
Ingeniería del Software 2 – Curso 2009-2010
Descripción de un patrón de diseño
Un patrón describe un problema recurrente, y el núcleo de la solución a ese problema, de
forma que se puede usar esta solución muchas veces sin hacerlo nunca de la misma manera.
En general, un patrón consta de cuatro elementos esenciales:
o Nombre (la enfermedad): para incrementar nuestro vocabulario de diseño.
o Problema (los síntomas): describe cuándo aplicar el patrón, el problema y su contexto;
por ejemplo, una estructura de clases que revela un diseño inflexible.
o Solución (la receta): describe los elementos de que consta el diseño, sus relaciones,
responsabilidades y colaboraciones; no describe un diseño concreto, sino una plantilla
(template) que puede aplicarse en muchas situaciones.
o Consecuencias (los efectos secundarios): balance de costes y beneficios de la
aplicación del patrón (mayor complejidad en el código frente a ventajas de su uso).
Los patrones son: descripciones de la estructura, comportamiento y comunicación de un
grupo de objetos y clases que resuelven un problema general de diseño y que deben
adaptarse a cada contexto particular (una plantilla de la solución a un problema recurrente).
Los patrones no son:
o Diseños codificados y encapsulados en clases reutilizables (listas enlazadas o tablas
hash...): eso son herramientas o utilidades (toolkits), que pueden implementar patrones.
o Diseños complejos específicos de un dominio concreto para una aplicación o
subsistema: eso son marcos (frameworks), que pueden usar patrones.
Patrones de creación: Abstract Factory
Propósito: proporcionar una interfaz para crear familias de objetos relacionados o
dependientes sin especificar sus clases concretas.
Ejemplos:
o Editor de documentos adaptable a distintos estándares de apariencia.
o Juegos con representación 2D o 3D.
o Diseño de interiores: mobiliario de una cocina.
Problema:
o El cliente usa una familia de objetos relacionados.
o Se desea flexibilizar el diseño para que sea fácil cambiar de una familia a otra.
o La estructura de la familia es común en todas las familias: cambia el tipo de familia,
pero no la forma de usarla (interfaz); cada familia representa un “estilo” diferente para
objetos conceptualmente iguales.
Solución:
o Se define una interfaz (clase abstracta) para cada objeto o producto de la familia: el
cliente conoce esta interfaz, pero no la clase concreta utilizada.
o Cada familia tiene una fábrica de objetos asociada; las distintas fábricas de objetos
tienen también una interfaz común.
o Se crea una fábrica concreta (instancia de la subclase concreta; patrón Singleton); es
fácil cambiarla en tiempo de ejecución; la fábrica escogida determina la familia entera,
estilo de presentación, etc.
o En lugar de usar los constructores concretos (mencionar las clases concretas), la fábrica
proporciona una interfaz uniforme para la creación de objetos de todas las familias.
Consecuencias:
o (+)El cliente manipula los objetos de la familia a través de la definición abstracta de la
familia (interfaz común); no menciona en absoluto las clases concretas utilizadas, que
sólo son conocidas por la fábrica.
o (+)Es fácil cambiar en tiempo de ejecución la familia concreta utilizada (estilo).
o (+)Facilita el uso consistente de objetos de una misma familia (sin mezclas).
o (–)Dificulta la adición de nuevos productos a la familia, ya que habría que modificar la
interfaz de las fábricas y todas las fábricas concretas.
55
Ingeniería del Software 2 – Curso 2009-2010
26. Patrones de diseño (2)
Cómo resolver problemas con patrones de diseño
Idea general: diseñar para el cambio, reducir dependencias, incrementar flexibilidad.
Herencia vs. delegación:
o Herencia: reutilización de caja-blanca (fuerte dependencia de la superclase).
o Asociación: reutilización de caja-negra (débil dependencia entre clases).
o Un objeto delega en otro la respuesta a una operación, análogamente a como una
subclase delega en la superclase la implementación (“invocación” de super).
o Ejemplo: heredar un algoritmo de ordenación / delegarlo a un objeto enlazado.
o El equilibrio se desplaza de la estructura estática en tiempo de compilación
(generalizaciones) a la estructura dinámica en tiempo de ejecución (asociaciones).
Ventajas: posibilidad de reuso múltiple (ya que no existe la herencia múltiple).
Inconvenientes: más “código pegamento”, dependencias menos claras, menos
eficiencia de ejecución, más instanciación, mayor complejidad y dificultad para
entender la estructura estática y dinámica.
Para flexibilizar el comportamiento en tiempo de ejecución es necesario combinar
inteligentemente asociaciones (delegación) y herencia. La asociación permite cambiar el
objeto enlazado, y la herencia polimórfica permite que haya comportamientos distintos.
Patrones de estructura: Decorator
Propósito: añadir nuevas responsabilidades a un objeto de modo dinámico; ofrece una
alternativa más flexible que la herencia para extender la funcionalidad.
Ejemplos:
o Añadir elementos gráficos en un editor de documentos.
o Añadir efectos sonoros o visuales en un juego.
Problema:
o Es posible añadir responsabilidades mediante la especialización: la subclase hace todo
lo que hace la superclase, y añade los efectos deseados.
o Pero este diseño resulta inflexible, ya que las responsabilidades se definen de modo
estático, no es posible añadirlas en tiempo de ejecución.
o Podría producirse incluso una explosión de especializaciones, para intentar reflejar
todas las posibles combinaciones de responsabilidades.
Solución:
o Empaquetar el objeto dentro de otro que añade las responsabilidades.
o El objeto que empaqueta o decorador tiene la misma interfaz que el objeto
componente, de modo que su presencia es transparente al cliente.
o Los decoradores se pueden empaquetar de modo recursivo, formando una cadena de
decoradores todo lo larga que se quiera hasta llegar al componente concreto.
o La cadena se puede modificar fácilmente en tiempo de ejecución, añadiento o quitando
responsabilidades.
o Cada operación del decorador se transmite por la cadena hasta llegar al componente
concreto; cada decorador puede añadir responsabilidades antes o después en cada paso.
Consecuencias:
o (+)Mayor flexibilidad que la herencia, que es estática.
o (+)Permite la adición incremental de responsabilidades, en lugar de intentar soportar
todas las responsabilidades imaginables en clases complejas.
o (–)Aunque la interfaz sea la misma, la identidad del decorador no es la misma que la
del componente: transparencia de interfaz, pero no de identidad.
o (–)Añade multitud de pequeños objetos que sólo se diferencian por la forma o el orden
en que están conectados, dificultando la comprensión de la estructura.
56
Ingeniería del Software 2 – Curso 2009-2010
Patrones de comportamiento: Command
Propósito: encapsular una petición de servicio dentro de un objeto (“todo es un objeto,
incluso los comandos”), de modo que se permita parametrizar las operaciones de otro objeto,
mantener colas o registros (logs) de operaciones, y soportar operaciones reversibles.
Ejemplos:
o Caja de herramientas para GUI: acciones en respuesta a entradas de usuario.
o Sistema transaccional: establecer los parámetros de la transacción y ejecutarla.
o En general, cualquier sistema que distinga dos niveles: operaciones de usuario de alto
nivel, construidas sobre operaciones primitivas de bajo nivel.
Problema:
o La caja de herramientas es previa a cualquier aplicación concreta que la use: no puede
implementar explícitamente la respuesta a la petición en cada elemento, sino que esta
respuesta debe ser parametrizable; sólo la aplicación sabe qué hacer con qué objeto.
o Se hace difícil compartir operaciones entre distintos elementos de la interfaz.
o Flexibilidad: desacoplar la interfaz de usuario de las operaciones subyacentes (MVC).
o Parametrizar la acción deseada mediante herencia conduce a una explosión de subclases
(copyChar, copyWord, copyLine, copyParagraph, etc.); además, si las acciones son muy
distintas y usamos herencia, las subclases resultan muy heterogéneas.
o En general, necesidad de definir y manejar acciones abstractas, sin conocer en concreto
ni el receptor de la operación ni cómo se materializará en él la operación requerida.
Solución:
o Convertir la petición misma en un objeto y sacarla del objeto invocador.
o Command declara una interfaz para ejecutar operaciones: execute( ).
Ejemplos: PasteCommand, OpenCommand, MacroCommand.
o Las subclases concretas de Command especifican los pares receptor-acción.
o El invocador (la interfaz de usuario) no sabe qué subclase de comando utiliza, por
tanto desconoce el receptor final y la petición enviada: sólo sabe ejecutar el comando.
o El cliente (la aplicación) enlaza invocador con comando, y éste con receptor.
o Comandos reversibles: añadir unexecute( ), state, history list…
o Comandos registrados: añadir store( ), load( ).
Consecuencias:
o (+)Desacopla el objeto que invoca la operación del objeto que sabe cómo realizarla.
o (+)Los comandos son objetos manipulables y extensibles como cualquier otro.
o (+)Permite combinar comandos en un comando compuesto (macros): patrón Composite.
o (+)Es fácil añadir nuevos comandos sin cambiar las clases existentes.
Cómo seleccionar un patrón de diseño
Considerar cómo los patrones de diseño resuelven problemas de diseño.
Explorar el “propósito” (intent) de cada patrón.
Estudiar cómo se interrelacionan los patrones.
Estudiar patrones de propósito similar.
Examinar posibles causas de rediseño.
Considerar lo que podría variar en el diseño.
Cómo usar un patrón de diseño
Leer el patrón entero para tener una impresión general sin profundizar.
Volver a estudiar la Estructura, Participantes y Colaboraciones.
Observar la Muestra de Código para ver un ejemplo concreto del patrón.
Elegir nombres para los participantes que sean significativos en el contexto de la aplicación.
Definir las clases.
Definir nombres específicos de la aplicación para las operaciones del patrón.
Implementar las operaciones para llevar a cabo las responsabilidades y colaboraciones.
57
Ingeniería del Software 2 – Curso 2009-2010
27. Implementación de asociaciones UML en Java (artículo)
Lectura obligatoria y examen
Gonzalo Génova, Carlos Ruiz del Castillo, Juan Llorens. “Mapping UML Associations into
Java Code”, Journal of Object Technology, 2(5): 135-162, Sep-Oct 2003.
1. Implementación de asociaciones: problemas de las asociaciones múltiples.
Una asociación múltiple es aquella en la que la multiplicidad máxima es mayor que uno.
Un array de objetos no es adecuado en general para implementarla, debido a que su tamaño es
fijo al instanciarlo.
Por tanto, el caso general requiere el uso de colecciones de objetos, que a su vez presentan dos
problemas:
o Pueden contener cualquier número de elementos, por tanto será necesario comprobar en
tiempo de ejecución que se cumple la multiplicidad máxima.
o Pueden contener cualquier tipo de objetos, por tanto será necesario comprobar en
tiempo de ejecución que sólo se almacenan objetos del tipo correcto.
Puede haber además problemas de precedencia al borrar o añadir enlaces. Si no se permite la
violación temporal de la multiplicidad máxima al añadir enlaces, entonces será distinto el resultado
de añadir + borrar, o bien borrar + añadir.
2. Implementación de una asociación bidireccional sencilla-sencilla: esquema básico y tareas
que deben realizar los métodos mutadores y lectores.
Esquema de implementación: Dos referencias cruzadas sincronizadas.
Mutadores: Preservan la sincronización (sólo si ambos extremos están de acuerdo, y de forma
atómica). No comprueban multiplicidad mínima pero sí máxima.
Orden en mutadores: debe ser primero borrar, luego añadir.
Métodos lectores: Comprueban la multiplicidad.
Cuestión para el debate: ventajas e inconvenientes de definir la operación set( ) en la interfaz
La interfaz uniforme para todo tipo de asociaciones define tres métodos mutadores
sobrecargados remove y dos métodos mutadores sobrecargados add. Supongamos que se definen
también tres métodos sobrecargados set:
o int set( ); // borra todos los enlaces existentes
o int set(%Target new_link); // borra todos los enlaces existentes y
añade un único enlace nuevo con el valor pasado como parámetro
o int set (Collection new_links); // borra todos los enlaces existentes
y añade tantos enlaces nuevos como los contenidos en la colección
pasada como parámetro
Se pueden plantear dos cuestiones interesantes: ¿Sería posible sustituir los mutadores remove
y add por los nuevos mutadores set? ¿Sería conveniente añadir los nuevos mutadores set a la
interfaz? Respecto a la primera pregunta, observemos que set es redundante, ya que puede
traducirse fácilmente en combinaciones de remove y add. Por ejemplo:
o set(%Target new_link) remove( ); add(%Target new_link)
Por el contrario, es imposible construir remove y add a base de set. Por lo tanto, set no es
suficiente para proporcionar una interfaz útil. Respecto a la segunda pregunta, aunque set sea
redundante, efectivamente puede ser útil su inclusión en la interfaz, ya que proporciona una forma
abreviada de realizar operaciones comunes, que incluso podrían realizarse de forma optimizada.
Cuestión para el debate: colecciones genéricas en nuevas versiones de Java
En el artículo (sección 2.2, página 7) se dice que no es posible especializar las colecciones
estándar de Java para que almacenen objetos que pertenezcan sólo a una clase determinada. Esto era
verdad en la época que se escribió el artículo (2003), pero versiones posteriores de Java sí permiten
esta especialización.
58
Ingeniería del Software 2 – Curso 2009-2010
28. Herencia múltiple
Qué es la herencia múltiple
Una clase hereda sus características (atributos y operaciones) de dos o más superclases:
o Las instancias directas de la subclase son instancias indirectas de las superclases.
o La subclase puede redefinir las operaciones de las superclases (en el ejemplo, sólo p).
No debe confundirse con la “clasificación múltiple”:
o Un mismo objeto es simultáneamente instancia de varias clases.
o Variante: un objeto puede cambiar de clase (“clasificación dinámica”, metamorfosis).
o Conceptualmente puede ser muy interesante, pero los lenguajes de programación
habituales no implementan esta posibilidad (¿cómo se instanciarían los objetos?).
No debe confundirse con la implementación múltiple de interfaces:
o Impropiamente denominada a veces “herencia múltiple de interfaces”.
o Las interfaces no definen atributos (en UML 1.x y Java): hay que definirlos en la clase.
o Las interfaces sólo definen la signatura de las operaciones: hay que implementarlas.
o Las instancias de la clase pueden considerarse instancias indirectas de las interfaces.
Cuándo es necesaria la herencia múltiple
Cuando resulte natural para resolver un problema:
o Lograr que un objeto pueda desempeñar dos o más roles distintos.
o Combinar dos o más abstracciones distintas, aprovechando las características de ambas.
Ejemplos:
o ProfesorAlumno: clase que reúne las capacidades de Profesor y Alumno.
o PacienteCliente: clase que reúne las capacidades de Paciente y Cliente de un Hospital.
Estos dos aspectos tienen soluciones parciales en los lenguajes habituales:
o El uso de interfaces múltiples permite a un objeto desempeñar roles distintos, pero no
permite a una clase reutilizar y redefinir operaciones.
o El uso de delegación permite a una clase reutilizar y redefinir operaciones, pero en
cambio no permite a un objeto desempeñar múltiples roles.
La herencia múltiple trata de resolver ambos problemas simultáneamente.
Debe cumplirse siempre el principio de sustitución de Liskov: si sólo se pretende la herencia
múltiple para reutilizar código, entonces es mejor usar el mecanismo de delegación.
Problemas conceptuales de la herencia múltiple
El problema esencial es la colisión de nombres, que se agrava en el caso (muy frecuente) de la
herencia replicada (“problema del diamante”), y que en general obliga a definir una estrategia
para redefinir operaciones y renombrar atributos.
o Operaciones: ¿qué hace G.p( )? La decisión no puede ser automática, pero el
compilador puede avisar del problema. Se puede resolver redefiniendo obligatoriamente
G.p( ) como una combinación adecuada de E.p( ) y F.p( ), definida por el programador.
o Atributos: ¿qué es G.x, cuál es su tipo, puede tener un valor por cada rol heredado? Los
atributos no se pueden redefinir, pero sí se pueden renombrar, y así conservar ambos.
Conclusión:
o Problema conceptual (el más serio): ¿qué quiero hacer, cuál es la combinación adecuada
de dos métodos heredados con el mismo nombre?
o Problema técnico: ¿cómo lo hago, cómo funciona el compilador de un lenguaje que
implementa herencia múltiple?
o Las soluciones técnicas pueden derivar en problemas complicados y de eficiencia.
o Los lenguajes más difundidos evitan el problema, pero renunciando a algo valioso.
o En todo caso es un problema de la hija, no de las madres: las superclases no deben verse
afectadas por el hecho de que alguna subclase herede de ellas (son independientes).
59
Ingeniería del Software 2 – Curso 2009-2010
Estrategia de implementación de herencia múltiple en lenguajes que no la soportan
Añadir las interfaces iA e iB, realizadas respectivamente por A y B.
C realiza también las interfaces iA e iB: esto garantiza que sus objetos pueden desempeñar
roles distintos.
Añadir las clases auxiliares Ca y Cb, que heredan respectivamente de A y B.
C se asocia con Ca y Cb para habilitar la delegación de p( ) y q( ): posibilita la reutilización.
Ca y Cb heredan p( ) y q( ) pero no las redefinen (son meras clases intermediarias, fáciles de
mantener, incluso creadas automáticamente). La redefinición, si se desea, debe estar en C.
Ca y Cb no son superfluas: en el caso general, A y B pueden ser abstractas, pero para delegar
hacen falta clases concretas que sean instanciables. Incluso C podría ser abstracta.
Limitaciones de esta estrategia
Obviamente, la estrategia no resuelve por sí misma el problema de la colisión de nombres,
porque es un problema conceptual, no técnico. La implementación de C debe resolverlo.
Requiere “código pegamento” en C para delegar en las auxiliares Ca y Cb. Es un problema
leve, y además la escritura de código pegamento es automatizable.
No permite la herencia de atributos, porque las interfaces no tienen atributos. Se puede
resolver facilitando el acceso a los atributos mediante operaciones.
No permite el acceso a atributos públicos de las superclases A y B, por parte del código
cliente de C. Para lograrlo habría que añadir getters y setters en C, y además habría que
modificar el código cliente. Ejemplo, para un atributo x de tipo X heredado de A, usado en el
código cliente a través del enlace c:
o Para usar su valor en expresiones, usar “getX( )” en lugar de “x”:
w = c.x+1 w = c.getX( ) + 1
o Para modificar su valor, trasformar “x = expr” en “setX(expr)”:
c.x = w+1 c.setX(w+1)
o Cuando x es pasado como parámetro modificable en la invocación de una operación,
hace falta una variable local adicional en el cliente, más aún si se trata de una operación
de otra clase distinta, para limitar el alcance de las modificaciones al cliente inmediato:
op(c.x) X x1 = c.getX( ); op(x1); c.setX(x1)
El código cliente debe modificarse: en lugar de definir variables de tipo A y B, hay que
definirlas de tipo iA e iB para permitir que haya objetos con los dos roles simultáneamente.
En todo caso, programar contra interfaces es un estilo habitualmente muy recomendable.
Hay que modificar las clases A y B, aunque sólo sea para decir que realizan iA e iB. Esto
contradice el principio de que los problemas debe resolverlos la hija, no las madres, y en
algunos casos ni siquiera será posible hacerlo (cuando A y B son clases de librería).
Y surgirán otras limitaciones, si escarbamos lo suficiente... pero la solución es útil.
Otros aspectos interesantes (fuera de programa)
Ejemplos: sirve cualquier situación en la que hay una clase con dos asociaciones (dos roles).
Sincronizable, Ordenable, Imprimible, CuentaPersonalEuro, VehículoTerrestreAéreo,
ConductorFrancésInglés, FiguraSimpleCompuesta...
Problemas conceptuales adicionales:
o Contratos de las operaciones, ¿cómo se heredan?
o Constructores: el constructor de la subclase invoca implícitamente el de la superclase,
¿entonces se invoca dos veces, por dos caminos distintos, el constructor de la supersuperclase?
Lenguajes que incorporan HM: Eiffel, Python, C++, Timor...
Otras estrategias de implementación: objetos mellizos, aplanamiento, expansión...
La variante asimétrica: heredar de una clase, implementar la interfaz de otra.
Otras limitaciones de la estrategia vista, particularmente el problema de la visibilidad.
HM y patrones, especialmente Decorator, Composite, State.
Implementación de asociaciones basada en HM.
60
Descargar