Está en la página 1de 24

MÓDUL0 3: PRINCIPIOS DE DISEÑO

Contenido
Evaluación de la complejidad del diseño 2

Acomplamiento 3

Cohesión 3

Separación de intereses o conceptos 4

Ocultación de información 9

Integridad Conceptual 12

Principios de generalización 14

Diagramas de clases UML especializados 16

Diagramas de secuencia UML 17

Diagramas de estado UML 20

Comprobación de modelo 22
Al finalizar este módulo, podrá:

(a) Comprender las pautas generales para evaluar la estructura de su


solución de software para que sea flexible, reutilizable y
mantenible. Estas pautas son:
a. evaluación de la complejidad del diseño con
acoplamiento y cohesión
b. la separación de preocupaciones
c. ocultación de información
d. integridad conceptual
e. principios de generalización

(b) Modele los comportamientos de los objetos en su software


utilizando el estado UML especializado y los diagramas de
secuencia UML.
(c) Explicar la importancia de la verificación del modelo.

Este módulo cubre las pautas generales para evaluar la estructura de las
soluciones de software. Estas pautas ayudan a garantizar que el software sea
flexible, reutilizable y mantenible. También cubrirá los comportamientos de
modelado de los objetos en su software utilizando el estado UML y los
diagramas de secuencia UML.

Evaluación de la complejidad del diseño


Es importante mantener los módulos simples cuando esté programando. Si la
complejidad de su diseño excede lo que los desarrolladores pueden manejar
mentalmente, los errores ocurrirán con más frecuencia. Para ayudar a controlar
esto, debe haber una forma de evaluar la complejidad de su diseño.

La complejidad del diseño se aplica tanto a las clases como a los métodos
dentro de ellas. Esta sección usará el término módulo para referirse a unidades
de programa que contienen clases y los métodos dentro de ellas.

Un sistema es una combinación de varios módulos. Si el sistema tiene un mal


diseño, los módulos solo pueden conectarse a otros módulos específicos y nada
más. Un buen diseño permite que cualquier módulo se conecte sin muchos
problemas. En otras palabras, en un buen diseño, los módulos son compatibles
entre sí y, por lo tanto, se pueden conectar y reutilizar fácilmente.
Las métricas que se utilizan a menudo para evaluar la complejidad del diseño
son el acoplamiento y la cohesión.

Acomplamiento

El acoplamiento se centra en la complejidad entre un módulo y otros módulos.


El acoplamiento se puede equilibrar entre dos extremos: acoplamiento
apretado y acoplamiento flojo. Si un módulo depende demasiado de otros
módulos, entonces está “estrechamente acoplado” a los demás. Este es un mal
diseño. Sin embargo, si a un módulo le resulta fácil conectarse a otros módulos
a través de interfaces bien definidas, está “ligeramente acoplado” a los demás.
Este es un buen diseño.

Para evaluar el acoplamiento de un módulo, las métricas a considerar son:


grado, facilidad y flexibilidad.

El grado es el número de conexiones entre el módulo y los demás. El grado debe


ser pequeño para el acoplamiento. Por ejemplo, un módulo debe conectarse a
otros a través de solo unos pocos parámetros o interfaces estrechas. Esto sería
un grado pequeño y el acoplamiento estaría flojo.

La facilidad es cuán obvias son las conexiones entre el módulo y los demás. Las
conexiones deben ser fáciles de realizar sin necesidad de comprender las
implementaciones de otros módulos, con fines de acoplamiento.

La flexibilidad indica cuán intercambiables son los otros módulos para este
módulo. Otros módulos deberían ser fácilmente reemplazables por algo mejor
en el futuro, con fines de acoplamiento.

Las señales de que un sistema está estrechamente acoplado y tiene un mal


diseño son:
• un módulo se conecta a otros módulos a través de una gran cantidad
de parámetros o interfaces
• módulos correspondientes a un módulo son difíciles de encontrar
• un módulo solo se puede conectar a otros módulos específicos y no se
puede intercambiar

Cohesión
La cohesión se centra en la complejidad dentro de un módulo y representa la
claridad de las responsabilidades de un módulo. Al igual que la complejidad, la
cohesión puede funcionar entre dos extremos: alta cohesión y baja cohesión.
Un módulo que realiza una tarea y nada más, o que tiene un propósito claro,
tiene una alta cohesión. Un buen diseño tiene una alta cohesión. Por otro lado,
si un módulo encapsula más de un propósito, si se debe romper un
encapsulamiento para comprender un método, o si el módulo tiene un
propósito poco claro, tiene baja cohesión. Un mal diseño tiene baja cohesión.
Si un módulo tiene más de una responsabilidad, es una buena idea dividir el
módulo.

Es importante equilibrar el bajo acoplamiento y la alta cohesión en el diseño


