Universidad Autónoma de Puebla

Facultad de Ciencias de la Computación

Ingeniería de Software

Principios del Diseño Orientado a Objetos

Prof. Dr. Manuel Martín Ortíz

Puebla, México Enero de 2003

1

Indice
INTRODUCCIÓN 4

1.

ENCAPSULACIÓN Y DEPENDENCIA ESTRUCTURA DE LA ENCAPSULACIÓN. NIVELES DE ENCAPSULACIÓN CRITERIOS DE DISEÑO Y NIVELES DE ENCAPSULACIÓN INTERDEPENDENCIA (CONNASCENCE). TIPOS DE INTERDEPENDENCIA CONTRA - INTERDEPENDENCIA (CONTRANASCENCE) = INDEPENDENCIA. FRONTERAS DE LA INTERDEPENDENCIA Y LA ENCAPSULACIÓN. INTERDEPENDENCIA Y MANTENIBILIDAD. ABUSOS DE INTERDEPENDENCIA EN SISTEMAS ORIENTADOS A OBJETOS. RECOMENDACIÓN FINAL.

5 5 5 7 9 9 13 15 16 18 19 20 20 21 21 22 23 23 26 26 28 29 29 30 32 33 35 35 38 40 41 44

1.1. 1.1.1. 1.1.2. 1.2. 1.2.1. 1.2.2. 1.3. 1.4. 1.5. 1.6. 2.

D O M I N I O S, E N C U M B R A M I E N T O Y C O H E S I Ó N.

2.1. DOMINIOS DE UNA CLASE DE OBJETOS. 2.1.1. EL DOMINIO DE BASE 2.1.2. EL DOMINIO DE LA ARQUITECTURA. 2.1.3. EL DOMINIO DE LOS NEGOCIOS. 2.1.4. EL DOMINIO DE APLICACIÓN. 2.1.5. LA FUENTE DE LAS CLASES EN CADA DOMINIO. 2.2. ENCUMBRAMIENTO (ENCUMBRANCE). 2.2.1. ¿ QUÉ ES EL ENCUMBRAMIENTO? 2.2.2. EL USO DEL ENCUMBRAMIENTO. 2.2.3. LA LEY DE DEMETRIO. 2.3. COHESIÓN DE UNA CLASE. 2.3.1. COHESIÓN DE MEZCLA DE INSTANCIA 2.3.2. COHESIÓN DE MEZCLA DE DOMINIO. 2.3.3. COHESIÓN DE MEZCLA DE ROLE. 3. ESPACIO DE ESTADOS Y COMPORTAMIENTO

3.1 ESPACIO DE ESTADOS Y COMPORTAMIENTO DE UNA CLASE. 3.2 ESPACIO DE ESTADOS DE UNA SUBCLASE. 3.3. EL COMPORTAMIENTO DE UNA SUBCLASE. 3.4. LOS INVARIANTES DE CLASE COMO UNA RESTRICCIÓN EN EL ESPACIO DE ESTADOS. 3.5. PRECONDICIONES Y POSTCONDICIONES.

2

DISEÑO

ORIENTADO

A

OBJETOS

4. CONFORMIDAD DE TIPOS Y COMPORTAMIENTO CERRADO. 4.1. CLASES VS. TIPOS. 4.2 EL PRINCIPIO DE CONFORMIDAD. 4.2.1. LOS PRINCIPIOS DE CONTRAVARIANZA Y COVARIANZA 4.2.2. ILUSTRACIÓN DE LA CONTRAVARIANZA Y COVARIANZA. 4.2.3. ILUSTRACIÓN DE CONTRAVARIANZA Y COVARIANZA. 4.2.4. RESUMEN DE LOS REQUERIMIENTOS PARA LA CONFORMIDAD DE TIPOS. 4.3. EL PRINCIPIO DE COMPORTAMIENTO CERRADO. 5. LOS PELIGROS DE LA HERENCIA Y EL POLIMORFISMO. 5.1. ABUSOS DE LA HERENCIA 5.1.1. AGREGADOS ERRÓNEOS. 5.1.2. JERARQUÍA INVERTIDA. 5.1.3. CONFUNDIENDO CLASES E INSTANCIAS. 5.1.4. MAL USO DE “ES UN”. 5.2. EL PELIGRO DEL POLIMORFISMO 5.2.1. POLIMORFISMO EN LAS OPERACIONES. 5.2.2. POLIMORFISMO DE VARIABLES. 5.2.3. POLIMORFISMO EN LOS MENSAJES 5.2.4. POLIMORFISMO Y GENERICIDAD. BIBLIOGRAFÍA

47 47 49 50 51 54 55 56 60 60 60 61 62 65 68 68 70 71 72 74

3

DISEÑO

ORIENTADO

A

OBJETOS

Introducción
Este material se ha desarrollado como un complemento para el curso de “Diseño Orientado a Objetos”, en el se cubre varios aspectos teóricos y prácticos. Se puede considerar el material como una selección de diversos materiales de estudio relativos al paradigma de orientación a objetos que se han generado en la última década. En particular se han tomado muchos elementos y partes del libro “Fundamentals Object-Oriented Design in UML” [1], él cual es un manual de la serie “Object Technology Series” de la corporación “Booch - Jacobson – Rumbaugh”. Espero sea de utilidad para las personas que inician a estudiar o quieren revisar lo alusivo al diseño orientado a objetos.

Dedicatoria
Quiero dedicar este material a todos los compañeros y compañeras que con mucho esfuerzo han logrado culminar sus estudios y a pesar de las condiciones adversas que muchas veces la vida nos impone, y han sabido encontrar fuerzas donde muchos no las ven y alegría en los momentos de desánimo.

4

DISEÑO

ORIENTADO

A

OBJETOS

1. E n c a p s u l a c i ó n y D e p e n d e n c i a
Este capítulo tratará las dos propiedades fundamentales de la estructura de los sistemas orientados a objetos: la encapsulación y la dependencia. Si bien estas dos propiedades de la estructura del software estaban presentes en los sistemas tradicionales, la orientación a objetos con sus nuevos paradigmas y complejidades, eleva su significado de manera considerable. La comprensión y mantenimiento del software orientado a objetos descansa en esencia sobre la encapsulación y la dependencia. El tema central de éste capítulo es la discusión de los conceptos antes mencionado y el cómo una buena combinación de ellos ayuda a alcanzar un buen diseño orientado a objetos.

1.1.

Estructura de la encapsulación.

Como es conocido, el software emergió los años 40’s del siglo XX como una colección de criaturas unicelulares conocidas como instrucciones en lenguaje de máquina. Posteriormente, éstas envolvieron a otras criaturas unicelulares conocidas como líneas en código ensamblador. Pero una estructura de mayor tamaño apareció pronto, dentro de la cual muchas líneas de código fueron reunidas en unidades procedurales con un solo nombre. Esta fue la subrutina o procedimiento. Un ejemplo de éstas últimas que alcanzó un lugar en el “Salón de la Fama” es la función sqrt, que como sabemos calcula la raíz cuadrada de un entero o real. La subrutina introdujo la encapsulación al software. Fue la encapsulación de líneas de código en una estructura un nivel superior que el código mismo. La subrutina en ese momento representó la maravilla del momento en el ambiente del software. Como es lógico, ésta redujo los requerimientos de memoria agrupando docenas de instrucciones, y por otro lado ayudó a los programadores, ofreciéndoles un término simple que hace referencia a docenas de líneas de código. Uno no se debe sorprender que con el advenimiento de la subrutina se produjera un gran entusiasmo entre los programadores y se convirtió en una unidad de trabajo de todos los días.

1.1.1.

Niveles de encapsulación

Se define el nivel de encapsulación de la subrutina como nivel-1. El código directo o rawel cero, que obviamente no contiene encapsulación se refiere como nivel-0. La orientación a objetos introduce un nivel más avanzado de encapsulación. La clase (u objeto) es una unión de subrutinas (conocidas como operaciones) en un nivel estructural todavía mayor. Ya que las operaciones, que son unidades procedurales, se encuentran en el nivel-1 de encapsulación, la clase se coloca en el nivel-2 de encapsulación. (Fig. 1.1)

5

DISEÑO

ORIENTADO

A

OBJETOS

La analogía entre organismos y estructuras de software, pensada con mayor profundidad, no es completamente gratuita. Los módulos procedurales, tales como aquellos que se encuentran en el diseño estructurado, no satisfacían la reusabilidad que la gente esperaba. Las clases autónomas se comportan mejor que los procedimientos. Las clases están mas cerca de los órganos biológicos por su capacidad de ser transplantados de aplicación en aplicación (de software evidentemente). Y las componentes lo hacen mejor todavía que las clases. Una pregunta que podemos hacernos es: ¿Y porqué detenerse en el nivel-2 de encapsulación?. Nosotros ya La estructura de clase u objeto Líneas de código Módulo Procedural

Nivel-0

Nivel-1

Nivel-2

Figura 1.1. Niveles de encapsulación que presentan las construcciones de software hemos visto estructuras de nivel-3 y nivel-4, en los cuales las clases se agrupan en estructuras de nivel aún superior, éstos corresponden a los paquetes y componentes. Estos se discutirán en capítulos posteriores con mayor detalle. Fuera del nivel-3 y estructuras superiores, solo algunas clases o parte de sus interfaces se hacen visibles, a éstas subestructuras se les conoce como parte pública. Existe un tipo de clase llamada clase de negocios, ésta se asocia en muchos casos con una organización grande estructura con el enfoque de objetos, éstas clases se agrupan de manera horizontal en estructuras de “nivel3” haciéndose una correspondencia con las áreas. Por ejemplo una aerolínea puede tener un área relacionada con el pasajero, otra para los aeropuertos, otra para el personal y otra para los vuelos. Así la clase relacionada con el “programa de pasajeros frecuentes” no tendría mucho que ver con la clase relativa a la lista de vuelos, las dos clases pueden ser encapsuladas en dos paquetes diferentes de nivel-3.

6

DISEÑO

ORIENTADO

A

OBJETOS

Las estructuras de nivel-3 pueden también ser agrupadas de manera vertical. Por ejemplo, las clases que operan juntas para implementar la política para ActualizarPasajeroFrecuente puede incluir: pasajero, Reservación, ConfiguracionDeAsientosDeVuelo, Asiento, así como clases de menor importancia como: PrioridadDeAtencion, Fecha, Hora y demás.

1.1.2.

Criterios de diseño y niveles de encapsulación

En la tabla siguiente (Tabla 1.1.) se resumen algunos criterios tradicionales del diseño estructurado en términos de los niveles de encapsulación. Ésta muestra que criterio se aplica a cada par de niveles de encapsulación. Tabla 1.1. Criterios de diseño estructurado(nivel-1) que gobiernan las interelaciones entre los elementos de cada par de niveles de encapsulación.
Para → construcción de nivel-0 Desde ↓ construcción de nivel-0 (línea de código) construcción de nivel-1 (procedimiento) (línea de código) Programación Estructurada Cohesión Acoplamiento Construcción de nivel-1 (procedimiento) fan-out

Por ejemplo, la cohesión es una medida clásica de la calidad de las relaciones entre un procedimiento (un constructor de nivel-1) y las líneas de código ( una construcción de nivel-0) dentro de un procedimiento. Los principios de la programación estructurada definen las relaciones entre una línea de código y otras líneas de código dentro del mismo procedimiento. El fan-out, la cohesión y el acoplamiento son términos del diseño estructurado [2,3]. A continuación se hace una breve recapitulación: • • • Fan-out. Es una medida del número de referencias a otros procedimientos por líneas de código dentro de un procedimiento dado. Cohesión. Es una medida del inconveniente simple que se presentan las líneas de código dentro de un procedimiento dado para cumplir el propósito de ese procedimiento. Acoplamiento. Es una medida del número y la fuerza de las conexiones entre procedimientos.

7

DISEÑO

ORIENTADO

A

OBJETOS

La siguiente tabla (Tabla 1.2) es una extensión de la tabla anterior e incluye la encapsulación de nivel-2. Puede notarse que las relaciones de los niveles básicos(1 y 2) se mantienen prácticamente iguales, el nivel-2 de encapsulación nos ofrece 5 relaciones a nominar. La cohesión entre clases es un análogo obvio a la cohesión de un procedimiento, pero es un nivel de encapsulación mayor. Esto demarca el inconveniente simple de un conjunto de operaciones (y atributos) al alcanzar el propósito de las clases. El acoplamiento entre clases es una medida del número y fuerza de las conexiones entre clases. Aunque los otros elementos (Tabla 1.2) no tienen nombre, es posible dárselos de manera contextual. Esto nos lleva a nueve nombres, si se incluyese el nivel-3 de encapsulación, la tabla tendría 16 elementos; estos pueden encontrarse con un poco de trabajo en la Literatura relacionada con la programación Orientada a Objetos. Tabla 1.2. Extensión de la Tabla 1.1 que incluye el nivel-2 de encapsulación
Para → construcción de nivel-0 Desde ↓ construcción nivel-0 (línea de código) construcción nivel-1 (procedimiento) construcción nivel-2 (clase) de (línea de código) de Programación Estructurada de Cohesión Acoplamiento construcción de nivel-1 (procedimiento) mensaje fan-out construcción de nivel-2 (clase)

____

____
Cohesión de clase Acoplamiento de clase

____

Más con las definiciones dadas debe ser suficiente. Cuando el número de partículas elementales que la física estalla en cantidad, los físicos empiezan a buscar cuando sus partículas son realmente fundamentales o se derivan de otras. De manera similar, cuando el número de criterios de diseño fundamentales crece de manera explosiva, puede ser que uno deba buscar un criterio mas profundo detrás de ellos. Si uno puede encontrar y determinar ese criterio, entonces se puede aplicar a los elementos del software en todos los niveles de encapsulación – inclusive en el nivel-5, sí acaso uno ha alcanzado tal estado.

8

DISEÑO

ORIENTADO

A

OBJETOS

1.2.

Interdependencia (Connascence).

La palabra connascence se deriva del latín y significa “que han nacidos juntos”. Una acepción alternativa es “que tienen destinos entrelazados en la vida”. Dos elementos de software que son interdependientes nacen de cierta necesidad relacionada – puede ser durante el análisis de requerimientos, diseño o programación – y comparten el mismo destino por al menos una razón. A continuación se hace una definición de interdependencia en el sentido de la Ingeniería de Software: La interdependencia entre dos elementos de Software A y B significa 1. que se puede postular algún cambio sobre A que requerirá que B sea modificado (o al menos revisado con cuidado) con el propósito de preservar la consistencia global, o 2. que se puede postular algún cambio que requerirá que tanto A como B sean modificados al mismo tiempo con el propósito de preservar la consistencia global. En esta sección se discutirá una variedad de formas de interdependencia. Y un propósito es presentar el concepto como una manera general para evaluar las decisiones en el proceso del diseño orientado a objetos, incluyendo aquellos diseños que contengan encapsulación de nivel-3 y nivel-4.

1.2.1.

Tipos de interdependencia

Iniciaremos con un ejemplo simple de interdependencia en un esquema no orientado a objetos. Tomemos cierto elemento de software A correspondiente a una línea tradicional de código en forma de declaración int i; // línea A

y un elemento B en forma de asignación: i := 12; // línea B

En éste contexto hay al menos dos ejemplos de interdependencia entre A y B. Por ejemplo, en la (improbable) situación que A fuese cambiada de tipo a char i; entonces B deberá ser cambiada también. A este fenómeno se le llama interdependencia de tipo. Así también, si A fuese cambiada a int j ; Entonces B deberá ser cambiada a j :=12 ;. Esta se denomina interdependencia de nombre. En el ejemplo anterior se dice que se tiene interdependencia explícita, la cual es fácilmente detectable mediante un buen editor de texto. En otro contexto la interdependencia puede ser implícita, por ejemplo en la siguiente rutina en ensamblador uno puede notar

9

DISEÑO

ORIENTADO

A

OBJETOS

X: JUMP Y+200 … Y:CLEAR R1 … CLEAR R2 … // 200 bytes de código específico // Esta es la instrucción a donde se debe saltar desde X – llamemos a esta línea Z

Es claro que la construcción de la rutina hay 200 bytes entre las instrucciones CLEAR R1 y CLEAR R2. De manera precisa exactamente 200 bytes, éste caso se denomina interdependencia de posición. Esta se produce en el marco que entre la dos inocentes instrucciones en Y y Z hay una liga muy fuerte debida al desagradable salto de la línea X. De cualquier manera la necesidad del desplazamiento (offset) de exactamente 200 bytes no es aparente, ni visible dentro del código después de la línea Y, el infortunio será para quién inserte otra instrucción en la sección de los 200 bytes, ya que, si al darle mantenimiento al programa se realiza la inserción en ésta zona por parte de algún programador, la próxima vez que el sistema corra se producirá un error de secuencia (crash) inmediatamente después de que se ejecute Y. En general debemos entender que lo explícito o implícito de la interdependencia no necesariamente se dará en los códigos con representación binaria únicamente o al usar medidas absolutas. La interdependencia más explícita es la que consume mayor tiempo y costo para ser detectada (excepto en los casos que este bien documentada en el lugar correcto). Las interdependencias que están dispersas a lo largo de grandes distancias de texto en la especificación de una biblioteca de clases u otra documentación, también consumirán gran cantidad de tiempo y son difíciles de descubrir. Se deben puntualizar algunas cosas: 1. Dos elementos de software no necesitan comunicarse uno con otro en cierto orden para ser interdependientes. ( Un ejemplo de esto es la interdependencia de posición entre las líneas Y y Z en la anterior rutina en ensamblador). 2. Algunas formas de interdependencia son direccionales. Si un elemento A hace referencia a un elemento B explícitamente, entonces A y B serán interdependientes unidireccionalmente (la dirección es de A hacia B). Muchos ejemplos de interdependencia de nombre son direccionales, por ejemplo, la interdependencia de nombre introducida cuando una clase hereda de otra. Si el elemento B también hace referencia al elemento A explícitamente, entonces A y B se dice que hay una interdependencia bidireccional. 3. Algunas formas de interdependencia son nodireccionales. Los elementos A y B serán interdependientes nodireccionales si nunca uno hace referencia explícita al otro. Por ejemplo, A y B serán interdependientes si usan el mismo algoritmo, a pesar que nunca uno haga referencia al otro en general.

