Resumen de Diseño (Ingeniería I)
Diseño
Diseñar involucra estructurar la solución utilizando abstracciones y relaciones entre las abstracciones apropiadas para poder:
- Documentar y Comprender la propuesta de solución
- Razonar sobre su grado de adecuación c.r.a los requerimientos
- Comunicarla
- Implementarla
- Verificar/Validar el producto final
- Modificar/Adaptar la solución en la medida que cambien los requerimientos
Objetivos
- Descomponer el sistema en entidades de diseño más chicas (ej: paquetes, clases, módulos, componentes)
- Determinar la relación entre entidades (ej dependencias)
- Fijar mecanismos de interacción (ej memoria compartida, RPC, llamadas a función)
- Especificar interfaces y funcionalidad de entidades (ej operaciones y sus aridades, descripción formal/informal de comportamiento)
- Identificar oportunidades para el reuso (tanto top-down como bottom-up)
- Documentar todo lo anterior junto con la fundamentación de las elecciones
Vistas
Una solución puede descomponerse o enfocarse desde varias vistas diferentes, para así reducir la complejidad del sistema y facilitar su comprensión, comunicación y análisis. Cada vista enfoca en una problemática en particular, permite responder cierta clase de preguntas y admite varios niveles de abstracción y técnicas de modelado.
Pueden clasificarse en estática, dinámica o de despliegue. Otra clasificación es en físicas (estructura visible) o lógicas (bussiness rules).
Vista Estática
Relacionada con el agrupamiento del código.
- Métodos, procedimientos, clases, librería, DLLs, APIs, paquetes, módulos
- Usa, subclase, contiene, depende-de, etc
- Diagrama de clases y de paquetes
Vista Dinámica
Relacionada con las entidades run-time.
- Procesos, threads, objetos, protocolos, ciclos de vida
- Se-comunica-con, bloquea, contiene, crea, destruye, etc
- Maquinas de estado, diagrama de secuencia y de colaboración, diagrama de objetos, diagrama de componentes
Vista de Deployment
Dónde residen las distintas partes.
- Recursos y repositorios además de entidades dinámicas o estáticas
- Procesos ejecuta-sobre server, código de módulos almacenado en directorio, equipo de trabajo desarrolla paquete, etc
- Diagrama de despliegue
Principios de Diseño
Descomposición
Divide and conquer, se parte cada parte del problema en subproblemas o componentes más pequeños siguiendo una estrategia adecuada (como ser pasos de ejecución, datos, tiempos, funcionalidades, etc), determinar las relaciones entre dichos componentes e iterar. Es importante tener una estrategia de composición, no sólo de división.
Abstracción
Se basa en suprimir detalles de la implementación y posponer decisiones de diseño que ocurren a distintos niveles del análisis. La abstracción puede ser procedural (funciones, métodos, etc), de datos (TADs) o de control (loops, iteradores, multitasking).
Encapsulamiento
También denominado information hiding. Se busca minimizar la información en la interfaz y esconder las decisiones de diseño factibles de cambio para minimizar el impacto a futuro (menor acoplamiento), exponiendo lo mínimo indispensable. Para esto se programa orientado a interfaces.
Se busca abstraer:
- Representación de datos
- Algoritmos
- Formatos de entrada y salida
- Interfaces de bajo nivel
- Separación de políticas y mecanismos
- Decisiones estructurales de más bajo nivel
- Aspectos funcionales
Dependency Inversion Principle: Las dependencias se hacen hacia interfaces o clases abstractas, no hacia las implementaciones concretas.
Control Inversion Principle: Uso de frameworks, implementaciones parciales que el usuario debe rellenar para lograr la funcionalidad deseada; la diferencia principal con las librerías es que es el framework el que invoca el código del usuario y no al revés.
Modularización
Un módulo es una entidad estática que agrupa ciertas funcionalidades (superior a una clase). Tiene una interfaz bien separada de su implementación, garantiza alta cohesión y bajo acoplamiento.
Indicios de una buena modularización es tener una jerarquía de módulos lo suficientemente independientes, con responsabilidades claras y delimitadas, donde un cambio en uno impacta lo menos posible en el resto del sistema.
Cohesión
Es el grado de foco / cuán bien trabajan juntos / coherencia / unión que tienen los distintos elementos de un módulo. Alta cohesión provee robustez, confiabilidad, reusabilidad, comprensibilidad, testeabilidad y mantenibilidad.
Cohesion is a measure of how strongly-related and focused the various responsibilities of a software module are. Cohesion is an ordinal type of measurement and is usually expressed as "high cohesion" or "low cohesion" when being discussed. Modules with high cohesion tend to be preferable because high cohesion is associated with several desirable traits of software including robustness, reliability, reusability, and understandability whereas low cohesion is associated with undesirable traits such as being difficult to maintain, difficult to test, difficult to reuse, and even difficult to understand (from wikipedia).
Pueden definirse distintos tipos de cohesión, de peor a mejor, considerándose aceptables sólo los tres últimos:
- Coincidental: Ej. mis funciones de uso frequente, utils.lib
- Lógico: Existe una categoría lógica que agrupa elementos aunque hagan cosas muy distintas (ej. todas las rutinas de I/O)
- Temporal: Agrupadas por el momento en que se ejecutaran (ej. funciones que atajan un error de output, crean un error en un log y notifican al usuario)
- Procedural: Agrupadas por pertenecer a una misma sequencia de ejecución o política (ej. funciones que chequean permisos y abren archivos)
- Comunicacional: Agrupadas por operar sobre los mismo datos (ej. objetos, operaciones sobre clientes)
- Secuencial: Agrupadas porque el output de uno es el input de otro
- Funcional: Agrupadas porque contribuyen a una tarea bien definida del módulo
Single Responsibility Principle: A class should have only one reason to change; busca obtener un alto grado de cohesión, una clase debe tener una y solo una responsabilidad.
Acoplamiento
Grado de dependencia del módulo sobre otros módulos y en particular las decisiones de diseño que estos hacen, generalmente proporcional al grado de cohesión. Un alto acoplamiento conlleva una alta propagación de cambios y necesidades de testing, dificulta la comprensión de los módulos de forma aislada y trae problemas al reuso y retesteo.
Coupling or dependency is the degree to which each program module relies on each one of the other modules (from wikipedia).
Los tipos de acoplamiento son, de mayor a menor:
- Contenido: Cuando un módulo modifica o confía en el lo interno de otro (ej. acceso a datos locales o privados)
- Común: Cuando comparten datos comunes (ej. una variable global)
- Externo: Cuando comparten aspectos impuestos externamente al diseño (ej. formato de datos, protocolo de comunicación, interfaz de dispositivo)
- Control: Cuando un módulo controla la lógica del otro (ej. pasándole un flag de comportamiento)
- Estampillado (Stamp): Cuando comparten una estructura de datos pero cada uno usa sólo una porción (ej paso de todo un registro cuando el módulo sólo necesita una parte)
- Datos: Módulos se comunican a través de datos en parámetros (ej. llamado de funciones de otro módulo)
- Mensajes: Módulos se comunican a través de mensajes, posiblemente no se conocen explícitamente
Interface Seggregation Principle: Many client specific interfaces are better than one general purpose interface, busca separar interfaces para minimizar dependencias y reducir el fanning.
Extensibilidad
El diseño debe ser capaz de acomodarse a los cambios de requerimientos sin sufrir modificaciones, siendo extendido con facilidad.
Open/closed Principle: Software entities should be open for extension but closed for modification. Suele implementarse mediante polimorfismo e interfaces.
Liskov Substitution Principle: Subclasses should be substitutable for their base classes.
Ley de Demeter: No hablar con extraños, se basa en que un método de un objeto sólo puede llamar métodos del propio objeto, sus parámetros o aquellos objetos que constituyen el objeto de manera directa o fueron creados por él. Se evita llamar métodos de objetos remotos retornados por otros métodos. Facilita la mantenibilidad y adaptabilidad pero tiende a generar wrappers molestos y poco cohesivos.
Ejemplos de Diseño
Los siguientes son ejemplos o buenas prácticas de diseño para llevar a cabo.
Diseño en capas
Los módulos se organizan en capas, con alta cohesión y acoplamiento dentro de cada capa, pero bajo acoplamiento entre capas. Las dependencias directas se dan solamente entre capas contiguas. Las capas pueden ser físicas (ej middleware) o lógicas (modelo 3 tier de data, bussiness y presentation layer).
Máquinas Virtuales
Se tiene un ambiente virtual por sobre la plataforma específica de hardware/software que provee mayor funcionalidad, simpleza, garantías y portabilidad; además de abstraer de los cambios de plataforma. Un buen ejemplo es el modelo OSI.
Jerarquía
El diseño en jerarquías agrupa a los módulos por niveles, restringiendo la topología del grafo determinado por la relación elegida. Esto facilita el desarrollo concurrente, testing, prototipación, comprensión, análisis modular y aíslan el impacto de cambios.
Las jerarquías pueden efectuarse sobre distintos tipos de relaciones, como ser: Utiliza-a, Depende-de, Está-compuesto-por, Tiene-un, Es-un.
Una de las más utilizadas es Depende-De (no confundir con utiliza, puede haber invocaciones que no impliquen dependencias, como logging de errores, o dependencias que no utilicen invocaciones como medio de contacto, sino acceso a memoria compartida o pasaje de mensajes). Puede que no genere una jerarquía (ej callbacks). Se permite a X depender de Y cuando:
- X es más simple porque depende de Y.
- Y no es substancialmente mas complejo porque X depende de Y
- Existe una porción no trivial de la jerarquía que contiene a Y y no a X. (i.e. reuso)
- No existe una porción no trivial de la jerarquía que contiene a X y no a Y. (i.e. Y es necesario)
Consejos
Consejos varios para lograr un buen diseño.
Errores comunes
- Diseño Depth First
- Sólo satisface algunos requerimientos
- Refinamiento directo de la especificación de requerimientos
- Puede llevar a un diseño ineficiente
- Olvidarse de pensar en cambios a futuro
- Diseñar para extensión (y contracción!)
- Diseñar demasiado en detalle
- Introduce demasiadas restricciones a implementación
- Es muy caro, no vale la pena
- Diseñar exclusivamente top-down
- Primero los requerimientos críticos!
- No todo lo vamos a construir. Selección de COTS influye en la descomposición
- Diseño documentado ambiguamente
- Interpretado incorrectamente en tiempo de implementación
- Decisiones de diseño indocumentadas
- Diseñadores son necesarios durante la implementación
- Decisiones de diseño sin justificación documentada
- Cambios mas adelante, aparentemente inofensivos, rompen el diseño
- Diseño inconsistente
- Módulos funcionan, pero no encajan
- Divide to conquer, reunite to rule
Reglas
- Asegurarse que el problema está claro
- En la medida de lo razonable, requerimientos claros, consistentes y completos
- Qué antes que cómo
- En la medida de lo posible, pensar en interfases primero
- Separar aspectos ortogonales
- No conectar lo que es independiente.
- Mantener las cosas lo más simple posible, pero no más.
- Diseños chetos tienden a tener mas errores, ser más difíciles de testear y ser mas ineficientes
- Trabajar a múltiples niveles de abstracción
- ...pero entender la relación entre estos niveles
- Pensar en la posiblidad de prototipar
- Vertical vs. Horizontal, para evolucionar vs. para tirar
- Mantener las representaciones consistentes
- Utilizar, en la medida de lo posible, patrones y esquemas conocidos
- No reinventar la rueda
- Ser auto-crítico
- Usar principios de diseño para evaluar el diseño
- No asumir que las reglas aqui expuestas son absolutas o completas