del sistema. Ambos son necesarios para un buen diseño. Sin embargo, en
sistemas complejos, la complejidad se puede distribuir entre los módulos o
dentro de los módulos. Por ejemplo, a medida que los módulos se simplifican
para lograr una alta cohesión, es posible que deban depender más de otros
módulos, lo que aumenta el acoplamiento. Por otro lado, a medida que las
conexiones entre módulos se simplifican para lograr un acoplamiento bajo, es
posible que los módulos deban asumir más responsabilidades, lo que reduce la
cohesión.

Separación de intereses o conceptos


Uno de los principios de diseño examinados en el módulo anterior fue el de la
descomposición. La descomposición divide un todo en diferentes partes. Para
comprender por qué es necesaria la descomposición en el diseño, se debe
examinar el principio de separación de intereses o conceptos.

Un interés o concepto es una noción muy general: es todo lo que importa para
proporcionar una solución a un problema. La separación de conceptos se trata
de mantener separadas los diferentes conceptos en su diseño. Cuando se
diseña software, se deben abordar diferentes conceptos en diferentes partes
del software.

Considere un sistema de software que resuelve un problema. Ese problema


puede ser simple, con una pequeña cantidad de subproblemas, o complejo, con
una gran cantidad de subproblemas. Los conceptos se pueden abstraer del
espacio del problema. Cuando estas abstracciones se implementan en el
software, pueden generar más preocupaciones. Por ejemplo, algunas de estos
conceptos podrían involucrar qué información representa la implementación,
qué manipula y qué se presenta al final. Para no perderse en las inquietudes y
subproblemas resultantes, el diseño debe organizarse de modo que todas las
inquietudes se consideren y aborden cuidadosamente. Para hacer esto, los
diferentes subproblemas y preocupaciones se separan en diferentes secciones
durante el diseño y la construcción del sistema de software. Esto aplica el
principio de separación de intereses.

La separación de intereses proporciona muchas ventajas. Le permiten


desarrollar y actualizar secciones del software de forma independiente. El uso
de la separación de intereses también significa que no necesita saber cómo
funcionan todas las secciones del código para actualizar una sección.
Finalmente, la separación de intereses permite realizar cambios en un
componente sin requerir un cambio en otro.

La separación de intereses es una idea clave que subyace en el modelado y la


programación orientados a objetos. Al abordar los intereses por separado, se
crean clases más cohesivas y se aplican los principios de diseño de abstracción,
encapsulación, descomposición y generalización:
• La abstracción ocurre cuando cada concepto en el espacio del
problema se separa con sus propios atributos y comportamientos
relevantes.
• La encapsulación se produce cuando los atributos y los
comportamientos se reúnen en su propia sección de código
denominada clase. El acceso a la clase desde el resto del sistema y su
implementación están separados, por lo que los detalles de
implementación pueden cambiar mientras que la vista a través de una
interfaz puede permanecer igual.
• La descomposición se produce cuando una clase completa se
puede separar en varias clases.
• La generalización ocurre a medida que se reconocen los puntos
en común y, posteriormente, se separan y generalizan en una
superclase.
La separación de intereses es un proceso continuo a lo largo del proceso de
diseño del sistema. Debido a la relación de la separación de intereses con los
principios de diseño, el uso de este concepto en el diseño de software crea un
sistema que es más fácil de mantener porque cada clase está organizada de
modo que la clase solo contiene el código que necesita para hacer su trabajo.
A su vez, se aumenta la modularidad, lo que permite a los desarrolladores
reutilizar y crear clases individuales sin afectar a otras.

Es importante señalar que los límites de cada clase no siempre serán evidentes
en la práctica. Decidir cómo abstraer, encapsular, descomponer y generalizar
para abordar las muchas inquietudes de un problema dado es el núcleo del
diseño de software modular.

Ejemplo de separación de intereses


Este ejemplo ilustrará la separación de intereses. Considere un teléfono
inteligente. Los teléfonos inteligentes son capaces de muchos
comportamientos: tomar fotos, programar reuniones, enviar y recibir correo
electrónico, navegar por Internet, enviar mensajes de texto y hacer llamadas
telefónicas. Este ejemplo solo se centrará en dos funciones, por simplicidad: el
uso de una cámara y las funciones tradicionales del teléfono.

public class SmartPhone {


private byte camera;
private byte phone;

public SmartPhone() { … }

public void takePhoto() { … }


public void savePhoto() { … }
public void cameraFlash() { … }
public void makePhoneCall() { … }
public void encryptOutgoingSound() { … }
public void decipherIncomingSound() { … }
}