10

DISEÑO

ORIENTADO

A

OBJETOS

Los ejemplos anteriormente descritos caen en la categoría de interdependencia estática. Esto es interdependencias que se corresponden al código de las clases que uno escribe, compila y liga. Esto es interdependencias que uno puede valorar y hallar dentro de la estructura léxica del código desarrollado. A continuación se realizará una revisión general (no completamente exhaustiva) de algunas variedades de interdependencia estática.

Interdependencia de nombre
Un ejemplo de ésta como se vio antes (líneas A y B), en el cual dos variables necesitaban tener el mismo nombre en cierto orden para hacer referencia a la misma cosa. Otro ejemplo es el siguiente: Una subclase que utiliza una variable heredada de su superclase debe obviamente usar el mismo nombre de la variable que definió la superclase. Si el nombre es cambiado en la implementación de la superclase, entonces deberá ser modificada también en la subclase si se desea conservar lo correcto de la construcción.

Interdependencia de tipo o clase
También se vio la interdependencia de tipo en el ejemplo anterior donde se definió la variable de tipo int. Si a i se le asigna el valor de 12 en la línea B, entonces i deberá ser declarada de tipo int en la línea A.

Interdependencia de convención
Digamos que la clase NumeroDeCuenta tiene instancias en las cuales los valores positivos de ella, tal como 1234, corresponden a personas; los negativos, como –234, corresponden a empresas; y el 0000 (cero) a todos los departamentos internos. El código para esta convención podría expresarse de la forma if orden.NumeroDeCuenta > 0 then … Entonces existe una interdependencia de convención entre todos los elementos de software que hagan contacto con un número de cuenta. A menos que ésta convención relativa al significado de la propiedad NumeroDeCuenta este encapsulado, estos elementos pueden estar repartidos a través del sistema. A este fenómeno también se le denomina interdependencia valor-significado, y es similar al acoplamiento híbrido en el diseño estructurado y ha causado muchos problemas de mantenimiento en los sistemas. Otro ejemplo es el siguiente: supongamos que cierta propiedad llamada direccion de la clase RV se puede representar de diferentes maneras, por ejemplo norte = 0 ; este = 1 ; norte = N ; este = E ; norte = 0 ; este = 90 ; sur = 2 ; sur = S ; sur = 180 ; oeste = 3 ; oeste = W ; oeste = 270 ;

11

DISEÑO

ORIENTADO

A

OBJETOS

Todo cliente (instancia) de RV que utilice el atributo estará expuesto a escoger una convención para representar la propiedad direccion; esto creara una gran interdependencia de convención. Entonces, es importante escoger una convención apropiada (por ejemplo la tercera) para representar a direccion.

Interdependencia del algoritmo
La interdependencia del algoritmo es similar a la interdependencia de convención. Por ejemplo; Un elemento de software inserta símbolos en una tabla de hash. Otro elemento busca por un símbolo en la misma tabla. Es claro que para este proceso ambos deben usar el mismo algoritmo para el hashing. Otro ejemplo es el de encriptar y desencriptar una cadena.

Interdependencia de posición
La mayor parte del código en las unidades procedurales tiene interdependencia de posición: Para que dos líneas de código sean ejecutadas en la secuencia correcta, ellas deben aparecer en la secuencia léxica correcta dentro del listado fuente. Hay muchas clases de interdependencia de posición, incluyendo la secuencial ( “las cosas deben aparecer en el orden correcto”) y de adyacencia (“las cosas deben estar una junto a la otra”). Otro ejemplo de interdependencia de posición corresponde a la dependencia de un mensaje entre los argumentos formales del emisor (del mensaje: sender) y los argumentos corrientes del destino (del mensaje: target). En la mayoría de los lenguajes, uno debe colocar los argumentos en la misma secuencia en que se definieron los formales, es decir debe haber una correspondencia entre los prototipos o plantillas y los llamados concretos. La interdependencia dinámica es la interdependencia que está basada ene l patrón de ejecución del código a ejecutarse – de los objetos más bien que de las clases, para ser estrictos. Éste tiene también una variedad de casos, a saber:

Interdependencia de ejecución
La Interdependencia de ejecución es el equivalente dinámico de la interdependencia de posición. Este se presenta en varias formas, incluyendo la secuencial (“debe llevarse a cabo en un orden dado”) y la de adyacencia (“debe llevarse a cabo sin intervención“). Hay varios ejemplos de éste tipo de interdependencia, incluyendo la inicialización de una variable antes de usarla, cambiar o leer los valores de variables globales en la secuencia correcta, y la prueba y modificación de los valores de los semáforos.

Interdependencia de valor
La Interdependencia de valor normalmente involucra alguna restricción aritmética. Por ejemplo durante la ejecución de un sistema, la propiedad ApuntadorBajo a un buffer circular no puede ser mayor que la propiedad ApuntadorAlto (bajo las reglas del módulo aritmético). Así también, las cuatro esquinas de un rectángulo deben preservar cierta relación geométrica entre sus valores. Uno no puede mover una esquina únicamente y mantener un rectángulo correcto. La interdependencia de valor es notoria cuando dos Bases de Datos contienen información redundante (repetida), a pesar de que este en diferentes formatos. En ésta situación, el software basado en estructuras procedurales debe mantener un puente de consistencia entre la bases de datos, para garantizar que cualquier dato duplicado tenga idénticos valores en cada Base de Datos. El
12

DISEÑO

ORIENTADO

A

OBJETOS

mantenimiento de éste software, que esta probablemente realizando tareas pesadas de traducción de formatos, puede ser incómodo y complejo.

Interdependencia de identidad
Un ejemplo de Interdependencia de identidad se presenta por una restricción típica en un sistema orientado a objetos: Dos objetos, U1 y U2, cada uno de los cuales contiene una variable que apunta a otro objeto (UX), deben apuntar “todo el tiempo“ al mismo objeto. Esto es, si U1 apunta a UX, entonces U2 debe apuntar a UX. (Por ejemplo si el reporte de ventas a punta a la tabla ENERO, entonces el reporte de operaciones debe apuntar también a la tabla ENERO). En ésta situación U1 y U2 tienen interdependencia de identidad; ellos deben apuntar al mismo (estrictamente idéntico) objeto. U1 UX U2
Figura 1.2. Un ejemplo de Interdependencia de Identidad.

1.2.2.

Contra - interdependencia (contranascence) = independencia.

Hasta ahora, se ha identificado de manera tácita la interdependencia con “relacionado”. Por ejemplo, dos líneas de código tienen interdependencia de nombre cuando la variable en cada una de ellas debe tener el mismo nombre. De cualquier forma, la interdependencia existe también en casos donde la diferencia es importante.. Primero veamos un ejemplo simple. Observemos las dos declaraciones siguientes: int i; int j; Por consistencia, exactitud y para que el código compile sin errores, los nombres de las variables, i y j, deben diferir uno del otro. Hay una interdependencia en el trabajo de codificación implícito: Sí por alguna razón, queremos cambiar el nombre de la primera variable a j, entonces debemos también cambiar el nombre de la segunda de j a alguna otra cosa. Por tanto las dos declaraciones no son independientes. Eventualmente a ésta clase de interdependencia se le llama “ interdependencia de diferencia ” o “interdependencia negativa ”. Una forma más simple para denominar el fenómeno es pedir que se preserve la diferencia mas que la identidad. Es decir pedimos que dos elementos de software no tengan que ver algo el uno con el otro, así una forma simple y directa para describir el proceso es pedir que sean independientes.

13

DISEÑO

ORIENTADO

A

OBJETOS

Un caso familiar de independencia se observa en los entornos orientados a objetos que permiten la herencia múltiple ( o sea la habilidad que tiene una subclase de heredar de múltiples superclases). Sí la clase C hereda tanto de la clase A como de la B, entonces las características de A y de B no deberán tener los mismos nombres: pues habrá interdependencia de nombre entre las características de A y las características de B. Pongamos un ejemplo más concreto, pensemos en una aplicación de software en un negocio de renta de videos. En el siguiente diagrama (Fig. 1.3.) se muestra una situación de herencia múltiple,
PhysicalInventoryItem props: length RecordingMedium props: length

ProgramRentalItem

Figura 1.3. Diagrama de herencia múltiple La clase ProgramRentalItem hereda de PhysicalInventoryItem y RecordingMedium. Ambas clases tienen un atributo llamado length. Pero el significado de length en la clase PhysicalInventoryItem se refiere a la longitud física de un ítem en centímetros, y length en la clase RecordingMedium se refiere al tiempo de duración de un programa ( la película o lo que sea que contenga el video). Aunque uno pueda argumentar que duration sería un nombre más apropiado para el segundo atributo, uno debe tomar lo que se encuentra dentro de la biblioteca de clases. En éste ejemplo, la clase ProgramRentalItem necesita heredar ambos atributos llamados length, y esta colisión en los nombres acarrea un serio problema. En la medida que uno es quién diseña las clases, será posible evitar éste tipo de enredos, pero sí uno debe utilizar una biblioteca de clases preestablecida, entonces se debe tener cuidado con situaciones como la anterior. Esto implica un estudio detallado y fino, de los nombres de las propiedades y métodos que conforman a las clases de donde uno hereda. A lo ancho de una biblioteca de clases completa, en la cual cualquier pareja puede compartir una subclase, hay un potencial cuadro de interdependencia de nombres a través de todas las clases debido a la herencia múltiple. Por ésta razón la herencia múltiple ha adquirido una mala reputación y no debe sorprender que un buen lenguaje de programación orientado a objetos contenga mecanismos que eviten esta interdependencia desenfrenada. Uno de los mejores mecanismos en un lenguaje es la instrucción rename de Eiffel. Ésta permite en ese entorno renombrar una característica a solicitud del programador, con esto se evita la duplicidad de significados.

14

DISEÑO

ORIENTADO

A

OBJETOS

1.3.

Fronteras de la Interdependencia y la Encapsulación.

Las características de interdependencia e independencia son el corazón de las construcciones modernas de la Ingeniería del Software. Para explicar esto, retomemos el tópico de encapsulación (sección 1.1). Aunque se ha definido la encapsulación y los niveles a los cuales ésta puede aspirar, no se ha dicho porque la encapsulación es importante. La encapsulación es un chequeo sobre la interdependencia, especialmente sobre la independencia. Imaginemos un sistema con 100,000 líneas de código. Imaginemos además que el sistema se organizó en un solo módulo, por decir algo en el procedimiento principal (main). Imaginemos luego que nosotros debemos dar mantenimiento y hacer modificaciones a éste sistema. La interdependencia entre los cientos de nombres de las variables será por sí misma una pesadilla: Simplemente para elegir un nombre para una nueva variable, uno debe revisar docenas de nombres existentes para evitar la duplicidad. Otro problema será la naturaleza engañosa del código. Consideremos dos líneas de código que son adyacentes en el listado fuente. Uno se debe preguntar por qué éstas son adyacentes: Es porque ellas deben ser adyacentes (debido a la interdependencia de posición) y luego preguntarnos que sucederá si se inserta una línea nueva de código entre ellas, ¿El sistema seguirá funcionando? Así, un sistema que no está partido en unidades encapsuladas tiene por naturaleza dos problemas: • • interdependencia potencial confusión sobre qué es verdadera interdependencia y dónde hay similaridad accidental o adyacencia.

El tomar en cuenta las interdependencias responde al porque la orientación a objetos “funciona”. La orientación a objetos elimina – o al menos domestica – algunas de las interdependencias que se presentan como rebeldes en los sistemas modulares tradicionales con sólo el nivel-1 de encapsulación. Revisemos un ejemplo, tomemos el caso antes expuesto de la tabla de hash. Asumiremos un sistema que mantiene una tabla simple de hash y está diseñado con sólo el nivel-1 de encapsulación (correspondiendo a un diseño simplemente estructurado). El sistema deberá acceder a la tabla de hash desde diferentes puntos dentro del código (al menos dos). El código en éstas posiciones tendrá interdependencia de algoritmo; sí uno quiere introducir un mejor algoritmo de hashing, entonces uno tendrá que buscar todas las posiciones dentro del código que usan el algoritmo anterior y realizar los cambios necesarios, por ejemplo en los parámetros. El nivel-1 de encapsulación no nos guiará a los lugares donde se encuentra el llamado, ni nos indicará cuántos son. Inclusive si hubiese sólo dos posiciones con el algoritmo de hashing, éstos pueden ser cercanos o lejanos dentro del listado del programa. Uno los deberá ubicar por si mismo, excepto que la documentación sea lo bastante fina para ayudarnos a encontrarlos. En contraparte, un sistema orientado a objetos, con al menosnivel-2 de encapsulación, debe tener un nicho natural para el algoritmo de hashing, digamos en la clase TablaDeSimbolos. Aunque persista una interdependencia de algoritmo entre la operación de InsertaSimbolo y la operación BuscaSimbolo, la interdependencia estará bajo control. Sí el diseño orientado a objetos utilizado es bueno, entonces no ha-

15

DISEÑO

ORIENTADO

A

OBJETOS

habrá interdependencia debida al algoritmo de hashing en algún lugar del sistema (esto es, en cualquier lugar fuera de la clase TablaDeSimbolos). En general un buen diseño permitirá la sustitución del algoritmo de manera transparente y no deberán modificarse los sitios donde se invoque a los métodos de inserción y búsqueda.

1.4.

Interdependencia y mantenibilidad.

La interdependencia ofrece tres líneas guía para mejorar el mantenimiento de los sistemas, éstas son: 1. Minimizar completamente la interdependencia mediante la fragmentación del sistema en elementos encapsulados. 2. Minimizar cualquier interdependencia remanente que cruce las barreras de la encapsulación. 3. Maximizar la interdependencia dentro de las fronteras de la encapsulación. Estas reglas van más allá de la orientación a objetos. Se pueden aplicar a cualquier diseño para la construcción de software con nivel-2 o nivel-3 de encapsulación, e inclusive para niveles superiores. Así, como ya se debe haber notado, éstas reglas expresan un viejo principio del diseño: “Mantenga las cosas similares juntas y las disímiles aparte”. De cualquier forma, éste viejo principio no nos dice como saber si una “cosa” es similar o disímil respecto a otra, de hecho, hay elementos de software con interdependencia mutua. La figura siguiente (Fig. 1.4.) muestra dos clases con interdependencia entre ellas, la interdependencia es direccional, la dirección se especifica con la punta de la flechas.

Operación

Clase

Variable privada

Clase 1

Clase 2

Figura 1.4. Esquema de interdependencia entre clases, las líneas más gruesas violan las fronteras de encapsulación.

16

DISEÑO

ORIENTADO

A

OBJETOS

Algunas de éstas interdependencias (ilustradas con las líneas anchas) violan los principios de diseño orientado a objetos debido a que conectan el diseño interno de una clase con el diseño interno de otra: de tal manera que las cosas parecidas se han puesto en diferentes estructuras de software. Por ejemplo, una línea de un método de la Clase 1 hace referencia a variables dentro de la Clase 2 lo cual viola el principio de encapsulación orientada a objetos.. Podemos decir que una interdependencia direccional viola la encapsulación sólo cuando cruza dentro de una unidad encapsulada. En la siguiente figura (Fig. 1.5.) se muestra otra pareja de clases que no violan la interdependencia de encapsulación.

Operación

Clase

Variable privada

Clase 1

Clase 2

Figura 1.5. Las líneas muestran las interdependencias, y ninguna viola las fronteras de encapsulación. La interdependencia representa un conjunto de vínculos en el software. La interdependencia explícita es aparente en el código fuente y puede ser descubierta a menudo con sólo un editor de textos usando la opción de búsqueda o mediante un listado de referencias cruzadas. La interdependencia implícita (que no es evidente mediante un análisis simple del código fuente en si) puede ser detectada sólo con un análisis humano apoyado por la documentación existente del sistema. La mente humana es una herramienta cara en general ( en caso que se pague por el trabajo), y muchas veces falible, para hacer el seguimiento de la interdependencia de dispersión amplia en el software. La interdependencia implícita, especialmente cuando traspasa los límites de la encapsulación presenta un reto extra a quien da mantenimiento a un sistema. La interdependencia implícita llega a ser particularmente difícil de seguir y ubicar meses después que el código ha sido diseñado, escrito y liberado. Puede ser que en un futuro las herramientas CASE puedan ayudar para monitorear las interdependencias en general y revelen interdependencias implícitas. Esto será de gran utilidad en sistemas de larga escala que exhiban encapsulación de nivel-2 o superior.

17

DISEÑO

ORIENTADO

A

OBJETOS

1.5.

Abusos de interdependencia en Sistemas orientados a objetos.

Como se ha visto en las secciones anteriores, las posibilidades de la encapsulación de nivel-2 que ofrece la orientación a objetos es de gran beneficio para un diseñador que trata de controlar (domar – domesticar) la interdependencia. De cualquier manera, en esta última sección acerca de la interdependencia, se darán tres ejemplos de cómo los diseñadores que utilizan el paradigma de orientación a objetos violan el principio de “mantener la interdependencia en casa”, es decir dentro de los límites de la encapsulación. 1. La función “friend” de C++. Esta función fue creada en C++ expresamente para violar las fronteras de la encapsulación. Es un elemento fuera de las fronteras de una clase que tiene acceso a los elementos privados de un objeto de dicha clase. Es decir, sí una función friend ff toma el manejador de un objeto de clase la C1, podrá interactuar al interior de tal objeto lo quiere o no aquel. La interdependencia entre ff y C1 es por lo tanto alta; esto incluye varios tipos de interdependencia: de nombre, de tipo, de convención, etc. Así cualquier cambio que el diseñador realice al diseño interno de C1 requerirá un ajuste en ff de manera exhaustiva. Sí ff es el friend de sólo una clase, uno puede argumentar que ff es realmente parte de esa clase y que todos los accesos caen dentro de sus fronteras. Lo cual suena bien, pero si ff es también el friend de C2, C3 y C4, entonces el argumento fallará. Desgraciadamente muchos diseños en C++ utilizan ésta estructura. Un uso legítimo desde los principios correctos del diseño orientado a objetos de la construcción friend es la depuración de los estados internos de los objetos que todavía están bajo prueba. 2. Herencia sin restricciones. La construcción basada en la herencia puede algunas veces introducir interdependencias terribles. Sí uno permite a una clase que haga uso tanto de elementos visibles externamente (públicos) y elementos sólo visibles internamente (privados) de una superclase, entonces uno introducirá un gran compromiso de interdependencia a lo largo de las fronteras de encapsulación de ella (la superclase). Esto incluirá interdependencias de nombre, clase y otras variedades. En general debemos cuidar que la herencia recibida por una subclase de su superclase esté restringida sólo a aquellos miembros que ya sean visibles externamente en la superclase (miembros públicos). Otra forma de postular esto es la siguiente, ”La noción de heredar el comportamiento abstracto debe estar divorciado de la noción de heredar la implementación interna de tal comportamiento” [4]. 3. Dependencia en los accidentes de implementación. Supongamos que un programador conocido como “El tlacuache” ha creado una clase Set que ofrece el comportamiento de la estructura matemática conjunto (Set). Ésta clase incluye operaciones tales como add, remove, size, etc. La clase aparentemente está bien diseñada y escrita, y bien realizada. “El tlacuache” utilizó Set en diferentes lugares dentro de su aplicación. Por ejemplo, utilizó la operación retrieve, la cual recupera elementos de un conjunto uno por uno en orden aleatorio hasta que han sido servidos todos los elementos (es decir que se cubrió todo el conjunto). En general “El tlacua18

DISEÑO

ORIENTADO

A

OBJETOS

che” sabe que retrieve recupera los elementos en el orden exacto en el cual ellos fueron insertados en el conjunto, pero esto no esta documentado o descrito como una propiedad de la operación. El posteriormente hace uso de éste hecho, de manera accidental y no documentada, muchas veces dentro de su aplicación. Por lo tanto el ha creado una interdependencia de algoritmo a lo largo de las fronteras de la encapsulación entre su aplicación y las características internas de la operación retrieve. Cuando posteriormente Set es reemplazado por una implementación correcta por otros programador, la cual cumple con el contrato original que dicta “recuperar los elementos del conjunto de forma aleatoria”, muchas de las aplicaciones que ha distribuido “el tlacuache” empiezan a dar problemas y muchos de los usuarios de ellas se incomodan, pero no hay donde encontrar al “tlacuache”. Se dice que ha cambiado de identidad y se mudó a un país árabe como incógnito. Esperemos que el día de los “tlacuaches” pase pronto y no regrese.

1.6.

Recomendación final.

Para finalizar, una regla básica de diseño orientado a objetos que se puede extraer de ésta sección es la siguiente: “Se deben minimizar las interdependencias en general – incluyendo la independencia – separando los sistemas en elementos encapsulados. Posteriormente se debe minimizar cualquier remanente de interdependencia que cruce las fronteras de la encapsulación maximizando la interdependencia dentro de las fronteras de encapsulación”

19

DISEÑO

ORIENTADO

A

OBJETOS

2. D o m i n i o s, E n c u m b r a m i e n t o y C o h e s i ó n.
Las clases en un sistema no son siempre lo que parecen ser. En esta sección se definirán algunos conceptos que ayudan a entender y colocar en una jerarquía a una clase.

2.1. Dominios de una clase de objetos. Un sistema normal orientado a objetos contendrá clases organizadas en cuatro grandes dominios, cada uno de éstos dominios tienen diferentes grupos dentro de ellos. La lista siguiente presenta un resumen de éstos cuatro grandes dominios. 1. Dominio de Aplicación. Comprende a las clases valorables para una aplicación. Sus grupos interiores de éste dominio son: • • 2. Clases para el manejo de eventos Clases para el reconocimiento de eventos

Dominio de Negocios. Comprende a las clases valorables para una industria o compañía. Sus grupos interiores son: • • • Clases de relación Clases de role (papel que se juega) Clases de atributo

3. Dominio de la Arquitectura. Comprende a las clases valorables para una arquitectura de implementación. Sus grupos interiores son: • • • Clases de interface-humana Clases de manipulación de bases de datos Clases de comunicación con la máquina

3. Dominio de Base. Comprenden a las clases valorables a lo largo de todos los negocios y arquitecturas. Sus grupos son: • • • Clases Semánticas Clases estructurales Clases fundamentales

20

DISEÑO

ORIENTADO

A

OBJETOS

En los apartados siguientes se explicará el significado de cada dominio comenzando con las clases de la parte baja de la estructura anterior.

2.1.1. El Dominio de Base

Las clases en el dominio de base se pueden usar en muchas aplicaciones de diferentes industrias corriendo sobre un amplio rango de arquitecturas. En otras palabras, el dominio de base comprende a las clases con la reusabilidad más extensa posible. El dominio de base tiene tres grupos de clases como se indicó antes, algunos ejemplos de clases de cada grupo son: • Las Clases fundamentales incluyen a Integer, Boolean y Char entre otras. Éstas clases son tan básicas que muchos lenguajes orientados a objetos las incluyen como predefinidas como tipos de datos simples y tradicionales. Las Clases estructurales implementan estructuras. Comprenden una lista equivalente a la de una asignatura típica de “Algoritmos y Estructuras de Datos” en los estudios de Licenciatura o Ingeniería en Computación. Elementos de ésta son Stack (Pila), Queue (Cola), List (Lista), BynaryTree (Árbol Binario), Set (Conjunto), etc. A éstas se les conoce también como clases contenedoras (container classes), éstas están muy a menudo diseñadas por medio de la genericidad. Las Clases Semánticas incluyen a Date (fecha), Time (hora), Angle (ángulo), Money (dinero), Mass (masa), Point, Line, Polygon y Circle. Las clases semánticas tienen un significado más rico que Integer o Char. Adicionalmente, los valores de sus atributos pueden ser expresados en unidades, tal como horas, minutos, pesos, dólares, etc.

Las clases del dominio de base, por definición, pueden ser de gran utilidad en cualquier aplicación en cualquier lugar. Una clase List o Date puede aprovecharse tanto en un sistema orientado a la medicina como a un sistema de una videoteca. Una clase de base puede estar construida sobre otras clases de base. Por ejemplo Angle puede usar a Real en su implementación, y Polygon puede usar a Set.
2.1.2. El Dominio de la Arquitectura.

Las clases en éste dominio se pueden usar también en muchas aplicaciones de muchas industrias diferentes. Sin embargo, la reusabilidad de las clases arquitectónicas está limitado a una arquitectura específica de computadora. Algunos ejemplos de los grupos que conforman éste dominio son: • Las Clases de comunicación con la máquina incluyen por ejemplo a Port que brinda el servicio de acceder a un puerto físico de la máquina (paralelo, serial, USB, Ethernet, etc.). RemoteMachine que facilita identificar o permitir que se conecte una máquina remota a través de una red de enlace.
21

DISEÑO

ORIENTADO

A

OBJETOS

Las Clases de Manipulación de Bases de Datos incluyen a Transaction que permite realizar una transacción en una base de datos y Backup que realiza un respaldo de la base de datos o de una parte de ella. Las Clases de Interface Humana incluyen a Window que es el elemento básico de las aplicaciones en los entornos gráficos multitarea y a CommandButton que dentro de las interfaces gráficas corresponde a un botón que al ser pulsado ejecuta una tarea dentro de una aplicación.

Éstas clases son útiles en cualquier aplicación o negocio, mientras tanto la aplicación esté implementada sobre una arquitectura física soportada por estas clases. En otras palabras, no puede haber una biblioteca de clases en el dominio de la arquitectura simple para todo mundo, debido a que clases como Port o Backup existirán en muchas versiones para los diferentes tipos de arquitecturas de máquina que existen. Por lo tanto la selección de la biblioteca de clases en el dominio de la arquitectura dependerá de la arquitectura física y real del hardware y software que donde uno implementará la aplicación a desarrollarse.
2.1.3. El Dominio de los Negocios.

Las clases de éste dominio son útiles en muchas aplicaciones, pero sólo dentro de un contexto simple, tal como la medicina, la banca, o las líneas áreas. Algunos ejemplos de estas clases son: • Las Clases de atributo capturan las propiedades de las cosas en el mundo de los negocios. Como ejemplo de éstas tenemos Balance (de una cuenta de banco) o BodyTemperature (de un paciente médico). Aunque estas clases son similares a Money y Temperature (que son clases de base semánticas), no son idénticas. Un balance de una cuenta de banco y la temperatura del cuerpo de un paciente serán sujetos de ciertas reglas que no se aplican a las clases de base generales. Por ejemplo, el valor de un balance de cuenta puede estar restringido a estar en ciertos límites. La transgresión de estos límites puede indicar un error o disparar otra actividad financiera. Las Clases de Role se derivan de “el papel que juegan las cosas” en los negocios, Un Role o Papel en el análisis orientado a objetos es análogo al tipo-entidad en el modelado de información. Ejemplos de estas clases son Customer (cliente) y Patient (paciente). Cuando uno analiza el dominio de los negocios, estas clases son las clases que primero y más obviamente uno identifica. Las Clases de relación se derivan de las asociaciones entre las cosas en el mundo de los negocios. Éstas incluyen a AccountOwnership (propietario de una cuenta de banco) y PatientSupervision (por parte de una enfermera).

La aplicabilidad de ciertas clases de negocios puede ser más restringida que una industria entera: Ellas serán útiles sólo a una corporación simple, ya que aunque algunas corporaciones grandes se estructuran internamente en diferentes áreas de operación, la utilización de una clase de operación dada no puede extenderse más que a una división de la corporación. Por ejemplo, supongamos que una empresa está desarrollando siete clases diferentes de OrdenDeCompra y la compañía tiene sólo nueve divisiones. Uno podría sugerir que ellos hagan reingeniería con la empresa para reducir el número de clases OrdenDeCompra incompatibles, pero uno podría tener la duda que ellos puedan de alguna forma reducir el número a una sola clase,

22

DISEÑO

ORIENTADO

A

OBJETOS

porque posiblemente dos divisiones no necesariamente levantan las ordenes de compra exactamente de la misma manera. Otra vez puede verse el patrón en el cual las clases en los dominios superiores se construyen sobre clases de los inferiores. Por ejemplo, AccountOwnership hará referencia a las clases Customer y Account (cuenta) y al menos a un atributo de Account será de la clase Balance.

2.1.4. El Dominio de Aplicación.

Una clase en éste dominio se utiliza dentro de una aplicación simple o de un pequeño número de aplicaciones relacionadas. Éste dominio contiene dos grupos de clases: de reconocimiento de eventos que corresponde a la detección de que cierto evento particular ha sucedido y de manejo de eventos que corresponde a la ejecución apropiada de la acción correcta para un evento particular. Ejemplos de éstas son: • Clases de Reconocimiento de eventos. En el sentido de la programación son demonios de eventos, construcciones de software que monitorean la entrada para checar la ocurrencia de eventos específicos en el medio ambiente. Un ejemplo es un objeto de la clase PatientTemperatureMonitor, la cual registrará por los eventos patient develops fever – el paciente tiene fiebre – y patient becomes hypothermic – el paciente esta muy frío – en general respecto a una media o a otros pacientes. Clases de Administración de eventos. Éstas llevan a cabo las acciones apropiadas cuando un evento de cierto tipo ocurre. Un ejemplo de éstas es un objeto de la clase WarmHypothermicPatient, la cual inmediatamente envía mensajes a otros objetos para incrementar la temperatura del paciente y solicitará atención médica haciendo sonar una alarma en el cuarto de enfermeras. Obviamente el objeto WarmHypothermicPatient pasará a su estado activo ante el evento correspondiente a un paciente hipotérmico.

Una clase en el dominio de Aplicación tiene una reusabilidad muy pequeña. Es importante notar que, la mayor parte de las clases en éste dominio son relevantes a sólo una aplicación y no tienen reusabilidad en general. Por ejemplo la clase ProgramaPacienteParaCirugia probablemente sea usada sólo en un Sistema de Administración Quirúrgica.
2.1.5. La fuente de las clases en cada dominio.

Para la mayor parte de la gente, la habilidad para acomodar las bibliotecas de clases con el software reusable es la razón misma de la orientación a objetos y muchos equipos de trabajo dedican muchos de sus esfuerzos de diseño en este sentido con el propósito de alcanzar la exquisitez de la reusabilidad de las clases que han construido. Pero como se ha mostrado en las secciones anteriores, las clases en los diferentes dominios tienen diferentes grados de reusabilidad. Las clases en el dominio más bajo tienen gran reusabilidad, mientras que las clases del dominio más alto tienen menos. La figura 2.1 ilustra lo antes dicho.

23

DISEÑO

ORIENTADO

A

OBJETOS

Aplicación
Manejo de eventos Reconocimiento de eventos

Baja Reusabilidad

Negocios
Relaciones Role Atributos Media Reusabilidad

Arquitectura
Interface humana Manipulación de BD Comunicación con la máquina

De base
Semántica Estructural Fundamental Alta Reusabilidad

Fig. 2.1. Dominios de las clases y su reusabilidad El origen de las clases depende fuertemente del dominio al que pertenece una clase. Para la clase de base la respuesta es simple: Generalmente es adquirida o comprada a un fabricante de clases, por decir a quien vende o distribuye compiladores o herramientas de dicha categoría. El emprender y desarrollar bibliotecas de clase base es normalmente difícil y caro. Por supuesto, si un equipo o persona decide hacerlo, entonces podrá tener las clases que quiera y pueda, pero invertirá un gran esfuerzo en lograrlo. Eventualmente para la venta de aplicaciones puede resultar miles de veces más caro que comprar la biblioteca, pero ésta tarea alguien la debe hacer. Uno en general debe aumentar la biblioteca de clases que ha comprado con algunas clases de diseño propio, esto está bien. Pero comprar, pedir prestada o regalada cualquier conjunto de clases base que uno necesite muchas veces resulta mejor opción que desarrollarla, siempre que el tiempo sea un factor determinante para la entrega de una aplicación a los clientes. Muchas veces cuando se está en la tarea de innovación se deben desarrollar las clases de base requeridas, por ejemlo ante nuevo hardware no soportado por el sistema operativo o en el marco de la implementación de nuevos algoritmos para la solución optimizada de ciertos problemas. La fuente de las clases en el dominio de la arquitectura es similar a las de base, pero hay cuatro diferencias importantes: • Uno puede tener que comprar clases en el dominio de la arquitectura del vendedor(es) del hardware y software que se adquiere. También existen los llamados vendedores alternativos (third-party) que proveen bibliotecas para las arquitecturas más populares.

24

DISEÑO

ORIENTADO

A

OBJETOS

• • •

Uno puede sufrir ciertas incompatibilidades a lo largo de ciertas partes de la biblioteca en el dominio de la arquitectura si se usan productos de diferentes vendedores. El proveedor de la biblioteca puede haber construido su producto en base a una biblioteca diferente a la que uno está utilizando. La biblioteca en el dominio de la arquitectura adquirida probablemente requiera mayor refinamiento, ajuste y modificaciones que la biblioteca de base con que se cuenta, debido a los dos puntos anteriores.

Las clases en el dominio de los negocios son de especial interés y retadoras. Estas son clases que no es posible comprar por el momento por al menos dos razones. La primera, los vendedores no tienen la experiencia industrial para desarrollar clases para hospitales o bancos o aviación o telecomunicaciones a la medida. Y la segunda radica en que el mercado para ésta clase de bibliotecas sería como una pesadilla ya que cada comprador desea la personalización de ellas. Uno puede escuchar al respecto argumentos como el siguiente: “Nuestra compañía está orgullosa en ser especial y diferente. La clase – por decir algo - Producto que Uds. nos venden no coincide con nuestras necesidades como empresa”. (Puff !!) De cualquier manera la situación está cambiando. Ahora se pueden encontrar clases en el dominio de los negocios y componentes para algunos propósitos corporativos a la venta, ejemplos de esto son la banca y las telecomunicaciones. Todavía y por un buen intervalo de tiempo el software en el dominio de los negocios será ampliamente vendible, por tanto hoy uno deberá seguir desarrollando las clases para este tipo de aplicaciones. Y uno deberá tener mucho cuidado en analizar los requerimientos – usando técnicas tal como el modelado función-relación o el entidad-relación – porque ellas se convertirán en el tesoro de cada compañía que desarrolla su software. De igual manera uno tendrá que prestar gran cuidado en su diseño, pues éstas clases tendrán una significativa reusabilidad sí han sido bien analizadas y diseñada, pero su reusabilidad será nula si no lo están.. Lo competitivo de una empresa dependerá del tiempo para poner en marcha los nuevos sistemas de software, lo cual dependerá también de manera significativa de la calidad de las clases de negocios en la biblioteca utilizada. El dominio que esta en la parte alta de la estructura de dominios es el dominio de aplicación. No debemos preocuparnos mucho por el diseño para la reusabilidad en este estrato; de cualquier manera aunque uno haga su mejor esfuerzo de diseño, uno no alcanzará mucha reusabilidad, lo cual es natural. La fuente principal para las clases en éste dominio son los eventos de negocios que se descubren durante el análisis. Puede ser que en los párrafos anteriores la categoría de “construcción” sea mejor que el término “clase”, ya que los reconocedores de eventos y manejadores de eventos deban ser implementados como procedimientos tradicionales o instancias simples de los paquetes de utilerías, más que clases originales e interesantes.

25

DISEÑO

ORIENTADO

A

OBJETOS

2.2. Encumbramiento (encumbrance). En la sección anterior se han tratado los dominios de las clases de una manera cualitativa, ahora pasaremos a un análisis cuantitativo, que nos dirá que tan alto una clase descansa respecto al dominio fundamental. Llamaremos a ésta medida Encumbramiento.
2.2.1. ¿ Qué es el encumbramiento?

Este mide las dimensiones de la maquinaría auxiliar de una clase. Es decir todas las otras clases de las cuales que una clase dada depende para poder operar. En otras palabras, si uno cuenta todas las clases a que hace referencia con detalle una clase C, el número total de ellas será el encumbramiento de C (es decir que tan arriba en la estructura de clases se encuentra C). Para definir de manera formal encumbramiento, primero definiremos algunos términos, Definición. “El conjunto de referencias directas a una clase C es el conjunto de clases a las cuales C hace referencia de manera directa”. En la mayor parte de los lenguajes orientados a objetos, una clase C puede hacer referencia directa a otra clase D en cualquiera de las siguientes maneras: • • • • • • • • C hereda de D. C tiene un atributo de clase D. C tiene una operación con un argumento de entrada de clase D. C tiene una variable de clase D. C tiene un método que envía un mensaje con un argumento de retorno de clase D. C tiene un método que contiene una variable local de clase D. C provee a D como un parámetro de clase actual a una clase parametrizada. C tiene a D como una clase friend (en C++).