Este código tiene una clase SmartPhone con atributos llamados camera y
phone, y comportamientos asociados. Este sistema tiene una baja cohesión, ya
que hay comportamientos que no están relacionados entre sí. No es necesario
encapsular los comportamientos de la cámara con los comportamientos del
teléfono para que la cámara haga su trabajo. Los componentes tampoco
ofrecen modularidad. Por ejemplo, no es posible acceder a la cámara o al
teléfono por separado si se construye otro sistema que requiere solo uno u
otro. La cámara tampoco podría reemplazarse con una cámara diferente, o
incluso con un objeto diferente, sin eliminar el código de la cámara por
completo de esta clase.

La clase SmartPhone necesita ser más cohesiva y cada componente del teléfono
inteligente debe tener una funcionalidad distintiva. Usando la separación de
preocupaciones, podemos identificar que la clase SmartPhone tiene dos
preocupaciones:
1. Actuar como un teléfono tradicional.
2. Para tomar fotos usando la cámara incorporada.
Con las inquietudes identificadas, es posible separarlas en sus propias clases
más cohesivas y encapsular todos los detalles sobre cada inquietud en clases
funcionalmente distintas e independientes.

La clase SmartPhone hará referencia a instancias de las nuevas clases para que
el teléfono inteligente pueda actuar como coordinador de la cámara y el
teléfono. Esto permitirá que nuestro smartphone proporcione acceso a todos
los comportamientos de la cámara y del teléfono, sin tener que saber cómo se
comporta cada componente.

Usando un diagrama de clase UML, así es como se vería el nuevo diseño para el
sistema SmartPhone:

Los atributos y comportamientos del teléfono y la cámara se han separado en


dos interfaces distintas. Cada uno de ellos se implementa con una clase
correspondiente.

El código se traduciría de la siguiente manera:


public interface ICamera {
public void takePhoto();
public void savePhoto();
public void cameraFlash();
}
public interface IPhone {
public void makePhoneCall();
public void encryptOutgoingSound();
public void deciphereIncomingSound();
}
public class FirstGenCamera implements ICamera {
/* Atributos abstraídos de cámara */

public class TraditionalPhone implements IPhone {


/* Atributos abstraídos de teléfono */
}

También será necesario rediseñar el código de la clase SmartPhone para que se


refiera a las dos clases separadas:

public class SmartPhone {


private ICamera myCamera;
private IPhone myPhone;

public SmartPhone( ICamera aCamera, IPhone


aPhone ) {
this.myCamera = aCamera;
this.myPhone = aPhone;
}

public void useCamera() {


return this.myCamera.takePhoto();
}

public void usePhone() {


return this.myPhone.makePhoneCall();
}
}

Con este rediseño, la clase SmartPhone brinda las funciones tanto de la cámara
como del teléfono. Sin embargo, las clases de cámara y teléfono están
separadas, por lo que sus funcionalidades están ocultas entre sí, pero aún se
agregan en la clase SmartPhone. También hay un constructor de SmartPhone
con una cámara y un teléfono como parámetros. Es posible crear una nueva
instancia de la clase SmartPhone pasando instancias de clases que
implementaron las interfaces ICamera e IPhone. Quién crea los objetos
apropiados de teléfono y cámara se deja como una responsabilidad separada,
ya que la clase SmartPhone en realidad no necesita saber esto. Finalmente, la
clase SmartPhone tiene métodos que reenvían las responsabilidades de usar la
cámara y el teléfono a estos objetos.

Esto crea un diseño modular: si el diseño requiere que las clases de cámara o
teléfono se cambien por otra cosa, entonces no es necesario tocar el código de
la clase SmartPhone. El código simplemente se cambia para instanciar el
SmartPhone y sus partes.

La clase SmartPhone ahora es más cohesiva, pero tiene un mayor acoplamiento


ya que la clase SmartPhone, necesita conocer las interfaces de la cámara y el
teléfono y depende indirectamente de otras clases.

En este ejemplo, la separación de preocupaciones fue utilizada por:

• Separar las nociones de cámara y teléfono a través de la generalización


y definir las dos interfaces.
• Separar la funcionalidad de una cámara de primera generación y un
teléfono tradicional aplicando abstracción y encapsulación, y
definiendo dos clases de implementación.
• Aplicar descomposición al smartphone para separar las partes
constituyentes del todo.

Ocultación de información
Un sistema bien diseñado está bien organizado, lo que se logra con la ayuda de
una serie de principios de diseño. Esta lección investigará más a fondo el
concepto de acceso a la información. No todos los componentes de un sistema
de software necesitan saber todo lo demás. Los módulos solo deben tener
acceso a la información que necesitan para hacer su trabajo. Limitar la
información a los módulos para que solo se necesite la cantidad mínima de
información para usarlos correctamente y "ocultar" todo lo demás se hace
mediante la ocultación de información.

La ocultación de información se asocia comúnmente con datos confidenciales:


cuanto más confidenciales son los datos, es más probable que tengan un acceso
limitado. En el diseño de software, la ocultación de información también se
utiliza específicamente para ocultar detalles modificables, como algoritmos o
representaciones de datos. Las suposiciones, por otro lado, no están ocultas y
normalmente se expresan en API e interfaces.
La ocultación de información permite a los desarrolladores trabajar en módulos
por separado, sin necesidad de que otros desarrolladores conozcan los detalles
de implementación del módulo en el que están trabajando. En cambio, el
módulo se utiliza a través de su interfaz.

Por lo tanto, una buena regla oro, es que las cosas que pueden cambiar, como
los detalles de implementación, deben ocultarse, y las cosas que no cambian,
como las suposiciones, se revelan a través de las interfaces.

La ocultación de información está estrechamente relacionada con la


encapsulación. La encapsulación agrupa atributos y comportamientos en su
clase apropiada, pero también se ocupa de brindar acceso a los módulos a
través de interfaces y restringir el acceso a ciertos comportamientos o
funciones. Dado que, a través de la encapsulación, la implementación de los
comportamientos se oculta detrás de una interfaz, que es la única forma de
acceder a métodos específicos, otras clases se basan en la información de estas
firmas de métodos y no en las implementaciones subyacentes. La ocultación de
información a través de la encapsulación permite que la implementación
cambie sin cambiar el resultado esperado. Las expectativas de un
comportamiento pueden cumplirse sin exponer cómo se logra.

Además de ocultar implementación o comportamientos, es posible ocultar


atributos. Esto evita que la información crítica de una clase se cambie
directamente. Por ejemplo, si un atributo es crítico para todos los
comportamientos de una clase, entonces no queremos que ninguna clase
externa lo cambie directamente.

La ocultación de información se puede lograr mediante el uso de


modificadores de acceso. Los modificadores de acceso cambian qué clases
pueden acceder a atributos y comportamientos. También determinan qué
atributos y comportamientos compartirá una superclase con sus subclases. Los
cuatro niveles de acceso en Java son:

• Público
• Protegido
• Por defecto
• Privado

Examinemos cada uno de estos a su vez.


Público
Cualquier clase de su sistema puede acceder a los atributos con un modificador
de acceso público. Esto significa que otras clases pueden recuperar y modificar
el atributo o el cambio. A los métodos también se les puede dar un nivel de
acceso público, por lo que cualquier clase en el sistema puede acceder al
método. Sin embargo, este acceso no permite que otras clases cambien la
implementación del comportamiento del método. Un método de acceso
público simplemente permite que otras clases llamen al método o invoquen el
comportamiento y reciban cualquier resultado de él. Sin embargo, la
implementación permanece oculta a través de la encapsulación.

Protegidos
Los atributos y métodos que están protegidos no son accesibles para todas las
clases del sistema. En cambio, solo están disponibles para la clase encapsulada,
todas las subclases y clases dentro del mismo paquete. Los paquetes son los
medios por los cuales Java organiza las clases relacionadas en un solo espacio
de nombres.

Predeterminado
Un modificador de acceso predeterminado solo permite el acceso a los
atributos y métodos de las subclases o clases que forman parte del mismo
paquete o encapsulación. Este modificador de acceso también se conoce como
acceso sin modificador porque no es necesario declararlo explícitamente en el
código.

Privados
Los atributos y métodos que son privados no son accesibles para ninguna otra
clase que no sea la propia clase encapsuladora. Esto significa que no se puede
acceder a estos atributos directamente y que ninguna otra clase puede invocar
estos métodos.

El siguiente ejemplo en código muestra cómo se pueden indicar los


modificadores de acceso:

public class Person {


String name;
}

En este ejemplo, el modificador de acceso public se ha utilizado para la


clase Person y la variable de miembro de nombre tiene acceso
predeterminado ya que no se indica ningún modificador. Los modificadores de
acceso protected o private también podrían usarse para que el nombre
aplique un acceso diferente.

Integridad Conceptual
La integridad conceptual es un concepto relacionado con la creación de
software consistente. La integridad conceptual implica tomar decisiones sobre
el diseño y la implementación de un sistema de software, por lo que incluso si
varias personas trabajan en él, parecería cohesivo y consistente como si solo
una mente guiara el trabajo. Esto generalmente se logra a través de un acuerdo
para usar ciertos principios y convenciones de diseño para crear el sistema.

Es importante que el concepto de integridad conceptual no se confunda con


ignorar la opinión de los miembros del equipo de desarrollo sobre el software.
Estos pensamientos siguen siendo importantes y deben ser discutidos
abiertamente. Sin embargo, cualquier idea debe cumplir con los principios y
convenciones acordados.

Existen múltiples formas de lograr la integridad conceptual. Éstos incluyen:


• comunicación
• revisiones de código
• usando ciertos principios de diseño y construcciones de programación
• tener un diseño bien definido o una arquitectura subyacente al
software
• conceptos unificadores
• tener un pequeño grupo central que acepte cada compromiso con el
código base.

La comunicación efectiva mantiene la integridad conceptual y permite que los


miembros del equipo discutan y acuerden usar ciertas bibliotecas o métodos al
abordar ciertos problemas. Esto, a su vez, ayuda a crear un código más
coherente. Algunas buenas prácticas para fomentar la comunicación incluyen
prácticas de desarrollo ágil como reuniones diarias y retrospectivas de sprint.

Las revisiones de código son exámenes sistemáticos del código escrito. Estos
son similares a la revisión por pares en la escritura académica. Los
desarrolladores revisan el código, línea por línea y descubren problemas en el
código de los demás. Esto ayuda a identificar errores en el software, pero
también ayuda a crear coherencia entre el código de diferentes
desarrolladores.
El uso de ciertos principios de diseño y construcciones de programación ayuda
a mantener la integridad conceptual. En particular, las interfaces de Java son
una construcción que puede lograr esto. Una interfaz define un tipo con un
conjunto de comportamientos esperados. La implementación de clases de esa
interfaz tendrá estos comportamientos en común. Esto crea coherencia en el
software y aumenta la integridad conceptual.

Tener un software subyacente bien definido de diseño o arquitectura ayuda a


crear integridad conceptual. Si bien el diseño de software generalmente se
asocia con guiar el diseño interno del software que se ejecuta como un solo
proceso, la arquitectura del software describe cómo el software que se ejecuta
como múltiples procesos funciona en conjunto y cómo se relacionan entre sí.
Esto ayuda a crear consistencia.

La unificación de conceptos en su software también aumenta la integridad


conceptual. Esto implica tomar diferentes conceptos y encontrar algo en
común, de modo que cada concepto pueda verse y tratarse de manera similar.
Por ejemplo, en los sistemas operativos Unix, cada recurso se puede ver y
manipular como si fuera un archivo. El mismo conjunto de operaciones se
puede usar en diferentes tipos de recursos, lo que simplifica el sistema para
que cualquier recurso se pueda tratar de la misma manera, lo que evita casos
especiales. Esto ayuda a crear consistencia.

Otro medio de aumentar la integridad conceptual es tener un pequeño grupo


central que acepte cada compromiso con la base del código. Esto es similar a
realizar revisiones de código, pero restringe la revisión solo a los miembros
principales del equipo de software. Estos miembros son responsables de
garantizar que cualquier cambio de software siga la arquitectura y el diseño
generales del software. Tener solo un individuo o un pequeño grupo a cargo de
esto ayuda a resolver problemas de diseño y crea consistencia.
La integridad conceptual a menudo se considera la consideración más
importante en el diseño del sistema.

¿SABÍAS?

El conocido arquitecto informático Fred Brooks analiza la integridad


conceptual en su libro The Mythical Man-Month. En este libro, afirma,

Es mejor tener un sistema que omita ciertas características y


mejoras anómalas, pero que refleje un conjunto de ideas de
diseño, que tener uno que contenga muchas ideas buenas
pero independientes y descoordinadas”.
Practicar la integridad conceptual ayuda a guiar al equipo de desarrollo de
software al hacer que el diseño y la lógica del software sean consistentes y
fáciles de seguir para cualquier miembro del equipo. Esto ayuda a los miembros
del equipo a saber cómo y dónde cambiar el software para cumplir con los
nuevos requisitos, lo que facilita el mantenimiento del software. La integridad
conceptual puede servir como estructura y marco para cualquier proyecto de
software al evitar el código informal y sin guía, lo que podría conducir a un
trabajo confuso y desorganizado.

Principios de generalización
En el módulo anterior se revisó los cuatro principios de diseño de abstracción,
encapsulación, descomposición y generalización. Estos principios ayudan a
guiar las elecciones que se deben hacer al diseñar un sistema orientado a
objetos.

Sin embargo, algunas decisiones de diseño son más fáciles de tomar que otras.
La generalización y la herencia son algunos de los temas más difíciles de
dominar en la programación y el modelado orientados a objetos.

La herencia es una poderosa herramienta de diseño que puede ayudar a crear


sistemas de software limpios, reutilizables y fáciles de mantener. Sin embargo,
su mal uso puede conducir a un código deficiente. Si los principios de diseño se
usan incorrectamente, pueden crear más problemas de los que resuelven.

Para identificar si se está haciendo un mal uso de la herencia, es una buena


práctica tener en cuenta un par de principios de generalización.

Un principio se puede formular como una pregunta para preguntarse si una


subclase debería existir: “¿Estoy usando la herencia para simplemente
compartir atributos o comportamiento sin agregar nada especial en mis
subclases?” Si la respuesta a esta pregunta es “sí”, entonces se está haciendo
un mal uso de la herencia, ya que no tiene sentido que existan las subclases. La
superclase ya debería ser suficiente.

Por ejemplo, un empleado es un tipo general para gerentes, vendedores y


cajeros, pero cada uno de esos subtipos de empleados realiza funciones
específicas. La herencia tiene sentido en este caso. Sin embargo, si está creando
diferentes tipos de pizza, no existe una verdadera especialización entre los
diferentes tipos de pizza, por lo que las subclases son innecesarias.
Otra técnica es determinar si se rompe el principio de sustitución de Liskov. El
principio de sustitución de Liskov establece que una subclase puede reemplazar
a una superclase, si y solo si, la subclase no cambia la funcionalidad de la
superclase. Esto significa que, si una subclase reemplaza a una superclase, pero
reemplaza todos los comportamientos de la superclase con algo totalmente
diferente, entonces se está haciendo un mal uso de la herencia.

Por ejemplo, si una clase de ballena que muestra un comportamiento de


natación se sustituye por una clase de animal, entonces se anularán funciones
como correr y caminar. La Ballena ya no se comporta de la forma en que
esperaríamos que se comportara su superclase, violando el principio de
sustitución de Liskov.

En Java, hay una biblioteca con una clase Stack, que es un ejemplo de mala
herencia. Una pila se entiende como una estructura de datos de tipo primero
en entrar, último en salir, con una pequeña cantidad de comportamientos bien
definidos como mirar, abrir y empujar. Pero, la clase Java Stack hereda de una
superclase Vector. Esto significa que la clase Stack puede devolver un elemento
en un índice específico, recuperar el índice de un elemento e incluso insertar
un elemento en un índice específico. Estos no son comportamientos que
normalmente se esperan de una pila.

En los casos en que la herencia no sea adecuada, la descomposición puede ser


la solución. Por ejemplo, un teléfono inteligente es más adecuado para la
descomposición que para la herencia. Recuerde nuestro ejemplo de la sección
sobre separación de preocupaciones, donde un teléfono inteligente podría
tener las dos funciones de un teléfono tradicional y como una cámara.
En este ejemplo, no tiene sentido usar la herencia de un teléfono tradicional a
un teléfono inteligente y luego agregar métodos de cámara a la subclase de
teléfono inteligente.

En cambio, la descomposición ayuda a extraer las responsabilidades de la


cámara en su propia clase. Esto permite que la clase SmartPhone proporcione
las responsabilidades de la cámara y el teléfono a través de clases separadas.
La clase SmartPhone no necesita saber cómo funcionan estas clases.

Aunque la herencia es un principio poderoso, es importante saber cuándo usar


correctamente una técnica o arriesgarse a introducir más problemas en un
sistema de software.

Diagramas de clases UML especializados


Ya hemos revisado el uso de diagramas de clases UML para expresar el diseño
técnico. Sin embargo, existen muchos tipos diferentes de diagramas de clases
UML. A continuación, la siguiente lección expone dos versiones especializadas
de diagramas de clases y cómo se pueden utilizar para mejorar su diseño
técnico.
Diagramas de secuencia UML

Los diagramas de secuencia UML son otra técnica importante en el diseño de


software. Son un tipo de diagrama UML, comúnmente utilizados como
herramienta de planificación antes de que el equipo de desarrollo comience a
programar. Los diagramas de secuencia se utilizan para mostrar a un equipo de
diseño cómo los objetos de un programa interactúan entre sí para completar
tareas. En términos simples, un diagrama de secuencia es como un mapa de
conversaciones entre diferentes personas, con los mensajes enviados de
persona a persona delineados.

Los diagramas de secuencia pueden ayudarlo a visualizar las clases que creará
en su software y determinar las funciones que deberán escribirse. También
puede ilustrar problemas en su sistema que antes eran desconocidos.

Examinemos cada componente de un diagrama de secuencia.

• Las cajas se utilizan para representar un papel desempeñado por un


objeto. El rol suele tener el nombre de la clase del objeto.
• Las "líneas de vida", que son líneas de puntos verticales, se utilizan en
el diagrama para indicar un objeto a medida que pasa el tiempo.
• Las flechas de línea continua se utilizan para mostrar mensajes que se
envían de un objeto a otro, o de un remitente a un receptor. Los
receptores están en el extremo puntiagudo de una flecha. Por lo
general, se incluye una breve descripción del mensaje encima de la
flecha.
• Las flechas de línea punteada se utilizan para mostrar el retorno de los
datos y el control de regreso a los objetos de inicio. Por lo general, se
incluye una breve descripción de la devolución de datos o control
encima de la flecha.
• Los pequeños rectángulos a lo largo de la línea de vida de un objeto
indican la activación de un método. Activa un objeto cada vez que un
objeto envía, recibe o está esperando un mensaje.
• Las personas o los actores también pueden incluirse en los diagramas
de secuencia si usan o interactúan con objetos. Los actores suelen estar
representados con figuras de palitos.
Los diagramas de secuencia normalmente se enmarcan dentro de un cuadro
grande. Los títulos del diagrama se indican en las esquinas superiores
izquierdas. Es una buena práctica proporcionar un título significativo, ya que se
hará referencia al diagrama para su desarrollo. Otra buena práctica es dibujar
objetos de izquierda a derecha en la secuencia a medida que interactúan entre
sí.

A continuación, se muestra un ejemplo de diagrama de secuencia para cambiar


el canal de su televisor (change TV channel) con un control remoto, con todos
los elementos descritos anteriormente.
Un diagrama de secuencia puede contener otros diagramas de secuencia
dentro de ellos. Por ejemplo, si está creando un diagrama de secuencia para
un cajero automático, podría haber una secuencia diferente para retiros y
depósitos; y durante un solo proceso, alguien podría querer retirar y depositar
dinero. En su diagrama de secuencia, tendría una gran secuencia de
actividades, con dos secuencias más pequeñas dentro de ellas.

A medida que diseña software, sus diagramas de secuencia pueden volverse


mucho más complicados. Los bucles y los procesos alternativos también se
pueden demostrar en un diagrama de secuencia. Un proceso alternativo es una
secuencia de acciones que ocurrirán si una condición es verdadera. Una
secuencia alternativa se puede colocar en un cuadro y etiquetar como “alt”
para alternativa en la esquina superior derecha.

Basémonos en el ejemplo anterior para ilustrar esto. Imagine un escenario en


el que el espectador de televisión no está seguro de a qué canal ir y le gustaría
navegar por los canales hasta que elija uno que le guste. Este escenario se
puede ilustrar bajo la secuencia anterior con la condición “[else]”. Esto indica
que este escenario ocurre solo si todas las otras alternativas son falsas.

Este escenario también contiene un bucle. Esto se puede ilustrar agregando un


cuadro etiquetado como “bucle”. Debajo de la etiqueta, debe escribirse la
declaración condicional para el ciclo. Si esa declaración es verdadera, entonces
el sistema pasará por el bucle. En este ejemplo, la secuencia de bucle debe
ocurrir continuamente si el televidente no está satisfecho con el canal que está
viendo.

Dentro del bucle, el televidente presiona la flecha hacia arriba o hacia abajo en
el control remoto para cambiar de canal. Esto envía un mensaje al control
remoto. Luego, el control remoto envía un mensaje al televisor con esta acción.
El televisor cambia el canal y se lo muestra al espectador. La secuencia final se
verá de la siguiente manera:
Los diagramas de secuencia son una técnica útil para ayudar a crear programas
limpios y bien diseñados.

Diagramas de estado UML

Los diagramas de estado UML son otro tipo de diagrama UML. Son una técnica
utilizada para describir cómo se comportan y responden los sistemas. Siguen
los estados de un sistema o de un solo objeto y muestran cambios entre los
estados a medida que ocurren una serie de eventos en el sistema.

Un diagrama de estado ilustra el comportamiento de un objeto al representar


los estados cambiantes de un objeto. Estos cambian en respuesta a diferentes
eventos. Un estado es la forma en que existe un objeto en un momento
determinado. El estado de un objeto está determinado por los valores de sus
atributos.

Una buena metáfora de los estados es la de un automóvil. Un automóvil con


transmisión automática puede tener diferentes estados: estacionamiento,
marcha atrás, punto muerto y conducción. Si un automóvil está en reversa,
solo puede comportarse moviéndose hacia atrás. Si desea avanzar, debe
cambiar el estado del automóvil para conducir. Esto es similar a los estados de
los objetos en un sistema de software. Cuando un objeto está en cierto estado,
se comporta de una manera específica o tiene atributos establecidos en valores
específicos. Examinemos los diferentes elementos de los diagramas de estado:

• Un círculo relleno indica el estado inicial del objeto. Cada diagrama de


estado comienza con un círculo relleno.

• Los rectángulos redondeados indican otros estados. Estos rectángulos


tienen tres secciones: un nombre de estado, variables de estado y
actividades.

o Los nombres de estado deben ser títulos breves y significativos


para el estado del objeto. Cada estado debe tener al menos un
nombre de estado.
o Las variables de estado son datos relevantes para el estado del
objeto.
o Las actividades son acciones que se realizan cuando se está en
un estado determinado. Hay tres tipos de actividades
presentes para cada estado: actividades de entrada, salida y
realización. Las actividades de entrada (entry/activity) son
acciones que ocurren cuando se ingresa al estado desde otro
estado. Las actividades de salida (exit / activity) son acciones
que ocurren cuando se sale del estado y se pasa a otro estado.
Las actividades hacer (do /activity) son acciones que ocurren
mientras el objeto se encuentra en un estado determinado.
• Las flechas indican transiciones de un estado a otro. Las transiciones
suelen desencadenarse por un evento. Este evento generalmente se
describe arriba de la flecha.

evento [condición] /acción

Cada flecha de transición siempre tendrá un evento, e incluso puede


tener una condición de guardia y una acción. La transición y la acción
suceden a partir de un estado dado si el evento ocurre y la condición
es verdadera.
• Un círculo con un círculo relleno dentro indica terminación. La
terminación representa la destrucción de un objeto o la finalización del
proceso. No todos los diagramas tienen una terminación; algunos
pueden ejecutarse continuamente.

Los diagramas de estado pueden ser útiles para ayudar a determinar los
eventos que pueden ocurrir durante la vida útil de un objeto, como las
diferentes entradas del usuario, y cómo debe comportarse ese objeto cuando
ocurren estos eventos, como verificar condiciones y realizar acciones. A veces
puede ser más fácil ver los cambios de estado en un diagrama, en lugar de leer
el código fuente.

Los diagramas de estado también pueden ayudar a identificar problemas en un


sistema de software, como descubrir una condición que no se planeó. También
pueden ayudar a crear pruebas: conocer los diferentes estados de un sistema
puede ayudar a garantizar que las pruebas estén completas y sean correctas.

Comprobación de modelo
Además de comprender las técnicas para diseñar un sistema de software, es
importante conocer las técnicas para verificar el sistema. Algunas de estas
técnicas incluyen pruebas unitarias, pruebas beta y simulaciones. Otra de estas
técnicas es la comprobación de modelos, que es una verificación sistemática
del modelo de estado de su sistema en todos sus estados posibles. La
verificación de modelos ayuda a encontrar errores que otras pruebas no
pueden.
En la verificación de modelos, verifica todos los diversos estados del software
para tratar de identificar cualquier error, simulando diferentes eventos que
cambiarían los estados y las variables del software. Esto ayudará a exponer
cualquier falla al notificarle cualquier violación de las reglas que ocurra en el
comportamiento del modelo estatal. Por lo general, las comprobaciones de
modelos se realizan mediante software de comprobación de modelos. Hay
diferentes tipos de software disponibles para tales pruebas, algunos de los
cuales son gratuitos y están disponibles para desarrolladores que utilizan
diferentes idiomas. La verificación del modelo generalmente se realiza durante
la prueba del software.

Imagine un software que tenga una regla para no producir un interbloqueo.


Interbloqueo es una situación en la que el sistema no puede continuar porque
dos tareas están esperando el mismo recurso. El verificador de modelos
simularía los diferentes estados que podrían ocurrir en su sistema y, si fuera
posible un interbloqueo, proporcionaría detalles de esta violación.

Repasemos el proceso para el software de verificación de modelos.

Los verificadores de modelos comienzan generando un modelo de estado a


partir de su código. Un modelo de estado es una máquina de estado abstracta
que puede estar en uno de varios estados. El verificador de modelos luego
verifica que el modelo de estado se ajuste a ciertas propiedades de
comportamiento. Por ejemplo, el verificador de modelos puede examinar el
modelo de estado en busca de fallas como condiciones de carrera, explorando
todos los estados posibles de su modelo.

Hay tres fases diferentes en la verificación del modelo.

La primera es la fase de modelado. Durante esta fase, la descripción del


modelo se ingresa en los mismos lenguajes de programación que el sistema.
También se describen las propiedades deseadas. Esta fase también realiza
controles de cordura. Los controles de cordura son controles rápidos que
deberían ser fáciles de hacer, ya que provienen de una lógica clara y simple. Es
beneficioso probar estos errores simples antes de usar verificadores de
modelos, por lo que el enfoque puede estar en especificar las propiedades más
complejas para verificar. Los controles de cordura pueden incluir algo tan
simple como encender y apagar el sistema.
La segunda fase es la fase de ejecución. La fase de ejecución es cuando se
ejecuta el verificador de modelos para ver cómo el modelo se ajusta a las
propiedades deseadas descritas en la fase de modelado.

La tercera y última fase es la fase de análisis. Esta fase es cuando se verifica


que todas las propiedades deseadas estén satisfechas y si hay alguna violación.
Las violaciones se llaman contraejemplos. El verificador de modelos debe
proporcionar descripciones de violaciones en el sistema, para que pueda
analizar cómo ocurrieron.

La información proporcionada por el verificador de modelos le permite revisar


su software y solucionar cualquier problema. Una vez solucionados los
problemas, es una buena práctica volver a ejecutar el verificador de modelos.
Repita este proceso hasta que esté seguro de que el software es correcto con
respecto a las propiedades deseadas.

La verificación del modelo ayuda a garantizar no solo que el software esté bien
diseñado, sino también que el software cumpla con las propiedades y el
comportamiento deseados, y funcione según lo previsto.

También podría gustarte