Definición. “Supongamos que el conjunto de referencias directas de la clase C contiene a las clases C1, C2, …, Cn. Entonces, el conjunto de referencias indirectas de la clase C es la unión de las referencias directas de la clase C y los conjuntos de referencias indirectas de C1, C2, … , Cn.”
26

DISEÑO

ORIENTADO

A

OBJETOS

Una definición matemática más formal es la siguiente: “El conjunto de referencias indirectas de una clase C es equivalente a la cerradura transitiva sobre el conjunto de referencias directas de C.” La primera definición de referencias indirectas es claramente recursiva. El límite de la recursión está – tanto para el caso directo como indirecto – en el punto donde se alcanzan las clases fundamentales, pues luego de ellas no hay mas clases. Definición. “El encumbramiento directo de una clase es el tamaño de su conjunto de referencias directas de clase. Y el encumbramiento indirecto de una clase es el tamaño del conjunto de sus referencias indirectas de clase”. A continuación se hará una ilustración del encumbramiento, usemos una flecha para mostrar una referencia de clase directa. En la figura 2.2. se presenta un ejemplo de referencia de clase directa. C En éste ejemplo el conjunto de referencias de clase directas es {C1, C2, C3}, por lo tanto el encumbramiento de C es simplemente 3. La figura 2.3. muestra un conjunto de referencia indirectas. Uno determina el encumbramiento indirecto de C contando todas las clases en el diagrama, esto es, todas las clases en el árbol cuya raíz es C y cuyas hojas son las clases fundamentales ubicadas en la parte inferior (denotadas por Fk).

C1

C2

C3

Fig. 2.2. C y su conjunto de referencias directas

C

El encumbramiento indirecto de C es por tanto 12. Debemos tener cuidado en incluir las referencias directas de clase en el conteo del conjunto de referencias indirectas de clase. C3 Una situación concreta se muestra en la figura 2.4.. En ésta se puede observar que el encumbramiento indirecto de Rectangle es 4, ya que tiene cuatro clases - Point, Length, Real y Boolean - en su conjunto de referencias indirectas. Aunque Real pueda hacer referencia a Boolean, por ejemplo en una operación de comparación y a Integer en una operación de redondeo, se puede adoptar la convención usual “cero” para definir el encumbramiento de Real a cero.

C1

C2

C11

C12

C21

C31

C32

F1

F2

F3

F4

Fig. 2.3. C y su conjunto de referencias de clase indirectas

27

DISEÑO

ORIENTADO

A

OBJETOS

Rectangle (4)

Fig. 2.4. Rectangle y su conjunto de referencias de clase indirectas.

Point (3)

Length (2)

Boolean (0)

Real (0)

2.2.2. El uso del encumbramiento.

El encumbramiento nos da una medida de la sofisticación de una clase - esto es, que tan arriba esta la clase respecto al dominio fundamental. Entonces, las clases en dominios superiores tendrán un mayor encumbramiento indirecto y aquellas en los dominios inferiores en posición tendrán menor encumbramiento indirecto.

Un encumbramiento indirecto inesperado puede indicar una falla en el diseño de una clase. Por ejemplo, sí uno encuentra una clase con mayor encumbramiento indirecto en un dominio bajo, entonces podrá haber problemas con la cohesión de la clase (este concepto se discutirá a detalle en la siguiente sección). De manera alternativa, sí una clase en un dominio superior tiene menor encumbramiento indirecto, entonces ésta ha sido probablemente diseñada a rasguños; esto es, el diseñador la ha construido directamente a partir de Integer, Char, y otras clases fundamentales, en vez de haber reutilizado clases intermedias de la biblioteca.

28

DISEÑO

ORIENTADO

A

OBJETOS

2.2.3. La Ley de Demetrio.

Lieberherr y Holland [5] proponen la Ley de Demetrio como un principio guía para limitar el encumbramiento directo de una clase, limitando el tamaño de su conjunto de referencias directas. Una síntesis general de la Ley de Demetrio es la siguiente: Para un objeto obj de clase C y para cualquier operación op definida para obj, el destino de un mensaje dentro de la implementación de op debe ser uno de los siguientes objetos: 1. 2. 3. 4. 5. El objeto obj mismo – específicamente, self y super (en Smalltalk), this (en C++ y Java), o current (en Eiffel). Un objeto referido como un argumento dentro de la firma de op. Un objeto referido mediante una variable de obj (incluyendo cualquier objeto dentro de las colecciones que hacen referencia a obj). Un objeto creado por obj. Un objeto referido por una variable global o de la clase principal (main).

Hay dos versiones de la ley, las cuales difieren sólo en la interpretación del tercer punto. La Ley Fuerte de Demetrio define a una variable como variable única, a una variable definida en la clase C misma. La Ley Débil de Demetrio define a una variable, ya sea una variable de C o una variable que C ha heredado de su superclases. La Ley de Demetrio es eminentemente razonable, pues restringe las referencias arbitrarias a otras clases dentro de una clase dada. Bajo esta ley, el código dentro de C hará referencia a sólo el mínimo número posible de otras clases. Obviamente es preferible usar la Ley Fuerte de Demetrio ala Débil porque además limita la interdependencia a lo largo de las fronteras de la encapsulación como se explicó en el capítulo anterior, es decir de las fronteras de clase de las superclases de C. Como se discutió en el capítulo anterior también, limitar la interdependencia en éste sentido facilita el mantenimiento y evolución del sistema por dos razones: • • Libera al diseñador de las superclases de C de rediseñar su implementación interna, y Mejora la comprensión de C, puesto que sí alguien trata de entender el diseño de C no estará continuamente arrastrado por los detalles de implementación de las superclases de C, o lo que es peor, aquellos de una clase completamente ajena.

2.3. Cohesión de una clase. La cohesión de clase es la medida de la interelación de las características (los atributos y operaciones) localizados en la interface externa de una clase.
29

DISEÑO

ORIENTADO

A

OBJETOS

En la Tabla 1.2. se inserto el término de cohesión de clase en un nivel de encapsulación superior que el correspondiente a la cohesión de procedural (modular). En el diseño estructurado, la cohesión procedural se interpreta como la medida de cómo las líneas de código corresponden entre sí con un módulo procedural simple, como es una función o un procedimiento. Puede ser que el término tipo de cohesión sea un término mejor que cohesión de clase, ya que con éste concepto se trata de evaluar que tan bien una clase “corresponde a una implementación de cierto tipo de dato abstracto”. Una clase con baja cohesión (o bien mala cohesión) tiene un conjunto de características que no corresponden entre sí. Una clase con alta cohesión (o buena cohesión) tiene en conjunto de características que contribuyen en conjunto al tipo de abstracción implementada por la clase. Existe otra interpretación de cohesión de clase por ciertos autores, éstos consideran como los métodos que implementan las operaciones de una clase usan las variables internas de la clase. La idea es la siguiente: “Mientras mayor sea el traslape en el uso de los métodos al usar las variables, será mayor la cohesión de la clase”. Ésta interpretación es cuestionable, por al menos razones. La primera razón es que la cohesión es una propiedad que debe ser aparente desde el “exterior” de una unidad de software encapsulada. Por lo tanto, parece erróneo tener que ver en el interior de una clase (en sus partes privadas) con el propósito de evaluar su cohesión. La segunda razón es que ésta medida es inestable y muy dependiente del diseño interno particular de los métodos, que podrá cambiar a lo largo del tiempo de vida de la clase. En consecuencia, una clase joven y poco madura (es decir aquella que empieza a diseñarse y desarrollarse) aparentará tener una cohesión baja que una clase madura (aquella que ha crecido a su forma final y adulta en sus características e implementación). Esto es injusto en general, pues implica un rechazo a los nuevos miembros en la familia de clases. Hay tres problemas identificables de cohesión en la asignación de las características de las clases, éstos son observables desde el diseño externo de la clase, concretamente los problemas se denominan: • • • cohesión de mezcla de instancia (mixed-instance cohesion) cohesión de mezcla de dominio (mixed-domain cohesion) cohesión de mezcla de role (mixed-role cohesion)

De los tres, la cohesión de mezcla de instancia es típicamente el mayor pecado y la cohesión de mezcla de role el menor. Una clase puede tener todos, algunos o ninguno de éstos tres problemas de cohesión. Una clase que no presente alguna de estas tres cohesiones mezcladas en enteramente cohesiva y se dice que tiene cohesión ideal. A continuación se hará una discusión de cada tipo de mezcla de cohesión y se evaluarán sus síntomas en la fase de diseño.
2.3.1. Cohesión de mezcla de instancia

Una clase con cohesión de mezcla de instancia tiene ciertas características que están indefinidas para ciertos objetos de la clase.

30

DISEÑO

ORIENTADO

A

OBJETOS

Por ejemplo, digamos que un departamento de ventas tiene vendedores con comisión y sin comisión. Digamos que Juan trabaja con comisión y María sin comisión. En una aplicación orientada a objetos que soporte a éste departamento, hay una clase Vendedor, de la cual los objetos que apuntan a las variables juan y maria son instancias. Considerando el caso anterior, el primero de los mensajes siguientes tiene sentido, pero el segundo no lo tiene: juan.EmitirComisionPorMes; maria.EmitirComisionPorMes; Uno podría mantener los dos mensajes y definir la comisión del objeto maria a cero. De cualquier forma, esto se vuelve un lío. María no tiene comisión cero; ella tiene una comisión indefinida. Es mas, Vendedor podría necesitar también una variable booleana EsComisionado (posiblemente disponible como un atributo). Y uno podría incluir en la operación Vendedor. EmitirComisionPorMes una instrucción if que prevenga la impresión de cheques por comisión de <$ 0.00 > para los “objetos sin comisión”. En general la idea va a trabajar, pero es un feo diseño. El problema real es que la clase Vendedor tiene cohesión de mezcla de instancia, porque es muy burda para la aplicación: se ha unido tanto a vendedores con comisión y sin comisión en una sola clase. Lo que uno debe hacer es crear subclases más finas VendedorConComision y VendedorSinComision a nuestro diseño. Estas nuevas subclases heredarán de su superclase, Vendedor. Así un vendedor con comisión (el caso de Juan) será representado por una instancia de VendedorConComision, mientras que un vendedor sin comisión se representara por una instancia de VendedorSinComision. Y la operación EmitirComisionPorMes se ubicará en EmitirComisionPorMes, el diseño se muestra en la figura 2.5.

Vendedor

Vendedor ConComision
/comisionPorMes : Money

Vendedor SinComision

Fig. 2.5. Remoción del problema de cohesión para Vendedor añadiendo subclases. La cohesión de mezcla de instancia usualmente indica una jerarquía de clases que no está bien refinada o que simplemente es incorrecta. Y como se ha visto, obliga a introducir trucos en el código que empañan el trabajo de análisis y diseño, en el ejemplo anterior corresponde al if que se introdujo para hacer compatibles las instancias, éste fenómeno se puede detectar al revisar los if’s que separan los casos dentro de una clase para que se puedan consideran las variantes incompatibles.
31

DISEÑO

ORIENTADO

A

OBJETOS

2.3.2. Cohesión de mezcla de dominio.

Una clase con cohesión de mezcla de dominio contiene un elemento que directamente encumbra a la clase con una clase extrínseca de un dominio diferente. En la definición anterior, se ha usado el concepto de “dominio” en el mismo sentido que se toma en la sección 2.1., Pero ahora se requiere definir el concepto de “extrínseco(a)”. La clase B es extrínseca a A si A puede ser completamente definida sin una noción de B. B es intrínseca a A sí B captura algunas características inherentes a A. En éste contexto, Rinoceronte es extrínseco a Persona, pues en ningún sentido “rinoceronte” captura alguna característica de una persona. Pero, Fecha (como en fecha de nacimiento) es intrínseca a Persona. Hay muchos ejemplos de cohesión de mezcla de dominios, algunos son obvios y otros muy sutiles. Como primer ejemplo suponga que a la clase Real (de número real) se le crease un atributo ArcTan (ángulo cuya tangente es). Esta inclusión relaciona a los números reales con los ángulos, pero muchas cosas son número reales y los números reales no corresponden a la categoría de los ángulos. Esto es una muestra de mezcla de dominios que no es evidente y elevaría a los objetos de tipo Real a la categoría de objetos de tipo Angulo, lo cual no es correcto pues los dominios son diferentes. ¿Qué pasaría si le adicionamos una operación llamada convierteTemperatura a la clase Real si podemos convertir grados Centígrados a Farenheit y Kelvin a Réaumur ? Esto encumbrará a Real junto a Temperatura. O bien, si queremos encontrar la equivalencia entre pesos y euros, podríamos encumbrar Real con Money. La lista de posibilidades absurdas no tiene fin, la figura 2.6. presenta una opción:

Real
ArcTan : Angle FarenheitEquival : FarenheitTemp CantidadEnEuros : DineroEuropeo TuPropioInvento : ?

Figura 2.6. Real con cohesión de mezcla de dominio exagerada Cuando uno diseña una clase en cierto dominio, uno deberá incluir clases de los dominios inferiores en el diseño – esto es lo que brindará reusabilidad. Pero uno debe estar seguro que se requieren esas clases debido a las propiedades intrínsecas de la clase que se está diseñando. Por ejemplo, es correcto esperar que para la clase Cuenta se tenga una operación que regrese un objeto de la clase Dinero o bien que tenga un atri-

32

DISEÑO

ORIENTADO

A

OBJETOS

buto de la clase Fecha. Pero uno debe guardar una mala sospecha si una clase regresa un objeto de la clase en el dominio de la arquitectura TercerPinDelPuertoSerialDB9. La afirmación: “Una clase en el dominio de la arquitectura a menudo se mezcla con el dominio de los negocios”, es incorrecta, excepto que el proyecto de negocios en desarrollo se refiera a la construcción de infraestructura de arquitectura de computadoras (por ejemplo una aplicación para INTEL o AMD). El viejo lema orientado a objetos de que “una cosa debe conocer algo acerca de sí misma”, como en el caso de que “un documento debe saber como imprimirse a sí mismo”, puede ser un poco atrayente. Una clase Documento con un conocimiento específico de una impresora tiene cohesión de mezcla de dominios. Cuando uno diseña una clase en un dominio dado, uno debe ser particularmente cauteloso de introducir clases de dominios superiores en la clase que está diseñando. Por ésta razón la clase Real tiene un problema. Real es una clase fundamental, y no debe ser encumbrada con clases de dominios superiores.

2.3.3. Cohesión de mezcla de role.

Una clase C con cohesión de mezcla de role contiene un elemento que directamente encumbra a la clase con una clase extrínseca que se ubica en el mismo dominio de C. A diferencia de una clase con cohesión de mezcla de dominio, una clase con cohesión de mezcla de role no favorece a ambos dominios. De cualquier forma, no incluye mas que un role de un dominio simple (donde role significa una abstracción de un grupo de cosas parecidas en el mundo real). Un ejemplo famoso se refiere a personas y gatos. Digamos que se tiene una clase Persona con un atributo numDeGatosEnPosesion (número de gatos que la persona posee). Si la operación de recuperación (get) para el atributo es una función simple (con el mismo nombre numDeGatosEnPosesion), podríamos usar algo como felipe. numDeGatosEnPosesion para encontrar cuantos gatos posee el objeto felipe (de la clase Persona). Es notorio que: La clase Persona no tiene cohesión de mezcla de instancia, porque los objetos sin-gatos pueden regresar simplemente un valor cero. No tiene cohesión de mezcla de dominio, porque Persona y Gato están en el mismo dominio de asunto. PERO, tiene cohesión en el dominio de role, porque Persona y Gato son conceptos diferentes, extrínsecos uno con otro. En términos puros de diseño, la cohesión de mezcla de role es la transgresión menos seria de las cohesiones de clase. De cualquier forma, al diseñarse una clase para aumentar la reusabilidad, entonces se debe prestar especial atención a la cohesión de mezcla de role. ¿Qué pasa cuando uno trata de reutilizar Persona en una aplicación donde los gatos no intervienen? Es posible retomar la clase Persona inicial, pero uno deberá acarrear equipaje extra (lo relativo a la posesión de gatos en el ejemplo) innecesario, y es posible que

33

DISEÑO

ORIENTADO

A

OBJETOS

se reciban algunos mensajes incómodos por parte del compilador o ligador relativos a la ausencia de gatos en las personas. La cohesión de mezcla de role es para todo mundo atractiva pues, • • Es muy sencillo escribir el código correspondiente para hallar cuantos gatos tiene alguien: felipe. numDeGatosEnPosesion Muchas aproximaciones al modelado de la información podrían tratar a numDeGatosEnPosesion como un atributo de Persona.

En general a pesar de lo atractivo de los argumentos anteriores, uno no debe distraerse en la creación automática de la clase (Persona) con cohesión de mezcla de role. Hay muchas otras opciones de diseño que no comprometen la cohesión de la clase (Persona) que pueden ser consideradas.

En resumen podemos decir que: Como diseñador con orientación a objetos, uno debe aspirar a crear clases con cohesión ideal, esto es, clases carentes de cohesión de mezcla de instancia, mezcla de dominio o mezcla de role. Las clases con cohesión ideal tienen la máxima reusabilidad, y debe ser el objetivo primario de la práctica orientada a objetos, particularmente para las clases en el dominio de los negocios.

34

DISEÑO

ORIENTADO

A

OBJETOS

3. E s p a c i o d e E s t a d o s y C o m p o r t a m i e n t o
En ésta sección se introducirán dos propiedades fundamentales de las clases: el espacio de estados y el comportamiento, luego se explorará el espacio de estados de las subclases y su comportamiento, se introducirá el invariante de clase como una restricción sobre el espacio de estados y finalmente las precondiciones y postcondiciones de las operaciones, las cuales juntas forman el contrato entre una operación y un cliente de ésta operación. Las clases invariantes, las precondiciones y postcondiciones forman la espina dorsal de un diseño orientado a objetos, al método se le denomina diseño por contrato.

3.1 Espacio de estados y comportamiento de una clase. Una clase representará una abstracción uniforme de las propiedades de los objetos individuales que corresponden a la clase. A pesar de que esto suena como una frase célebre, ¿realmente qué significan éstas palabras? Por “abstracción” debemos entender que uno no necesariamente tiene que considerar todas las posibles propiedades de las cosas del mundo real que están representadas en los objetos de software. Por ejemplo, al crear una clase ClienteBancarioHumano, no está uno obligado a incluir la propiedad DiametroCraneal en la clase. Por “uniforme” entenderemos que la abstracción que uno selecciona para una clase se aplica de la misma manera a cada uno de los objetos que son instancias de ella. Por ejemplo, Si uno está interesado en la fecha de nacimiento para ClienteBancarioHumano, entonces estamos interesados en la fecha de nacimiento de todos los clientes bancarios humanos. Por ”propiedades” de debe entender simplemente que: Las dos propiedades de una clase están en su espacio de estados y en su comportamiento permitido. Gran parte de la discusión de ésta sección girará alrededor de los conceptos de espacio de estados y comportamiento y sus implicaciones prácticas en el diseño orientado a objetos.

35

DISEÑO

ORIENTADO

A

OBJETOS

Antes de proceder a una definición formal de éstos conceptos se hará una ilustración de ellos. Pensemos en un “caballo” como pieza de un tableo de ajedrez – como se muestra en la figura 3.1 - a éste lo podemos representar como un objeto de la clase CaballoAjedrez.

Fig. 3.1. Un tablero de ajedrez con una reina y un caballo.

Las líneas negras indican los posibles comportamientos del caballo y las claras el de la reina. El espacio de estados para el CaballoAjedrez corresponde a todo cuadro en el tablero, y el espacio de estados para la ReinaAjedrez también corresponde a todos los cuadros del tablero. Pero las clases ReinaAjedrez y CaballoAjedrez no son iguales, ¿Cuál es la diferencia entre ellas?; su comportamiento. La reina se pude mover a lo largo de cualquier columna, renglón o diagonal a partir del punto donde ella se encuentra, por lo cual ella podrá alcanzar cualquier posición en un tablero vacío en sólo dos jugadas. O sea puede ubicarse en otro estado de su espacio de estados. Por otro lado, el caballo necesitará realizar hasta seis (6) jugadas para alcanzar cualquier cuadro en el tablero. Diremos que las clases ReinaAjedrez y CaballoAjedrez tienen espacios de estados idénticos, pero diferentes comportamientos. Imagine ahora otra clase de “caballo”, tal que se mueva como lo hace un caballo de ajedrez normal, pero no le es permitido ocupar los cuatro cuadros centrales del tablero (Fig. 3.1.). Este caballo es diferente al primero, ya que aunque se mueve con las mismas reglas (es decir tiene el mismo comportamiento), su estado de estados es diferente al del primero.

36

DISEÑO

ORIENTADO

A

OBJETOS

Revisando los ejemplos anteriores, se puede concluir que dos clases pueden diferir ya sea en sus espacios

Fig. 3.2. Un Caballo con restricción de los 4 cuadros centrales de estados, en su comportamiento o en ambas cosas. El espacio de estados de una clase C es el conjunto de todos los estados que tiene permitidos un objeto de la clase C. Las dimensiones del espacio de estados son las coordenadas necesarias para especificar el estado de un objeto dado. Definiremos informalmente, el estado de un objeto es el “valor” que él tiene en un momento dado. Con mayor propiedad diremos que, el estado de un objeto es el conjunto ordenado de objetos a lo cuales el objeto hace referencia a un tiempo dado. Por ejemplo, el estado de un objeto Alberca ahora puede ser definido por la terna (30, 2, 10, 25) – largo, profundidad y ancho en metros y temperatura en grados centígrados. En otras palabras, este objeto Alberca actualmente hace referencia a un objeto de la clase Longitud (30 m), otro de clase Profundidad (2.5 m), uno mas de clase Ancho (10 m) y a uno de clase Temperatura (25 °C) . Una imagen que uno se puede formar del espacio de estados corresponde a un arreglo de puntos, donde cada punto corresponde a un estado. Cada objeto que es una instancia de la clase es un punto que pasa su vida moviéndose de un lugar a otro dentro del espacio de estados de la clase. Estos movimientos se denominan transiciones. En la figura 3.3. se presenta una representación de un espacio de estados tridimensional, que corresponde a una clase llamada Producto. Las dimensiones de la matriz corresponden a tres propiedades de la clase:

37

DISEÑO

ORIENTADO

A

OBJETOS

peso, precio y disponibilidad. Ya que éstas variables son mutamente independientes, un objeto se puede encontrar en cualquier lugar de la matriz. Los objetos se representan como puntos dentro de la retícula (matriz).

Peso del Producto

Disponibilidad del Producto Precio del Producto

Figura 3.3. Espacio de estados de la clase Producto como una matriz 3D, cada producto (instancia) es un punto en la rejilla.

Las dimensiones del espacio de estados de una clase son equivalentes a los atributos primarios definidos en la clase. En general, dado que cada dimensión es en si una clase, los valores a lo largo de cada dimensión son objetos de la dimensión de la clase.

3.2 Espacio de estados de una subclase. Si B es una subclase de A, entonces el espacio de estados de B puede estar completamente contenido dentro del espacio de estados de A. Diremos que el espacio de estados de B esta confinado por el espacio de estados de A. Un ejemplo de ésta situación es el siguiente, digamos que la clase A corresponde a VehiculoDeCarretera. Por simplicidad, asumiremos que su espacio de estados tiene una sola dimensión: PesoCorriente. Definiremos los límites de peso superior e inferior en 0.5 tons y 10.0 tons (toneladas) para VehiculoDeCarretera.PesoCorriente. Supongamos ahora que la subclse B es Automovil. Si se definen sus límites inferior y superior en 1.0 tons y 3.0 tons para Automovil.PesoCorriente, entonces estaremos en buena forma. El espacio de estados de Automovil esta confinado dentro del espacio de estados de VehiculoDeCarretera y por lo tanto Automovil es una subclase válida de VehiculoDeCarretera. La figura 3.4 muestra los rangos de VehiculoDeCarretera.PesoCorriente y Automovil.PesoCorriente de manera gráfica.

38

DISEÑO

ORIENTADO

A

OBJETOS

Puede observarse que si se definieran los límites inferior y superior de Automovil.PesoCorriente en 0.2 y 14.0 tons se presentaría un problema. Por ejemplo, un automóvil de 13 tons no caería en la definición de VehiculoDeCarretera, pues limita a los vehículos de carretera a 10 tons. De donde, el espacio de estados de Automovil ya no estaría confinado dentro del de VehiculoDeCarretera y por lo tanto Automovil ya no sería una subclase válida de VehiculoDeCarretera. Esta violación al espacio de estados puede permitir a un objeto ser una instancia legal de su propia clase,
espacio de estados de Automovil

espacio de estados de VehiculoDeCarretera

0

1

2

3

4

5

6

7

8

9

11

Figura 3.4. Espacio de estados unidimensional de Automovil y VehiculoDeCarretera.

pero una instancia ilegal de su superclase. En términos del ejemplo tendríamos que: “ Un coche es un automóvil, pero no un vehículo de carretera” - lo cual es absurdo. En general dado que una subclase hereda de su superclase, entonces está en la libertad de definir nuevas propiedades particulares (para la subclase), esto implica que una subclase en general puede tener mas dimensiones que su superclase – lo cual no debe sorprendernos. Si B es una subclase de A, entonces el espacio de estados de B debe comprender al menos las dimensiones de A, pero puede incluir otras más. En caso que comprenda mas dimensiones, se dirá que el espacio de estados de B extiende al de A. En el ejemplo anterior un caso sería el siguiente, Automovil puede incluir una dimensión llamada NumeroDePasajerosActual, que en general no sea una dimensión de VehiculoDeCarretera, por ejemplo ausente en los vehículos de carga, pipas de agua, etc. Esto puede en general confundir, pues uno puede preguntar ¿Cómo puede el espacio de estados de una subclase tanto estar confinado por y extender al espacio de estados de su superclase? La respuesta es la siguiente: Dentro de las dimensiones definidas por la superclase, el espacio de estados de una subclase puede ser menor que el de la superclase – pero al mismo tiempo, el espacio de estados de la subclase puede extenderse a otras dimensiones que están indefinidas en la superclase. En la figura 3.5. se muestra el espacio de estados de Automovil simultáneamente confinado por y extendiendo al espacio de estados de VehiculoDeCarretera.

39

10

12

DISEÑO

ORIENTADO

A

OBJETOS

En la dimensión PesoCorriente, el espacio de estados de Automovil está todavía confinado dentro de VehiculoDeCarretera, por que el rango de Automovil.PesoCorriente todavía cae dentro del rango de VehiculoDeCarretera.PesoCorriente. Por otro lado, Automovil tiene una dimensión extra (NumeroDePasajerosActual) en su espacio de estados. Por lo tanto su espacio de estados se extiende a una segunda dimensión; Específicamente, como se muestra, un automóvil puede tener de cero a siete pasajeros abordo. Mientras que para la clase más general (la superclase) está indefinido VehiculoDeCarretera. NumeroDePasajerosActual.
NumeroDePasajerosActual

10 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4

Espacio de estados de Automovil

Espacio de estados de VehiculoDeCarretera

5

6

7

8

9

10

11

12

13

14

PesoCorriente

Figura 3.5. Espacio de estados 2D para Automovil y de espacio de estados 1D para VehiculoDeCarretera.

3.3. El comportamiento de una Subclase. La mayoría de los objetos – excepto aquellos inmutables – realizan transiciones dentro de su espacio de estados en la medida que uno o varios de sus atributos cambian de valor. Estas transiciones constituyen el comportamiento permitido para una clase, que se define de la siguiente manera: El comportamiento admitido de una clase C es el conjunto de transiciones que un objeto de clase C tiene permitido realizar entre los estados en el espacio de estados de C. Esta definición implica que no todas las posibles transiciones son legales para un objeto. Un objeto se mueve dentro del interior del espacio de estados de su clase sólo en la manera prescrita para él dado el comportamiento de la clase. En el ejemplo anteriormente expuesto de los “caballos” en el tablero de ajedrez, un caballo puede encontrarse en cualquiera de las 64 casillas del tablero, pero sólo se permiten un conjunto finito de transiciones en una jugada simple a partir de una posición definida, las cuales a lo mas son 8.

40

DISEÑO

ORIENTADO

A

OBJETOS

¿Y qué se puede decir acerca del comportamiento en las subclases?, ¿Habrá una relación entre el comportamiento de una subclase y su superclase, como sucede con sus espacios de estados?, ¿Éste comportamiento estará confinado y/o extendido en una subclase? La respuesta a éstas preguntas es afirmativa. Primero, consideremos la extensión del comportamiento que B (la subclase) posee, pero de que A (la superclase) carece. Es obvio que dicho comportamiento extra que tiene B debe existir, pero ¿cómo una instancia e B administra la parte del espacio de estados de B que se extiende fuera del correspondiente de A? Tomemos el ejemplo de los vehículos. En éste, Automovil tiene un comportamiento facilitado por operaciones tales como SubePasajero y BajaPasajero, que permiten incrementar o decrementar el número de pasajeros. Éste comportamiento de forma clara va más allá de cualquier comportamiento que VehiculoDeCarretera pueda tener, después de todo, VehiculoDeCarretera no tiene alguna noción de lo que es un pasajero – pues está fuera de su especificación. Es decir VehiculoDeCarretera carece de la dimensión NumeroDePasajerosActual en su espacio de estados. De donde, cualquier comportamiento que se mueva en la dimensión de NumeroDePasajerosActual no será posible definir para VehiculoDeCarretera. En el ejemplo discutido se muestra que Automovil puede extender el comportamiento de su superclase, VehiculoDeCarretera, en la medida que Automovil extiende el espacio de estados de VehiculoDeCarretera. El comportamiento de Automovil estará confinado por el comportamiento de VehiculoDeCarretera. En la medida que el espacio de estados de Automovil está confinado por el espacio de estados de VehiculoDeCarretera. Un ejemplo posible es el siguiente: Uno podrá incrementar un 5 ton al peso de un vehículo para un vehículo de carretera genérico, en la medida que éste no exceda el límite de 10 ton, de cualquier forma uno no podrá incrementar 5 ton al peso de un automóvil por que su peso máximo es de 3 ton y al hacer esto uno se saldrá del espacio de estados de Automovil. En general el confinamiento del comportamiento de una clase es un principio muy importante en el diseño de las subclases, a este principio se le denomina comportamiento cerrado y será discutido en el capítulo siguiente.

3.4. Los Invariantes de Clase como una restricción en el espacio de estados. La mayoría de los espacios de estados que se han ilustrado en éste capítulo han sido completos en el sentido que un objeto puede ocupar cualquier lugar en la rejilla del espacio de estados. Pero, muchas clases no autorizarán a sus objetos ésta libertad. Por ejemplo el caso del “caballo” en el tablero con bloque de la figura 3.2. muestra un caso de restricción. En general será necesario definir de manera formal los estados permitidos para los objetos de una clase. El espacio de estados legal de una clase se define por sus invariantes de clase.

41

Un invariante de clase es una condición que todo objeto de esa clase debe satisfacer en todo momento – cuando el objeto está en equilibrio.Ñ O O R I E N T A D O A O B J E T O S DISE La expresión “cuando un objeto está en equilibrio” significa que un objeto puede obedecer a sus invariantes de clase todo el tiempo cuando no está inmerso en un ambiente de cambios de estado. En particular, un objeto debe estar en la línea de invariantes cuando el objeto es inicializado en el momento de la instanciación – en general mediante algún constructor de la clase – y antes y después que cualquier operación (pública) sea ejecutada. Para ejemplificar tomemos la clase Triangulo. Si los lados de un triángulo son Triangulo.a, Triangulo.b y Triangulo.c, como se muestra en la figura 3.6. Triangulo props: b a a b c c

Figura 3.6. Clase Triángulo y sus propiedades básicas. Un invariante de clase para Triangulo puede ser: a + b ≥ c and b + c ≥ a and c + a ≥ b. Que establece que, independientemente de la clase de objeto de la clase Triangulo uno tenga, la suma de las longitudes de cualquiera de sus lados debe ser mayor o igual que la longitud de tercer lado. El espacio de estados de Triangulo se puede representar como un sistema (Figura 3.7) de tres coordenadas etiquetadas a, b y c. Más en general esto está restringido sólo a un conjunto de puntos en el espacio (a, b, c), en particular aquellos que satisfacen el “invariante” para Triangulo formarán es verdadero espacio para la clase.

b Figura 3.7. Espacio de estados 3D para Triangulo – donde no todas las posiciones representan triángulos válidos.

a

c

42

DISEÑO

ORIENTADO

A

OBJETOS

Por ejemplo no es posible formar un triángulo con lados (1, 2, 5) o (2, 3, 9), pues violan el invariante de clase. Más las ternas (3, 4, 5) y (4, 4, 6) si lo satisfacen. En la figura 3.8 se muestra una jerarquía de las variedades de triángulos.
Triangulo

TrianguloRectangulo

TrianguloIsosceles

TrianguloIsoscelesRectangulo

Figura 3.8. Cuatro tipos de triángulos válidos

La clase TrianguloIsosceles tiene el invariante, a = b or b = c or c = a. Por otro lado la clase TrianguloRectangulo tiene el invariante (basado en el Teorema de Pitágoras), si c es el lado mayor, a2 + b2 = c2 . Dado que las invariantes se heredan, una clase dada debe obedecer las invariantes de sus superclases. Por lo tanto como TrianguloIsosceles y TrianguloRectangulo son subclases de Triangulo, también heredarán el invariante a + b ≥ c and b + c ≥ a and c + a ≥ b. En consecuencia, los objetos de la clase TrianguloIsosceles siempre obedecerán al invariante compuesto (a + b ≥ c and b + c ≥ a and c + a ≥ b) and (a = b or b = c or c = a.). Para mantener éstas características adicionales, la clase TrianguloIsoscelesRectangulo - que hereda tanto de TrianguloIsosceles como de TrianguloRectangulo – tiene un espacio de estados formado por los puntos que son válidos en forma simultánea TrianguloIsosceles y TrianguloRectangulo. Su invariante de clase es en consecuencia el and lógico de los invariantes de TrianguloIsosceles y TrianguloRectangulo, explícitamente:
(a + b ≥ c and b + c ≥ a and c + a ≥ b) and (a = b or b = c or c = a) and si c es el lado mayor, a2 + b2 = c2.

43

DISEÑO

ORIENTADO

A

OBJETOS

En lenguajes como C++ y Java, un invariante de clase es un constructor teórico, que no se implementa de manera directa en el lenguaje. En estos lenguajes, los invariantes de clase no se heredan realmente en el mismo sentido que se heredan los atributos.

3.5. Precondiciones y Postcondiciones. Una vez que se han revisado los invariantes o reglas que gobiernan a una clase de manera global, en esta sección se estudiarán las condiciones que rigen las operaciones de manera individual. Toda operación tiene una precondición y una postcondición. La precondición es una condición que debe cumplirse cuando la operación inicia su ejecución. En caso de no cumplirse, entonces la operación podrá de manera legítima rehusar su ejecución y posiblemente disparar alguna excepción. La postcondición es una condición que debe ser verdadera cuando la operación termina su ejecución. Sí no es verdadera, entonces la implementación de la operación tiene defectos y deberán ser corregidos. Esquemáticamente tendremos (fig. 3.9):

En caso de no cumplirse
Precondición Postcondición No ejecutar operación y disparar excepción. Implementación incorrecta de la operación y se requiere corregirla.

Figura 3.9. Medidas a tomar en caso de una violación a la pre o post condición.

Tomemos por ejemplo la operación Pila.pop, que extrae el elemento que se encuentra en la cima de la pila. La precondición para ésta operación es not vacía. Si la precondición de una operación se cumple, la implementación de la operación debe garantizar que la postcondición de ella se cumpla cuando ésta se termine. Por ejemplo, la postcondición para Pila.pop puede ser (numElementos = old numElementos-1) and not Llena, donde la función old se debe interpretar “cualquier valor que esto tenía antes de que la operación sea ejecutada”.

44

DISEÑO

ORIENTADO

A

OBJETOS

Bertrand Meyer y otros autores describen las pre- y postcondiciones de una operación como un contrato entre una operación y un cliente que envía un mensaje a esa operación [6]. La metáfora del contrato implica que 1. Si el transmisor (sender) del mensaje puede garantizar que la precondición sea verdadera, entonces el receptor (target) encargado de la operación garantizará que la postcondición sea verdadera después de la ejecución. 2. Si, por otro lado, el transmisor del mensaje no puede garantizar que la precondición sea verdadera, entonces el contrato completo se ha violado y es inválido: La operación no está obligada a ser ejecutada y de serlo a garantizar la postcondición. Debemos puntualizar que un invariante de clase es verdadero tanto cuando una operación inicia su ejecución y cuando la termina. Así la historia completa de las pre- y postcondiciones es que una operación está enmarcada (ensandwichada) por las dos condiciones compuestas, tal que: Invariante de clase and Precondición de la operación Ejecución de la operación

Invariante de clase and Postcondición de la operación Como un ejemplo, consideremos una clase Rectangulo, que define cuatro propiedades – puede que no sea la mejor construcción, pero sirve para el ejemplo. La operación que se introducirá es escalaHorizontal y lo que hará es estirar o reducir el tamaño del rectángulo a través de factor real multiplicativo y tendrá un argumento FactorEscala. La ficha de la clase puede quedar de la siguiente manera: Rectangulo props: w1 h1 w2 h2 // ancho inferior // alto izquierdo // alto superior // alto derecho

metodos: escalaHorizontal(real FactorEscala)

45

DISEÑO

ORIENTADO

A

OBJETOS

El invariante de clase para se puede definir: w1 = w2 and h1 = h2. Una precondición para la escalaHorizontal será: AnchoMaximoPermitido ≥ w1 * FactorEscala . Y la postcondición natural será: w1 = (old w1) * FactorEscala. Poniendo todo junto tendremos que: w1 = w2 and h1 = h2 and AnchoMaximoPermitido ≥ w1 * FactorEscala escalaHorizontal(real FactorEscala)

w1 = w2 and h1 = h2 and w1 = (old w1) * FactorEscala La primera línea corresponde a la precondición, la segunda a la operación y la tercera a la postcondición. Es bueno aclarar que algunas personas escriben los invariantes de clase como parte de las pre- y postcondiciones de las operaciones; lo cual eventualmente es tedioso y redundante. Sin embargo, los invariantes de clase están dados implícitamente y uno puede incluirlos en la fase de diseño ya sea dentro de las operaciones o de los métodos que las implementan. En resumen podemos decir que: 1. Todo objeto de una clase dada debe satisfacer los invariantes de clase. Estos invariantes actúan como una constricción, limitando el tamaño del espacio de estados de la clase. Una subclase heredará los invariantes de clase de su(s) superclase(s) y añadirá un conjunto extendido de restricciones propias en general. 2. Cada operación de una clase tendrá una precondición y una postcondición, que en conjunto forman un contrato entre la operación y el cliente de tal operación [6]. Las precondiciones deben cumplirse para ejecutar la operación. Las postcondiciones establecen cuando la operación se ha realizado con éxito. Hablando formalmente, los invariantes de clase son parte de toda precondición y postcondición de una operación. Desde el punto de vista práctico, uno no escribe los invariantes de clase como parte de las pre- y postcondiciones, los aplica implícitamente. 3. Los invariantes de clase, junto con las pre- y postcondiciones de las operaciones forman un marco de trabajo (framework) para el proceso de diseño conocido como Diseño por Contrato, el cual garantiza que la operación del objeto receptor generará una respuesta correcta a un mensaje enviado a él y que el cliente ha obedecido las precondiciones de la operación.

46

DISEÑO

ORIENTADO

A

OBJETOS

4. Conformidad de Tipos y Comportamiento Cerrado.
Una jerarquía robusta de clase es de servicio pesado (heavy-duty) y de una tranquilidad permanente. Los objetos de una clase tendrán un buen comportamiento en relación a los objetos de una superclase, y la jerarquía como un todo soportara los embates del tiempo. Por otro lado, los diseños pobres de la jerarquía de clases tendrán una vida corta, influyendo negativamente en los proyectos realizados y la orientación a objetos. En este capítulo se introducirán un par de principio que son vitales en la construcción de la salud y la fortaleza de las jerarquías de clase. Los principios son la conformidad de tipos y el comportamiento cerrado.

4.1. Clases vs. Tipos. La mejor manera de pensar de una clase es como una implementación de un tipo, que corresponde a una vista abstracta o externa de la clase. De otra manera, los tipos incluyen el propósito de la clase, junto con su espacio de estados y su comportamiento. De manera específica el tipo de una clase esta definido por los siguientes elementos: 1. el propósito de la clase 2. el (los) invariante(s) de la clase 3. los atributos de la clase 4. las operaciones de la clase 5. las precondiciones y postcondiciones 6. las definiciones 7. las firmas

47

DISEÑO

ORIENTADO

A

OBJETOS

Una clase en suma, incorpora un diseño interno que implementa características externas como un tipo. El diseño interno, incluye el diseño de las variables de la clase y el diseño de los algoritmos para los métodos de operación. Claro está que un tipo simple puede ser implementado mediante una serie de clase, donde cada clase tendrá su propio diseño interno particular. Por ejemplo, uno puede crear diversas clases del mismo tipo con el propósito de darle a cada clase sus propias ventajas para hacerla eficiente en algunas circunstancias especiales. De igual forma uno puede diseñar una versión especial de una clase que utilice un algoritmo que corra muy rápido sobre un modelo particular de computadora. Por ejemplo pensemos en diseño de ColeccionOrdenada que implemente por un lado: recorrido y otra que sea muy eficiente para insertar y eliminar elementos. Ambas versiones de ColeccionOrdenada pueden implementar el mismo tipo con los mismos atributos y operaciones. La figura 4.1. muestra en notación UML un tipo, que es idéntico al símbolo de la clase, pero tiene el estereotipo life en el compartimiento de nombre. Cuando los tipos aparecen en un diagrama, uno deberá usar el estereotipo class en los símbolos de clase para distinguirlos con énfasis de los tipos (fig 4.1).
“type” Coleccion {abstract}
addElement(new element){abstract}

“class” ArbolConApuntadores

“implementa a”

“type” Arbol

“class” ArbolConMatricesDispersas

addElement(new element)

“implementa a”

Figura 4.1. El tipo Colección con su subtipo Arbol, que está implementado por dos clases

Las líneas punteadas marcadas con el estereotipo “implementa a” indican que clase es una implementación de que tipo. Como se muestra en la figura 4.1, un tipo simple puede ser implementado por múltiples clases, cada una probablemente con sus características propias de eficiencia Normalmente, éstas clases no tienen nombres tan largos; y sus técnicas de implementación podrán aparecer en sus documentaciones internas.

48

DISEÑO

ORIENTADO

A

OBJETOS

La flecha a la izquierda muestra que el tipo Arbol es un subtipo de colección. El hecho que Colección sea también {abstract} significa que Colección no necesita ser implementada por alguna clase; sólo los subtipos de Colección requerirán implementar clases. En general un tipo representa la vista externa de una clase, el concepto de subtipo es diferente del de subclase. Incluso sin una definición formal de subtipo, uno puede probablemente intuir que las subclases no son subtipos válidos de sus superclases respectivas, como se realizará en el ejemplo siguiente. Dado que uno puede crear cualquier clase sintácticamente como una subclase de cualquier otra, podríamos crear algo como que Elefante herede de Rectangulo o que ReporteDeVentas herede de Ladrillo. Éstas en general son estructuras válidas y legales de clase/subclase, pero semánticamente no tienen sentido, por que las subclases no tienen que ver con sus respectivas superclases. Por ejemplo, no tendrá sentido aplicar el Teorema de Pitágoras a un elefante con el propósito de hallar su hipotenusa. Y por otro lado la operación ComisionDeEnvio no calza con una pieza de material de construcción cuando está esperando un representante de ventas. Así que, siempre que S sea una subclase de T, no se sigue de manera automática que S sea también un subtipo de T. Uno deberá trabajar con el diseño de la clase S si desea hacerlo un subtipo de T. Esto se basará en que S deberá ser un subtipo válido de su superclase T.

4.2 El principio de Conformidad. El principio de diseño que se deriva de la conformidad de tipos viene de la teoría abstracta de tipos de datos, en la cual se fundamenta la orientación a objetos. Éste principio, que es extremadamente importante en la creación de las jerarquías de clases que forman una biblioteca de clases, establece que Si S es un subtipo válido de T, entonces S debe estar en conformidad a T. En otras palabras, un objeto de tipo S puede ser suministrado en cualquier contexto donde un objeto de tipo T sea esperado, y lo consistencia se preservará a pesar de que cualquier operación de acceso al objeto sea ejecutada. Por ejemplo, Circulo es un subtipo de Elipse. Cualquier objeto que es un círculo es también una elipse – es decir una elipse simétrica, redondeada. Así, cualquier operación que espere a una elipse como argumento de un mensaje, no tendrá problemas en recibir un círculo. Lógicamente la operación fallará si la operación intenta achatar al círculo, pues dejará de serlo y ya no cumplirá con su invariante de clase. Como se ha marcado, una subclase y un subtipo corresponden a conceptos distintos, regresando al punto se puede decir que En un diseño orientado a objetos, el tipo de cada clase debe estar en conformidad con el tipo de su superclase. En otras palabras, la jerarquía de herencia para las clases/subclases deberá seguir el principio de conformidad de tipos.

49

DISEÑO

ORIENTADO

A

OBJETOS

La razón que subyace en éste argumento es que con el propósito de fortalecer el polimorfismo, uno debe ser capaz de pasar objetos de una subclase en lugar de objetos de una superclase. ¿Pero como se garantizará que el de tipo de cada subclase realmente y de manera honesta esta en conformidad al tipo de su superclase? La respuesta es la siguiente: Se requiere de dos principios anexos a la conformidad mas: el principio de contravarianza y el principio de covarianza, éstos usarán las nociones de: invariante de clase, pre- y postcondiciones de las operaciones, espacio de estados y comportamiento.

4.2.1. Los Principios de Contravarianza y Covarianza

Estos principios no son simples de exponer y comprender, por lo cual se recomienda una lectura minuciosa y reflexiva. Para garantizar la conformidad de tipos en una subclase, uno primero necesita garantizar que el invariante de la subclase sea al menos tan fuerte como el de la superclase. Por ejemplo sí, Rectangulo tiene el invariante w1 = w2 and h1 = h2. Cuadrado tiene el invariante w1 = w2 and h1 = h2 and w1 = h1. Entonces todo marcha bien, por que el invariante de Cuadrado es más fuerte que el Rectangulo. Puede verse que un objeto que utilice el invariante de Cuadrado estará obligado a utilizar el invariante de Rectangulo, pero un objeto que cumpla con el invariante de Rectangulo no cumplirá en general el correspondiente a Cuadrado. Segundo, uno necesita garantizar las siguientes tres restricciones en la operación a realizarse: 1. Toda operación de la superclase tiene una operación correspondiente en la subclase con el mismo nombre y firma. 2. Toda precondición de una operación no puede ser más fuerte a la de la operación correspondiente en la superclase. Éste se denomina Principio de Contravarianza, denominado así por que la fuerza de las precondiciones de una operación en las subclases van en sentido contrario a la fuerza del invariante de clase. Esto es, las precondiciones de una operación son, de haberlas, más débiles. 3. Toda postcondición de una operación debe ser al menos tan fuerte como la correspondiente a la operación en la superclase. Esto se llama Principio de Covarianza, y es denominado así por que la fuerza de las postcondiciones de una operación en una subclase están en sentido contrario

50

DISEÑO

ORIENTADO

A

OBJETOS

que la fuerza del invariante de clase. Esto es, las postcondiciones de una operación son, de haberlas, más fuertes. Los términos “más fuerte” y “más débil” en las restricciones antes expuestas no describen la calidad o robustez en algún sentido. En general debemos entender que “más fuerte” no se debe considerar “mejor” o “más débil” como algo “peor”. Éstas restricciones se satisfarán trivialmente si una subclase hereda una operación tal cual es de su superclase. En ese caso, el nombre, firma, precondiciones y postcondiciones son idénticas tanto en la superclase como en la subclase. Una situación que se vuelve interesante, es cuando una subclase invalida (override) una operación de una superclase mediante una operación propia. En la sección siguiente se plantea un ejemplo ilustrativo.

4.2.2. Ilustración de la Contravarianza y Covarianza.

La clase Empleado tiene una subclase Administrador, es decir los administradores son una subclase de los empleados. ¿Qué se debe garantizar para que Administrador sea un subtipo válido de Empleado? Primero, Digamos que el invariante de Empleado es escolaridad > 0 y el invariante de Administrador es escolaridad > 20. Esto ocasiona que el invariante de clase de Administrador es más fuerte que el de Empleado, por lo cual hasta aquí todo está en regla con las reglas de diseño. Segundo, consideremos calculaEstimulo como una operación de Empleado. Ésta tomará a EvalRend (Evaluación del Rendimiento) y calculará PorcEstim (Porcentaje de Estímulo), que corresponde a un porcentaje del salario base del empleado (sin prestaciones). En la figura 4.2. se muestra un diagrama en UML para la firma de calculaEstimulo. Empleado
calculaEstimulo(EvalRend)

Administrador
calculaEstimulo(EvalRend) Figura 4.2. Definición de calculaEstimulo para Empleado y Administrador.

Digamos por simplicidad que EvalRend (el argumento que se pasa a calculaEstimulo) es un entero entre 0 y +5. Y que la salida de calculaEstimulo está entre 0 y 10 por ciento.

51

DISEÑO

ORIENTADO

A

OBJETOS

Los algoritmos para calcular los estímulos deberán ser diferentes para los administradores y los que no son administradores. En consecuencia, la clase Administrador puede invalidar calculaEstimulo con una operación implementada por ella (con el mismo nombre y firma). Regresando a la operación calculaEstimulo como fue definida en la clase Administrador. Recordando que para que Administrador esté en conformidad con Empleado, entonces Administrador.calculaEstimulo debe tener una precondición igual a o más débil que Empleado.calculaEstimulo. Esto implica que en particular el rango del argumento de entrada EvalRend de Administrador.calculaEstimulo debe ser igual a o mayor que el rango del argumento de entrada EvalRend de Empleado.calculaEstimulo. Una manera recordar que “mayor rango = condición más débil”, es asociar “mayor” como “flojo” – “suelto” y en contraposición “menor” como “apretado”. Así entonces, cada uno de los siguientes rangos para el argumento de entrada EvalRend para Administrador.calculaEstimulo será legal: 0…5 0…8 -1…9 // igual para las clases Administrador y Empleado // mayor (más débil) en Administrador // mayor (más débil) en Administrador. Suponiendo que una // evaluación negativa tiene sentido. Al contrario, los siguientes rangos para el argumento de entrada EvalRend de Administrador.calculaEstimulo serán ilegales: 1…5 2…4 // menor (más fuerte) en Administrador // todavía menor (más fuerte) en Administrador

En la figura 4.3 se ilustra los ejemplos anteriores, relativos a los rangos para EvalRend.

} }
0 1 2 3 4 5 6

Rangos legales

Rangos ilegales
EvalRend
7 8 9 10 11 12 13 14

Figura 4.3. Contravarianza: posibles rangos para EvalRend en

Administrador.calculaEstimulo(EvalRend)
52

DISEÑO

ORIENTADO

A

OBJETOS

Para que Administrador esté en conformidad con Empleado, la operación calculaEstimulo de Administrador debe tener una postcondición igual o más fuerte que la correspondiente calculaEstimulo de Empleado. Esto significa que en particular, el rango del argumento de salida PorcEstim de Administrador.calculaEstimulo debe ser menor que o igual al rango del argumento de salida PorcEstim de Empleado.calculaEstimulo. Así, cada uno de los siguientes rangos para el argumento de salida PorcEstim de Administrador.calculaEstimulo será legal: 0% … 10% 0% … 6% 2%…4% // igual en ambas clases Administrador y Empleado // menor (más fuerte) en Administrador // todavía menor (más fuerte) en Administrador.

Al contrario, los siguientes rangos para el argumento de salida PorcEstim de Administrador.calculaEstimulo serán ilegales: 0% … 12% -1% … 13% // mayor (más débil) en Administrador // mayor (más débil) en Administrador. Suponiendo que un estímulo // negativo tiene sentido. En la figura 4.4. se muestran los rangos legales e ilegales para PorcEstim de forma pictórica:

}
0 1 2 3 4 5 6 7 8 9 10 11

Rangos legales

}
12 13 14

Rangos ilegales
PorcEstim

Figura 4.4. Covarianza: posibles rangos para PorcEstim en

Administrador.calculaEstimulo(EvalRend)

Los principios de precondiciones débiles y postcondiciones fuertes en las operaciones de las subclases no son intuitivos. Estos principios son importantes en el diseño orientado a objetos cuando uno quiere que la jerarquía de clases implantada siga una jerarquía de tipos correcta.

53

DISEÑO

ORIENTADO

A

OBJETOS

La relevancia de la conformidad de tipos proviene de las demandas del diseño de segundo orden, en el cual un argumento (de un método) en un sistema orientado a objetos, que es un objeto, acarrea “el código genético completo de su clase” junto con él. El diseño de segundo orden aparece cuando los argumentos de un mensaje presentan encapsulación de nivel-2 – en otras palabras, cuando los argumentos de los mensajes son referencias a objetos. En el diseño de primer orden, los componentes de software se comunican pasándose argumentos con nivel-1 de encapsulación (funciones y procedimientos). En el diseño de orden cero, los argumentos son simplemente datos. Se entiende entonces que: El diseño estructurado corresponde a un diseño de orden cero, con algunas ocurrencias de diseño de primer orden cuando se pasan como argumentos funciones o procedimientos.

4.2.3. Ilustración de contravarianza y covarianza.

Supongamos que se quiere conectar una manguera de un diámetro con otra de otro diámetro, para esto resultará útil un cople convertidor – con un diámetro en un extremo (correspondiente a una manguera) y otro diámetro en el otro extremo (correspondiente a la otra manguera). Las mangueras acopladas formarán un nuevo sistema con un atributo nuevo, que no permite un mayor alcance que cada una por separado. Lo cual es conveniente pues resuelve nuevos problemas. Esquemáticamente podemos presentar la situación con la figura siguiente (figura 4.5)

argumentos de entrada actuales

argumentos de entrada formales

argumentos de salida formales

argumentos de salida actuales

invocando operación i

operación invocada T.op

invocando operación i

Figura 4.5. Invocación de la operación op del supertipo T.

Usando la metáfora del convertidor de diámetros para las mangueras, la figura ilustra la conexión del llamado una operación i al llamado de una operación op sobre una clase T. Del lado izquierdo (extremo de entrada), se ve que los argumentos de entrada actuales de i pasan como parámetros a la operación op. Ya que el rango de todo argumento actual de i cae dentro del rango de sus correspondientes argumentos formales en op, los argumentos de entrada serán aceptables para op. Para simbolizar esto, se ha dibujado la entrada del tubo que corresponde a los argumentos formales de entrada mas grande que el correspondiente a los argumentos de entrada actuales de i.

54

DISEÑO

ORIENTADO

A

OBJETOS

A la derecha (extremo de salida), se tiene la situación contraria, como la operación invocada op regresa su resultado. En este caso, el rango de cada argumento formal de salida debe caer dentro del rango de valores
argumentos de entrada actuales argumentos de entrada formales argumentos de salida formales argumentos de salida actuales

invocando operación i

operación invocada S.op

invocando operación i

Figura 4.6. Invocación de la operación op sobre un subtipo S.

aceptables para los argumentos actuales de i correspondientes. Para simbolizar el hecho, se han dibujado los argumentos formales de salida de op como un tubo de menor diámetro que el tubo que corresponde a los argumentos de salida actuales de i. Echando un vistazo a la figura 4.6, se muestra otra operación op, ésta vez definida sobre S, que es una subclase de T. En otras palabras, S.op invalida y redefine T.op. Si S es un subtipo válido de T, entonces las operaciones de S, como es el caso de op, deben obedecer a los principios de contravarianza y covarianza. Se ha simbolizado el principio de contravarianza haciendo que el diámetro del tubo que representa a los argumentos formales de entrada de S.op (al lado izquierdo) sea todavía mayor que el diámetro del tubo correspondiente a los argumentos de entrada formales de T.op. Así, si el tubo de los argumentos formales de entrada de T.op fuera suficientemente grande para aceptar los argumentos actuales de i, entonces el de S.op sería con seguridad suficientemente grande también. En éste contexto se debe entender el concepto de rango grande en vez que el concepto más general de supertipo como una ilustración de precondiciones débiles. En el lado derecho (el lado de salida), otra vez se tiene la situación contraria. Ahora se ha simbolizado el principio de covarianza haciendo que el tubo de los argumentos de salida formales de S.op es todavía menor que el tubo correspondiente a los argumentos de salida formales de T.op. Es decir, si el tubo de los argumentos de salida formales de T.op fuera suficientemente pequeño para coincidir con el tubo de los argumentos actuales de i, entonces el de S.op sería seguramente suficientemente pequeño también. Las figuras anteriores tratan de ilustrar la naturaleza de los principios de contravarianza y covarianza. Para un subtipo que conforma a un objeto del subtipo debe ser aceptable en todo lugar un objeto del supertipo correspondiente, esto es para casar con su supertipo.

4.2.4. Resumen de los requerimientos para la conformidad de tipos.

55

DISEÑO

ORIENTADO

A

OBJETOS

El principio de conformidad exige que para que una subclase S sea un subtipo válido de la clase T, se cumplan las siguientes seis demandas 1. El espacio de estados de S debe tener las mismas dimensiones de T. Pero S puede tener dimensiones adicionales que extienden el espacio de estados de T. 2. En las dimensiones que S y T comparten, el espacio de estados de S debe ser ya sea igual a o caer dentro del espacio de estados de T. Es decir, el invariante de clase de S debe ser igual o más fuerte que el respectivo de T. Para cada operación de T que S invalide y redefina con S.op. 3. S.op debe tener el mismo nombre como T.op. 4. La lista de argumentos de la firma formal de S.op debe corresponder a la lista de argumentos de la firma formal de T.op. 5. La precondición de S.op debe ser igual a o más débil que la precondición de T.op. En particular, cada argumento de entrada formal a S.op debe ser un supertipo de (o del mismo tipo que) el argumento de entrada formal a T.op. Esto establece el principio de contravarianza. 6. La postcondición de S.op debe ser igual a o mas fuerte que la postcondición de T.op. En particular, cada argumento de salida formal de S.op debe ser un subtipo de (o del mismo tipo que) el argumento de salida formal de T.op. Este es el principio de covarianza.

4.3. El Principio de Comportamiento Cerrado. En las secciones anteriores, se ha estudiado el principio de conformidad de tipos. Aunque el respeto a la conformidad de tipos es necesario para ser capaces de diseñar jerarquías de clases de sonido, la conformidad de tipos no es suficiente. Hablando informalmente, la conformidad de tipos por si sola nos llevará a un diseño aplicable solo en situaciones de solo lectura (read-only) para el sonido, esto es, solo ante operaciones de lectura.

En una jerarquía de herencia basada sobre una jerarquía de tipo/subtipo, la ejecución de cualquier operación sobre un objeto de clase C – incluyendo cualquier operación heredada de la(s) superclase(s) de C – debe obedecer al invariante de clase de C. Para manejar situaciones en las cuales se ejecuten operaciones de modificación, se requerirá el Principio de Comportamiento Cerrado. Éste principio pide que el comportamiento heredado por una subclase de una superclase respete el invariante de la subclase. Sin éste principio, uno podrá diseñar subclases con operaciones de modificación que serán propensas a un comportamiento erróneo.

Principio de comportamiento cerrado.

56

DISEÑO

ORIENTADO

A

OBJETOS

Como un ejemplo de éste principio, analicemos cómo el comportamiento definido en una superclase Poligono puede afectar a objetos de una subclase Triangulo. Es importante hacer notar que en el desarrollo de éste ejemplo, en cada uno de los casos expuestos, el objeto involucrado es de la clase Triangulo, pero el comportamiento esta definido por una operación de Poligono. Caso 1. Tomaremos el objeto tr1 como el triángulo izquierdo de la figura 4.7, y la operación perteneciente a Poligono llamada mover, que hace que un objeto se mueva a la derecha un centímetro.

tri1 : Poligono
mover

mover(distanc : longitud)

Fig. 4.7. Operación Poligono.mover, aplicada al triángulo tr1, preserva la “triangularidad”.

Después del movimiento, tr1 sigue siendo un triángulo. Este comportamiento para una instancia de Poligono deja al objeto que estaba en el espacio de estados de Triangulo dentro del mismo espacio de estados. Diremos que la subclase Triangulo es cerrada bajo el comportamiento definido por la operación mover de su superclase. Caso 2. Tomemos otra vez a tr1 como un triángulo. Y consideremos una operación aumentaVer-

tri1 : Poligono
aumentaVertice

aumentaVertice(new Vert : punto)

Fig. 4.8. Operación Poligono.aumentaVertice, aplicada (erróneamente) al triángulo tr1, destruye la propiedad de “triangularidad”.

tice (heredada de Poligono) que incorpora un nuevo vértice al polígono (Figura 4.8.). Después de ésta transición, tr1 ya no será un triángulo: se convertirá en un cuadrilátero. Diremos que la subclase Triangulo no es cerrada bajo el comportamiento definido por la operación aumentaVertice de la superclase Poligono. La cerradura de una subclase bajo el comportamiento de su superclase no se da de manera automática, como se mostró en el caso 2. Uno debe “diseñar dentro”. Es decir, para diseñar una subclase, uno debe de manera explícita y deliberada invalidar ciertas operaciones de la superclase que puedan violar el invariante de la subclase.

57

DISEÑO

ORIENTADO

A

OBJETOS

En el ejemplo, como diseñador de Triangulo, uno debe realizar una de las tres acciones correctivas siguientes: • • • Evitar la Herencia de aumentaVertice, o Invalidar aumentaVertice de tal forma que no tenga efectos ( posiblemente disparando una excepción), o Estar preparado (en la operación aumentaVertice) reclasificar al objeto Triangulo como Rectangulo, si ese comportamiento, que no preserva la cerradura de Triangulo es aceptable en la aplicación.

En general, el diseñador de una clase tiene el deber de garantizar el comportamiento cerrado de la clase. Los diseñadores de otras clases en consecuencia no deberán preocuparse por el mantenimiento del invariante de clase. De cualquier forma no será mala costumbre hacer una revisión de rutina. Si uno está diseñando una clase que envía un mensaje a un objeto invocando una operación de modificación, uno deberá revisar la cerradura del comportamiento de la clase en cuestión. Si uno envía un mensaje suponiendo el caso general (superclase), uno debe estar preparado para esperar un rechazo del mensaje por parte del objeto o simplemente hacer un retorno (return) sin acción alguna. Si este s el problema, entonces antes de enviar el mensaje se requerirá realizar una de las siguientes aciones: • • • Revisar la clase en tiempo de ejecución (run time) con la prueba, o Restringir el polimorfismo sobre la variable que apunta a la prueba, o Diseñar el mensaje bajo la hipótesis que la prueba es de lo más específico, es decir una clase en la parte más baja de la jerarquía – es decir, la clase con las mayores restricciones en su comportamiento.

En resumen podemos decir que el principio de conformidad enuncia que: una clase B es un subtipo de la clase A solo sí un objeto de la clase B será aceptable en cualquier contexto en el cual un objeto de la clase A sea esperado y ninguna operación de modificación sea ejecutada. Por otro lado Si una clase B es una subclase de la clase A, entonces B estará en conformidad con A. Para alcanzar ésta propiedad, uno debe asegurar lo siguiente: 1. El invariante de cada subclase es al menos tan fuerte como el de su superclase

58

DISEÑO

ORIENTADO

A

OBJETOS

2. Cada operación de la superclase tiene una operación correspondiente en la subclase con el mismo nombre y firma; cada precondición en las operaciones de las subclases no son más fuerte que la operación correspondiente en la superclase 3. Y que cada postcondición en las operaciones de las subclases es al menos tan fuerte a la operación correspondiente en la superclase. Los dos últimos principios están vinculados, respectivamente, con el principio de contravarianza y el principio de covarianza, los cuales son muy importantes para determinar las clases correctas en los argumentos de las operaciones de las subclases. Cada clase en una jerarquía de clases deberá obedecer al principio de comportamiento cerrado. Esto obliga a que el comportamiento que una subclase hereda de su(s) superclase(s) respete el invariante de la subclase. El diseñador de subclases logrará esto mediante: 1. La prohibición de la herencia de comportamientos conflictivos. 2. La invalidación de operaciones a heredarse que tengan conflictos de comportamiento. 3. La migración de los objetos de una clase a otra, siempre que se haya violado el invariante de clase, eventualmente se deberá crear una clase especializada en caso de no hallarse alguna a donde migrar el objeto.

Los principios planteados en éste capítulo son importantes en sí y nos guían para un buen diseño.

59

DISEÑO

ORIENTADO

A

OBJETOS

5. Los Peligros de la Herencia y el Polimorfismo.
La herencia y el polimorfismo colocan a la orientación objetos en un lugar aparte respecto a la manera tradicional de la construcción de software. En general aunque la herencia en muy poderosa, es a la vez el mecanismo de construcción de software más sobreutilizado desde la aparición de la instrucción goto de los primeros lenguajes de tercera generación. Los principiantes en el mundo de la programación orientada a objetos sienten que deben diseñar sus aplicaciones usando la herencia en toda oportunidad posible con el propósito de demostrar que son capaces en el mundo de la orientación a objetos. Lo cual resulta en diseños muchas veces difíciles o bien imposibles de implementar. 5.1. Abusos de la Herencia Los siguientes ejemplos han sido tomados de algunos diseños reales [1] y se han adaptado para mostrar algunos de los problemas de la herencia mal manejada. El propósito de ésta sección es examinar diferentes patrones de herencia y prevenir la pérdida de tiempo en los proyectos de software apuntando a entender donde es apropiada e inapropiada la herencia.
5.1.1. Agregados erróneos.

En la parte superior de la figura 5.1.a. se muestra un diagrama de herencia de clase, se puede ver que la clase Avion con sus cuatro supuestas subclases: Ala, Cola, Motor y Fuselaje. Avion

Ala

Cola

Motor

Fuselaje

Fig. 5.1.a. Un Diagrama de herencia para la clase Avion – con un mal diseño implícito.

Éste “diseño” no podrá expresarse como código, pues no existe una manera para programar las subclases que requiere la aplicación. El diseñador ha mezclado los conceptos de herencia de clase y composición de objetos. Si uno hace una lectura del diagrama como se presenta, uno deberá asumir que una cola “es una clase de Avion” y que también un ala “es una clase de Avion”.

60

DISEÑO

ORIENTADO

A

OBJETOS

El diseño que se muestra en la figura 5.1.b. hay herencia múltiple y corresponde a otro supuesto diseño que resuelve la composición de objetos. Ala Cola Motor Fuselaje

Avion
Fig. 5.1.b. Un Diagrama de herencia múltiple para la clase Avion – con otro mal diseño implícito.

El diseñador de éste segundo Avion lee el diagrama como, “Un avión es un ala, una cola, un motor y un fuselaje”. Lo cual suena en general correcto. Sin embargo la manera correcta de interpretar el diagrama es: “Un aeroplano es simultáneamente una clase de ala, una clase de cola, una clase de motor y una clase de fuselaje” Y esto es evidentemente erróneo. Si uno intenta usar éste segundo modelo para generar código, se enfrentará a la situación de que un avión tiene dos alas.
5.1.2. Jerarquía Invertida.

El ejemplo de herencia que se muestra en la figura 5.2. requiere a ojos vista una segunda revisión, debido a que su estructura corresponde de manera exacta a un mapa organizativo normal.
MiembroMesaDirectiva

Gerente

Un empleado reporta a un gerente y un gerente reporta a un accionista. Pero hay un problema. Lo que el diagrama dice es que: “Un empleado es una clase de administrador, y que un administrador es una clase de miembro de la mesa directiva”.

Empleado

Uno puede notar que la realidad es otra, uno espera que “Un miembro de la mesa directiva es una clase de administrador y que un administrador es una clase de empleado”.

Figura 5.2. Todo parece bien ¿ pero es así ?

Para realizar una representación correcta, uno deberá simplemente invertir la figura 5.2 y colocar a Empleado, que es la clase más general, en la parte superior.

61

DISEÑO

ORIENTADO

A

OBJETOS

5.1.3. Confundiendo Clases e Instancias.

El ejemplo de herencia múltiple que se presenta en la figura 5.3. es muy sutil. Cuando uno lo revisa con calma, puede verse que algo anda mal, pero no es inmediato ver qué es lo incorrecto. Oso EspecieEnPeligro

Panda

Fig. 5.3. ¿Qué significa Panda realmente?

Un camino para entender el problema es formularse la siguiente pregunta. ¿Cuáles son las instancias de las tres clases involucradas? En el cuadro siguiente se presentan algunas posibilidades. Panda An – An Ling – Ling Hee – Hee Yogi Teddy Winnie Oso EspecieEnPeligro musaraña de nariz corta orquídea araña de montaña rata gigante de Sumatra

Puede notarse que, dos clases (Panda y Oso) tienen animales individuales como instancias, mientras que la tercera (EspecieEnPeligro) tiene especies enteras como instancias. Es correcto decir que: “Ling – Ling es una instancia de Panda” o “Ling – Ling como Panda, es también una instancia de oso”. Pero no es correcto decir que: “Ling – Ling es una instancia de especie en peligro”. Por lo tanto, la clase Panda puede heredar de Oso, pero no de EspecieEnPeligro. Entonces, ¿ Cómo debemos realizar el diseño para que modele el hecho de que algunas especies están en peligro? En la figura 5.4. se presenta una propuesta. El diagrama del lado izquierdo muestra la clase Panda que hereda de Oso. Un panda es una clase de oso, pero por ser {incomplete} nos dice que, no la única

62

DISEÑO

ORIENTADO

A

OBJETOS

clase de oso. El diagrama de la derecha muestra que EspecieEnPeligro y EspecieSinPeligro heredan de Especies: Las dos clases representan subconjuntos mutuamente exclusivos de la clase completa especies.

Oso

Especies

{incomplete}

{disjoint, complete}

Panda

EspecieEnPeligro
Fig. 5.4. Una jerarquía de herencias corregida

EspecieSinPeligro

Las dos clases a la izquierda tienen que ver con animales, mientras que la clase a la derecha tiene que ver simplemente con especies. Pero ¿ Cómo se ligan los dos lados? ¿ Cómo se diseña el hecho que los pandas y algunas otras especies están en peligro? Un posible diseño puede incorporar un atributo de instancia, estaEnPeligro: boolean, en Oso, o bien en su superclase. A pesar que esto puede funcionar, es una medida excesiva, debido a que produce también mucha generalidad. Por ejemplo, uno estría posibilitado a definir que Yogi esta en peligro, mientras que Teddy no lo está. En la figura 5.5. se incorpora una nueva clase Animal, a la cual se le asigna un atributo especie y una variable interna especie para vincularla con el objeto Especie. Cada subclase de Animal representa una especie (como Oso, Panda, Musaraña, etc), y especie contiene el valor fijo apropiado para objetos de esa clase.

63

DISEÑO

ORIENTADO

A

OBJETOS

Animal
especie : Especie

Especie
nombreTaxonomico : Nombre /estaEnPeligro : Boolean

{incomplete}

Oso
/especie = oso(const)

{disjoint, complete}

EspecieEnPeligro
/estaEnPeligro = true(const) /cuandoInicioPeligro : Date

EspecieSinPeligro
/estaEnPeligro = false(const)

{incomplete}

Panda
/especie = panda(const)

Fig. 5.5. Jerarquía de herencia correcta, con mayor detalle.

Por ejemplo, cada objeto Oso puede dar un valor específico al ser instanciado, o bien cada objeto Oso puede recuperar el valor de su atributo especie de una constante de clase nuestraEspecie: Especie =oso dentro de la clase como un todo. De cualquier manera Oso.especie obtendrá su valor, los objetos de la clase Oso pueden preguntar por sus especies – así como por las propiedades asociadas a sus especies – en tiempo de ejecución con instrucciones tales como: this.especie.estaEnPeligro; this.especie.PesoMaximo; Donde PesoMaximo: Peso, por ejemplo, puede ser un atributo de instancia de Especie y estaEnPeligro: Boolean puede ser un atributo de instancia, por el momento constante, tanto en EspecieEnPeligro como en EspecieSinPeligro, definidas en el estado true y false de manera respectiva.

64

DISEÑO

ORIENTADO

A

OBJETOS

5.1.4. Mal uso de “es un”.

En éste ejemplo se requiere recordar la longitud, ancho y alto de habitaciones – digamos de un hotel – que dentro de la aplicación se tratarán como “cuboides”. Además se requiere conocer el volumen de cada habitación. Suponiendo que se cuenta con la clase Cuboide en la biblioteca de clases, se diseñará un Salon de clases que herede simplemente de Cuboide, la situación se muestra en la figura 5.6. En general el diseño suena bien. Un objeto de la clase Salon puede regresar su volumen simplemente ejeCuboide
/volumen volumen reducir (… ) rotar ( … ) …

Salon

Fig 5.6. Un diseño simple de Salon que hereda de Cuboide.

cutando su operación de recuperación volumen (implementada como una función) que hereda de Cuboide; de donde no se requiere algún código adicional en principio. Además, ya que un salón es un cuboide, el requerimiento es un (is a) para una herencia válida se satisface. Más de una manera sutil es aquí donde el diseño falla. El primer problema es el comportamiento que Salon hereda de Cuboide. Éste comportamiento viene de las operaciones de Cuboide, tales como reducir, rotar, etc. Ya que éstos comportamientos son ilegales para un Salon (al menos en el “mundo real”), lo cual nos obliga a invalidarlos, esto es cancelarlos para Salon. Más se pueden observar problemas mayores cuando uno tiene que tratar con salones de otra geometría. Supongamos que ciertos salones son ovalados. La herencia, que ha trabajado bien para cuboides, también funcionará con óvalos. Así pues, como se muestra en el diagrama de la figura 5.7., Salon hereda de manera múltiple tanto de Cuboide como de Ovalo. ¿Pero qué significa esto? La herencia múltiple implica que un salón es tanto un óvalo y un cuboide. Esto nos llevará a un salón con una forma extraña, y en particular no es exactamente lo que al diseñar uno espe65

DISEÑO

ORIENTADO

A

OBJETOS

raba. Una posibilidad (de primera impresión) sería invertir el diagrama, el modelo se presenta en la figura 5.7 a la derecha.

Cuboide
/volumen volumen

Ovalo
/volumen volumen

Salon
/volumen volumen {cómo esta implementado ??}

Salon
volumen {heredado de dónde ??}

Cuboide
/volumen volumen

Ovalo
/volumen volumen

Fig. 5.7. Dos intentos para introducir salones ovaladas

Parece ser que no se ha logrado algo con la segunda propuesta, ya que la jerarquía en la cual Cuboide u Ovalo heredan de Salon con un esfuerzo mínimo. Si uno instancia un objeto a partir de Cuboide u Ovalo, entonces ese objeto heredará todas las propiedades necesarias para Salon. Pero todavía existe un problema, y éste es el comportamiento extraño (tal como reducir y rotar), que ahora no es posible invalidar (override). El segundo diseño en la figura 5.7. tiene problemas más profundos. Se sabe, del capítulo que abordó el encumbramiento, que no debemos encumbrar una clase de un dominio bajo (Cuboide u Ovalo) con alguna de un dominio alto (Salon). De hacerlo, la biblioteca de geometría desarrollada necesitará la clase Salon para poder operar. Y por otro lado, ya que no todos los cuboides son salones, la clase Cuboide presentará cohesión de mezcla de instancias, así como cohesión de mezcla de dominios. La raíz del problema está como se planteó antes en la afirmación original: “Un salón es un cuboide”, que se utilizó para justificar la herencia. Un requerimiento más preciso es, “ Un salón tiene el atributo de forma. La forma de todos los salones en ésta aplicación es la de un cuboide” Lo cual marca la diferencia. Un nuevo diseño para Salon se muestra en la figura 5.8. Ahora, Salon tiene una variable de instancia forma que hace referencia a un objeto de la clase FormaCerrada3D (o una de sus subclases, tales como Cuboide, Ovalo o Tetraedro). En otras palabras, Salon contiene la declaración, var forma: FormaCerrada3D;

66

DISEÑO

ORIENTADO

A

OBJETOS

Cuando se inicializa un salón particular, la variable forma es asignada a un objeto de la forma y tamaño correcto para un salón dado. La operación Salon.volumen ahora opera preguntando al objeto que hace referencia mediante la variable forma para evaluar el volumen, como muestra la figura 5.8.. La fórmula particular que se utilice para evaluar el volumen del cuboide o el volumen del óvalo, dependerá de la forma actual del salón – definida mediante el mecanismo de polimorfismo.
forma

Salon
/volumen : Volumen volumen : Volumen

FormaCerrada3D
0…* 1 {abstract}
/volumen : Volumen volumen : Volumen {abstract}

Cuboide
/volumen : Volumen volumen : Volumen

Ovalo
/volumen : Volumen volumen : Volumen

Fig. 5.8. Un Salon tiene una forma de classe FormaCerrada3D, y requiere su volumen de forma.

Ésta técnica para acceder al código de otra clase se llama despacho de mensajes (forwarding). Un objeto de la clase Salon despacha el mensaje de volumen a otro objeto de clase Cuboide (o a quién corresponda). El esquema de diseño de envío de mensajes, que es un mecanismo alternativo a la herencia, no garantiza de manera automática el acceso a todas las facilidades de otra clase. En vez de esto, uno tiene que dirigir el acceso, mensaje por mensaje, a los atributos y operaciones de la ora clase que uno requiere usar. Uno puede ver los dolores de cabeza que el esquema original, basado en un diseño de herencia ocasionó y valorar como el modelo de despacho de mensajes lo ha arreglado. Algunos autores usan el término delegación en vez de despacho de mensajes. Más éste último se acostumbra utilizar con mayor frecuencia en el diseño orientado a objetos.

67

DISEÑO

ORIENTADO

A

OBJETOS

5.2. El peligro del Polimorfismo El polimorfismo promueve precisión en la programación orientada a objetos permitiendo que una operación sea definida con el mismo nombre en más de una clase y permitiendo que una variable haga referencia a un objeto de más de una clase. Eso implica que el polimorfismo habilita al ambiente operativo a escoger de manera automática la operación correcta a ejecutar como resultado de un mensaje, sin necesidad de utilizar una sentencia case complicada. Así pues, las operaciones y variables pueden exhibir polimorfismo. En un buen diseño orientado a objetos, éstas dos facetas del polimorfismo trabajan de manera armónica. En un diseño imperfecto, el polimorfismo puede ser un riesgo: Un objeto puede recibir un mensaje que no entienda y puede en consecuencia disparar una excepción en tiempo de ejecución. En ésta sección se discutirán y analizarán algunos detalles peligrosos correspondientes al uso del polimorfismo.
5.2.1. Polimorfismo en las operaciones.

Para explicar el riesgo de enviar mensajes a un objeto que no entienda – y como evitar este riesgo – se requiere introducir algunos términos. El ámbito (scope) del polimorfismo de una operación op es el conjunto de clases sobre las cuales op está definida. Un ámbito del polimorfismo (AdP)que forma una derivación de la jerarquía de la herencia – esto es, una clase A junto con sus subclases – se denomina un como de polimorfismo, con A como el vértice (apex) del polimorfismo. En la figura 5.9. se presenta un árbol de herencia de clase. Si una operación op está definida sobre cada una de las clases sombreadas, entonces éstas clases sombreadas forman el cono de polimorfismo de op (CdP).

Vértice de Polimorfismo (VdP)

Vértice de Polimorfismo (VdP)

Fig. 5.9. La estructura de un CdP.

68

DISEÑO

ORIENTADO

A

OBJETOS

Éste es un como en el cual las clases sombreadas forman una rama completa: la clase A es el vértice de polimorfismo (VdP). Pongamos un ejemplo más concreto, si tomamos la operación ObtenerArea definida sobre Poligono y sobre todas las subclases de Poligono – ya sea localmente o a través de herencia -, entonces el AdP de ObtenerArea forma un cono, teniendo a Poligono en el vértice. La figura 5.10 ilustra la situación.

Poligono
/ ObtenerArea ObtenerArea

Triangulo
/ ObtenerArea ObtenerArea

Rectangulo
/ ObtenerArea ObtenerArea

Hexagono
/ ObtenerArea ObtenerArea

Figura 5.10. El Cono de Polimorfismo (CdP) de ObtenerArea.

tra situación corresponde al caso en el cual una operación no esta definida en todas las clases de una rama de la jerarquía, a é caso se le conoce como un Ámbito de Polimorfismo desigual (ragged). En la figura 5.11 sólo las clases sombreadas contienen la operación, digamos oper. En éste caso no se forma un cono sobre una rama completa.

Fig. 5.11. La estructura de un CdP desigual.

69

DISEÑO

ORIENTADO

A

OBJETOS

Un caso simple y conocido de AdP desigual para una operación puede ser la operación print – la cual asumiremos envía información de cierto objeto a un objeto DriverDeImpresora – la cual tiene un conjunto disperso de clases que requieren de la operación de impresión. Por ejemplo, ésta estará definida en HojaDeClaculo, ProcesadorDeTexto y CorreoElectrónico, pero no en clases del tipo Elefante, ControlTCPIP y Objeto. Por tanto, su AdP no será un cono. Nótese que en la jerarquía Objeto se encuentra en la cima del árbol.
5.2.2. Polimorfismo de Variables.

En general el polimorfismo también se presenta en las variables, las cuales en varios momentos apuntan a objetos que corresponden a diversas clases. Se puede definir el Ámbito del Polimorfismo de forma alterna, ésta vez aplicado los términos a una variable que guarda un manejador de un objeto, en vez de a una operación. El ámbito de polimorfismo de una variable v es el conjunto de clases cuyos objetos son apuntados mediante v (durante el tiempo de vida completo de v). El ámbito de polimorfismo de una variable es similar al ámbito de polimorfismo de una operación. Ambos comprenden a un conjunto de clases. Las clases en el AdP para una variable, de cualquier manera, son aquellas a las que pertenecen todos los objetos a los cuales la variable puede hacer referencia en algún momento durante la ejecución del sistema. Se pueden usar también los términos cono de polimorfismo y vértice de polimorfismo para una variable, en el mismo sentido que se usó para una operación. A continuación se enlistan tres ejemplos que ilustran el ámbito de polimorfismo para una variable. 1. Digamos que la declaración var t:Triangulo permite a la variable t apuntar a cualquier objeto de la clase Triangulo o bien de los descendientes de Triangulo (Esto es natural en lenguajes como Java, Eiffel, o C++, en los cuales el polimorfismo de una variable se restringe comúnmente a los descendientes de una clase). En éste ejemplo, por tanto, el AdP de la variable forma un cono, donde la clase Triangulo se encuentra en el vértice superior del árbol. Digamos que a una variable v l es permitido, en varios momentos, apuntar a un objeto de la clase Caballo, Circulo, o Cliente. ( Esto puede ocurrir con facilidad en Smalltalk, en el cual el polimorfismo de las variables es típicamente sin restricciones; en otros lenguajes, uno deberá declarar a v que pertenezca a la clase más general, Objeto). En éste ejemplo el AdP de la variable no es un cono en el cual las clases Caballo, Circulo, y Cliente no tienen una superclase común inmediata para formar un cono de polimorfismo (CdP). Al menos, se presume (por la disparidad de ellas) que no tienen una superclase común. EN éste ejemplo digamos que (otra vez en Smalltalk) tenemos una declaración var x: Object, donde la clase Object es la cima de la jerarquía de clases. En otras palabras, la variable x puede apuntar a cualquier objeto donde se encuentre (puesto que todas las clases son descendientes de Object). Esta vez el ámbito de polimorfismo de la variable formará un cono. Obviamente éste cono es el más grande de todos, debido a que su vértice es la cima de la jerarquía de herencia de las clases.

2.

3.

70

DISEÑO

ORIENTADO

A

OBJETOS

5.2.3. Polimorfismo en los mensajes

Un mensaje se compone de una variable que apunta a un objeto destinatario y a un nombre de operación que establece la operación a ser invocada. Como se ha discutido, tanto la variable como la operación tienen un AdP. La relación entre estos dos AdPs tiene un impacto significativo sobre la confiabilidad del sistema. Asumiremos que los AdP de la operación y de la variable son conos de polimorfismo (CdPs) – es decir, que forman conos completos. Llamemos al mensaje de prueba ObjDest.ObjOper, donde ObjDest es la variable que apunta al objeto destino y ObjOper es la operación que será invocada sobre el objeto destino. Existen dos posibles parentescos entre el CdP de ObjDest y el CdP de ObjOPer: Caso 1: El CdP de ObjDest cae dentro del CdP de ObjOper. En otras palabras, el CdP de la variable cae dentro del CdP de la operación. Esto se muestra en la figura 5.13.
AdP de ObjOper

AdP de ObjDest

Figura 5.13. El CdP de la variable ObjDest cae dentro del AdP de la operación ObjOper.

Caso 2: Parte o todo del CdP de ObjDest cae fuera del CdP de ObjOper. Es decir, algunos de los CdP de la variable caen fuera de los CdP de la operación. Esto se ilustra en la figura 5.14.
AdP de ObjOper

AdP de ObjDest

Figura 5.14. Parte del CdP de la variable ObjDest cae fuera del AdP de la operación ObjOper.

En el primer caso, todo se puede considerar correcto dentro del diseño. No importa que objeto ObjDest apunte a, aquel objeto será de la clase que “entiende” el mensaje ObjOper. En el segundo caso, se puede

71

DISEÑO

ORIENTADO

A

OBJETOS

decir que no enfrentamos a un diseño pobre y nada robusto. Puede notarse que al ejecutar la aplicación es muy posible que ObjDest apunte a un objeto para el cual su clase no tenga definida ObjOper. Si esto sucede, entonces el programa probablemente explotará (se detendrá) con un error en tiempo de ejecución (run-time error). Siempre que uno quiera utilizar los lujos de los mensajes polimórficos, uno debe asegurarse que exista contención completa del AdP de los mensajes de las variables en el AdP de los mensajes de las operaciones. Uno debe ser especialmente diligente si tanto las variables destino o las operaciones tienen un AdP disperso y desigual.
5.2.4. Polimorfismo y genericidad.

Una clase parametrizada (denominada plantilla de clase en C++) es una clase que toma un nombre de clase como un argumento siempre que uno de sus objetos sea instanciado. Los diseñadores de software frecuentemente utilizan clases parametrizadas para construir contenedores tales como listas, pilas y árboles ordenados. Pero las clases parametrizadas de igual forma que los mensajes polimórficos pueden crear problemas en tiempo de ejecución, debido a conflictos en el ámbito del polimorfismo. Para ilustrar la situación, introduzcamos una clase parametrizada ArbolOrdenado<NodoDeClase>. La siguiente instrucción instancia un árbol ordenado específico: arbolNumReal := ArbolOrdenado<Real>.New; que crea un nuevo objeto, un árbol ordenado al cual apunta arbolNumReal, que almacenará números reales en sus nodos. También se puede escribir arbolClientes := ArbolOrdenado<Cliente>.new; que podrá almacenar objetos de la clase Cliente en sus nodos. Dentro de la clase ArbolOrdenado, uno podrá escribir instrucciones tales como nodo := NodoClase.new; Ésta instrucción creará un nodo nuevo de la clase Real (para el primer objeto ArbolOrdenado anterior) o bien de la clase Cliente (para el segundo objeto ArbolOrdenado). También en el código de ArbolOrdenado (digamos en la operación imprimeArbol), nos gustaría ver algo como nodo.imprime; que enviará un mensaje al objeto que apunta el nodo para que “se imprima a si mismo”. En algún otro lugar del código dentro de ArbolOrdenado, no gustaría realizar comparaciones de la forma: if newItem.lessThan(nodoActual) // newItem y nodoActual apuntan a un objeto NodoClase then …

72

DISEÑO

ORIENTADO

A

OBJETOS

El problema con la comparación anterior es éste: El diseñador de ArbolOrdenado no tiene ni la más mínima idea que clase actual será pasada como un argumento en tiempo de ejecución. Por ejemplo, alguien puede escribir ArbolOrdenado<Fuselaje>.New, ArbolOrdenado<Complex>.New, o ArbolOrdenado<Animal>.New. La primera de las tres clases puede no entender imprime, y la segunda puede no entender lessthan, mientras que la tercera puede no entender ni imprime ni lessThan. Por tanto, existe un tremendo riesgo de fallas en tiempo de ejecución cuando un objeto se almacene en el árbol, por ejemplo de la clase Animal y se le indique “imprímete tu mismo”. El problema se da porque el ámbito de NodoClase es ilimitado.

Con esto terminamos ésta pequeña introducción al diseño orientado a objetos, pero es claro que hay mas que estudiar, aprender y practicar.

73

DISEÑO

ORIENTADO

A

OBJETOS

Bibliograf ía
[1] [2] [3] [4] [5] [6] Page-Jones Meilir. Fundamentals of Object-Oriented Design in UML. Dorset House Publishing, New York: Addison-Wesley, 2000. Page-Jones Meilir. The practical guide to structured systems design, 2nd. ed. Englewood Cliffs, N.J.: Prentice Hall, 1988. Yourdon & Constantine. Structured Design, 2nd ed. Englewood Cliffs, N.J. Prentice-Hall, 1979. Porter H. H.., Separating the Subtype Hierarchy from the Inheritance of Implementation, Journal of ObjectOriented Programming, Vol. 4. No. 9, pags. 20-29, 1992. Lieberherr K.J. & I.M. Holland. Assuring Good Style for Object – Oriented Programs. IEEE Software, Vol 6, No. 9, pags. 38-48, 1989. Meyer, B. Object-Oriented Software Construction. Englewood Cliffs, N.J. Prentice-Hall, 1988.

74

Sign up to vote on this title
UsefulNot useful