Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4. La herencia, más aumento de la ambigüedad Índice 4. La herencia, más aumento de la ambigüedad ....................................................... 103 Sobre el capítulo ................................................................................................... 104 Motivaciones .................................................................................................... 104 Objetivo ............................................................................................................ 104 Contenido ......................................................................................................... 104 4.1 La herencia ................................................................................................... 105 4.2 El polimorfismo ............................................................................................ 106 4.3 La herencia como taxonomía........................................................................ 107 4.4 Curiosidades biológicas de la herencia software .......................................... 108 4.5 La herencia y la evolución (equivocada) del software ................................. 109 4.6 Delegar en vez de heredar ............................................................................ 112 4.7 El principio de sustitución de Liskov ........................................................... 114 4.8 La evolución segura del software ................................................................. 115 4.9 El aporte del principio de sustitución, la ambigüedad .................................. 117 4.10 Condiciones del principio de sustitución ...................................................... 118 4.11 Contraejemplo de la herencia. El cuadrado no es un rectángulo .................. 119 4.12 Las clases abstractas ..................................................................................... 122 4.13 Las clases generales, una solución alternativa, pero… ................................ 124 4.14 Las clases particulares, beneficios y problema ............................................. 126 4.15 La ambigüedad, solución al problema de la diversidad................................ 127 4.16 La ambigüedad es la clave, no la división .................................................... 128 4.17 La herencia múltiple ..................................................................................... 128 4.18 Aproximación al patrón adaptador ............................................................... 131 4.19 La herencia vista desde el código, un ejemplo ............................................. 131 4.20 El polimorfismo en Java ............................................................................... 136 4.21 Ejercicio ........................................................................................................ 143 Una solución ......................................................................................................... 144 4.22 Otro ejercicio ................................................................................................ 147 La búsqueda de la plasticidad, un cambio del diseño ........................................... 148 La solución del rectángulo.................................................................................... 151 Bibliografía ........................................................................................................... 152 103 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Sobre el capítulo Motivaciones La herencia ha sido y es un canto de sirena en los objetos. Quien lo escucha con oídos ingenuos se hunde con el sistema. Parece que la herencia es la fuente de ahorro de código, de la facilidad de modificación y extensión de los programas, pero realmente es lo contrario, salvo que se utilice con el papel y la forma adecuada. El papel adecuado de la herencia es como medio de aumento de la ambigüedad y la forma adecuada de uso es mediante el polimorfismo. Objetivo El objetivo del presente capítulo es que los alumnos comprendan: 1. El concepto de herencia, su papel favorable y sus papeles perjudiciales 2. El concepto de polimorfismo 3. Que es mejor delegar que heredar 4. El principio de sustitución de Liskov, como forma segura y útil de la herencia 5. Las clases abstractas 6. Que la ambigüedad es la clave, no la división Contenido La primera parte profundiza en la herencia, sus aspectos buenos y malos, y recomienda que es preferible delegar que heredar. Se enuncia también el concepto de polimorfismo. La segunda parte estudia con detalle el principio de sustitución de Liskov, sus condiciones y cómo un cuadrado no es un rectángulo, en términos de la herencia software. La tercera parte se dedica a las clases abstractas; a la ambigüedad como solución de la diversidad y, en general, como solución más poderosa que el limitado principio de divide y vencerás. La cuarta parte discute la herencia múltiple, sus efectos nefastos actuales y las restrictivas condiciones donde es favorable, por ejemplo, en el patrón adaptador. 104 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.1 La herencia La herencia es uno de los cantos de sirenas del enfoque de objetos. Se dice mucho de sus favores y muy poco de sus peligros. Por esta causa primero se estudiará la definición y los peligros, y después se verá la forma de obtener los favores de la herencia. Desde el punto de vista formal, [Booch 94] establece que: La herencia es una relación entre clases donde una clase comparte la estructura o comportamiento definido en otra clase (herencia simple) o en más clases (herencia múltiple). La herencia define una jerarquía “es-un” entre clases, en la cual una subclase hereda de una o más clases generalizadas; una subclase típicamente especializa su superclase añadiendo o redefiniendo la estructura y el comportamiento. La Figura 4. 1 ilustra el mecanismo de herencia simple. A a1 a2 a3 herencia superclase m1 m2 m3 C a1 a2 a3 a4 m1 m2 m3 m4 B a1 a2 a3 subclases m1 m2 m3 Figura 4. 1 La herencia En la Figura 4. 1, la clase B comparte las propiedades de la clase A, pero redefine a2 y m2. La clase C, también, comparte los atributos de la clase A, pero añade las propiedades a4 y m4. Las subclases no pueden rechazar ninguna propiedad de sus 105 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad superclases. Se acostumbra a escribir sólo lo nuevo y se omiten las propiedades sin cambio. La relación de herencia se establece desde las subclases hacia la superclase. Es decir, B y C indican explícitamente que son subclases de A en cada una de sus declaraciones. La palabra subclase sólo denota que es una clase que hereda y la palabra superclase sólo denota que es una clase de la que se hereda. La superclase y la subclase reciben también reciben otros nombres, por ejemplo: clase base y clase derivada respectivamente. Desde el punto de vista del código, los objetos de la subclase B son también objetos de la clase A, aunque dos de sus propiedades (a2, m2) tienen cualidades distintas a los objetos de la clase A. De modo semejante, los objetos de la subclase C son también objetos de la clase A. 4.2 El polimorfismo El polimorfismo es una de las cualidades importantes del enfoque de objetos por su aporte de ambigüedad en el diseño. Está asociado con el mecanismo de herencia y permite que la operación definida por una misma cabecera (signatura) sea implementada de maneras distintas. Se denomina polimorfismo a la capacidad de una operación para manifestar un comportamiento diferente dependiendo del objeto que la ejecuta. Por ejemplo, la operación m2 es polimórfica porque su comportamiento depende de si la ejecuta un objeto de la clase A o un objeto de la clase B. Los clientes de una operación polimórfica la invocan a través del mismo mensaje, por ejemplo v.m2, pero el comportamiento depende del objeto que exprese la variable v. A continuación se muestra el pseudocódigo y resultado de una operación que utiliza el servicio polimórfico m2 ofrecido por objetos de las clases A, B y C. v es A declaración de la variable v v ← O:A se asigna a la variable v un objeto de la clase A v:m2 mensaje al objeto asignado a v para que ejecute m2 Se ejecuta la operación m2 de la clase A 106 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad v es A declaración de la variable v v ← O:C se asigna a la variable v un objeto de la clase C v.m2 mensaje al objeto asignado a v para que ejecute m2 Se ejecuta la operación m2 de la clase A porque la clase C lo utiliza sin redefinición. v es A declaración de la variable v v ← O:B se asigna a la variable v un objeto de la clase B v.m2 mensaje al objeto asignado a v para que ejecute m2 Se ejecuta la operación m2 de la clase B porque la clase B ha redefinido m2. v es A declaración de la variable v v ← O:C se asigna a la variable v un objeto de la clase C v.m4 mensaje al objeto asignado a v para que ejecute m2 Equivocación: La clase A carece del método m2. En principio, cualquier operación puede ser polimórfica a través del mecanismo de herencia, salvo las operaciones de creación de objetos que por su tarea específica, no pueden ser polimórficas. Un constructor crea objetos de una clase y no de otra. Más adelante se volverá a tratar este tema. 4.3 La herencia como taxonomía Desde el punto de vista conceptual se podría decir que los objetos de B y C son variantes de A. Esta idea permite asociar la herencia con una relación jerárquica “es un”. Es decir, como un instrumento de clasificación, al estilo taxonómico de las ciencias naturales. Abundan los ejemplos que ilustran la herencia a través de modelos taxonómicos: la clase perro es una subclase de la clase mamífero. Pero, en general, constituye un ejemplo poco afortunado. Primero, porque cualquier clasificación es subjetiva y siempre admite objeciones, de manera que al introducir clasificaciones en el 107 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad software se introducen fuentes de cambio. Segundo, porque las clasificaciones usuales pueden ser contraproducentes para el sistema software como sucede con el clásico ejemplo del cuadrado y el rectángulo, discutido más adelante. 4.4 Curiosidades biológicas de la herencia software El sabor biológico del mecanismo software denominado herencia se hace explícito en su propio nombre. Algunos lenguajes se diferencian y le llaman extensión (“extends”) o implementación (“implements”) según sea el caso. Hay varias curiosidades asociadas con este mecanismo. La herencia software parece un mecanismo “sexuado” puesto que un descendiente puede tener varios progenitores. Vista así, la herencia simple se corresponde con el fenómeno biológico denominado partenogénesis donde un solo progenitor es capaz de crear. Los descendientes tienen, en general, el mismo sexo que el progenitor. En la práctica software es una curiosidad sin trascendencia, aunque de valor nemotécnico como se verá después. Otra curiosidad, pero esta vez clave, de la “herencia” software es que los “progenitores” software desconocen a sus retoños, al revés de la mayoría de los humanos. En el enfoque de objetos, las hijas son las que reconocen a sus madres. La nueva clase se declara hija de alguna clase ya existente como sucede con el pájaro cuco insertado en nido ajeno. En otros casos los recién nacidos reconocen como progenitor a cualquiera que esté presente en el momento de su nacimiento. Si las “madres” software tuviesen que declarar a las hijas, habría que modificar la declaración (código) de la madre cada vez que se creara una hija. Por este motivo, la referencia hereditaria es de descendientes hacia ascendientes, en el software de objetos. La “herencia” software es un reflejo extremo del modelo de Lamarck donde se heredan los cambios somáticos. En la “herencia” software, si al progenitor (superclase) se le quita o cambia una propiedad todos sus descendientes pierden o cambian, inmediatamente, esa propiedad. Lamarck era contemporáneo de Darwin, pero de ideas diferentes. Esta otra curiosidad también es clave en el software. Si la “madre” software adelgaza (se le quita alguna propiedad) todas las descendientes (hijas, nietas,… choznas,…) nacidas o por nacer adelgazarán inmediatamente. En fin que, cualquier 108 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad modificación en los predecesores se reflejará en los futuros sucesores y se propagará a los ¡que ya existen!, hecho que no sucede en ninguna herencia natural. La propagación de los cambios puede parecer una virtud de la herencia, pero realmente es una catástrofe. En primer lugar porque cualquier clase puede ser madre de muchos descendientes sin saberlo; no hay referencia a ellos. La búsqueda de los descendientes es un problema laborioso, sobre todo si los esquemas no existen, están desactualizados o no se dispone de un medio automático para hacerlo. En segundo lugar, porque cualquier cambio afecta a quienes utilizan esa clase y localizar a los afectados es una tarea mucho más difícil de realizar. Mientras más descendientes, mayor será el problema. La situación es equivalente a una epidemia de cambios y fallos. 4.5 La herencia y la evolución (equivocada) del software El símil biológico inspira a usar la herencia como una vía de evolución del software, que se ajusta a las nuevas necesidades, a través de la creación de clases hijas que heredan las propiedades de las clases existente y las modifican o añaden otras propiedades nuevas. Parece que la herencia permite ahorrar código, no tocar lo que ya funciona, y además, permite que se puedan cambiar muchas clases cambiando una sola. Son ideas atractivas, como las sirenas. La Figura 4. 2 muestra un árbol jerárquico de herencia cuya raíz es la clase Madre. De ella derivan las clases Hija 1 e Hija 2, que a su vez tienen hijas: la clase Nieta 1 y Nieta 2, Para compactar la figura, los atributos y los métodos se han separado por comas, aunque cada uno debe ir escrito en una línea, según la notación de UML. 109 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Madre a1,a2,a3 herencia m1,m2,m3 soy hija de Hija1 Hija2 a4 a2 m4 m2 Nieta1 Nieta2 soy hija de a5, a6 m3, m5 m2 Figura 4. 2 Ejemplo de herencia En la figura sólo aparecen los atributos y los métodos añadidos o modificados con respecto a los predecesores. Por ejemplo, la clase Hija 1 añade el atributo a4 y el método m4. La clase Hija 2 modifica el atributo a2 y el método m2, y los declara para diferenciarlos de los predecesores. La clase Nieta 1 añade los atributos a5 y a6, y el método m5. Pero también, modifica el método m3 de la clase Madre, es decir de su abuela. Y por último, la clase Nieta 2 modifica el método m2 de la clase Hija 2, su madre. La descripción ya es confusa. 110 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La facilidad que parece ofrecer la herencia para la evolución del sistema software se puede evaluar en la práctica con la Figura 4. 3, que se ha repetido en la Figura 4. 2 por comodidad. Madre a1,a2,a3 herencia m1,m2,m3 soy hija de Hija1 Hija2 a4 a2 m4 m2 Nieta1 Nieta2 soy hija de a5, a6 m3, m5 m2 Figura 4. 3 Repetición de la Figura 4. 2 Al ver en el código la clase Nieta 2, se puede pensar que sólo tiene la propiedad m2, pero no es así. Como esta clase es hija de Hija 2, habrá que buscar y localizar a la clase madre para saber qué propiedades hereda de ella. Una vez localizada, se sabe que Nieta 2 tiene la propiedad m2 (propia, porque redefine a la madre) y la propiedad a2, según la madre, Hija 2. Pero, la historia continua. Hija 2 es hija de Madre. Entonces, Nieta 2 no está casi vacía como parece. Nieta 2 tiene las propiedades: m2, propia; a2 de Hija 2 (su madre, que a su vez redefine a Madre, la madre de Hija 2). Y tiene también, las propiedades a1, a3, m1 y m3 de Madre porque Hija 2 no las redefine. En fin, con apenas tres clases se pudiera exclamar: “Madre, el drama padre”, título de un delicioso enredo teatral de Jardiel Poncela. Mientras más genealogía, más calvario y mayor probabilidad de equivocaciones. Por otra parte, si se quiere reutilizar alguna clase hija habrá que llevarse a la nueva aplicación, todas las clases antecesoras. 111 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Resumiendo, la herencia es un potente instrumento de trabajo, pero peligroso porque puede ocasionar graves efectos negativos según se ha visto. La herencia ofrece su mayor utilidad y menor riesgo cuando se emplea para ampliar la capacidad de expresar ambigüedad del diseño, según se deriva del principio de Liskov. Para otro tipo de uso es mejor delegar que heredar. 4.6 Delegar en vez de heredar Esta idea es vieja en el software, ya tiene más de quince años. [Rumbaugh 95] utilizó el ejemplo de una pila y una lista, pero se puede usar el ejemplo de cliente y persona. Figura 4. 4 Delegar en vez de heredar Persona Persona Cliente Cliente Figura 4. 4 Delegar en vez de heredar Una situación concreta puede sugerir el diseño de la derecha (herencia) porque los clientes del negocio son personas. Los objetos de la clase Cliente son también de la clase Persona en virtud de la relación “es un” de la clase Cliente hacia la clase Persona. Sin embargo, para ser cliente no es imprescindible ser persona por tanto la relación “cliente es una persona” resulta una relación circunstancial susceptible de cambio. Mañana cliente puede ser una empresa o hasta un plutoniano, ahora que Plutón ha dejado de ser un planeta (el eterno problema de las clasificaciones). 112 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad No conviene utilizar la herencia para relacionar Cliente y Persona por varias razones. Primero, porque es previsible que cambie y cambiará, según Murphy. Segundo, porque los cambios son más engorrosos dadas las fuertes ligaduras entre Cliente y Persona a causa de las propiedades que hereda Cliente de Persona. Tercero, porque conceptualmente es una relación temporal y la herencia expresa una relación permanente. La relación de composición de entre las clases Cliente y Persona (una parte de persona es la situación de cliente) reduce los inconvenientes citados porque una persona siempre puede estar en la situación de cliente, al menos de forma potencial. Por tanto, es una relación más estable, más próxima a la condición de permanencia que expresa la composición, en consecuencia, hay menos posibilidad de cambio en la relación. Además, de producirse cambios, serían menos engorrosos porque la ligadura de Cliente hacia Persona es mucho más débil puesto que no hereda nada. En vez de composición se pudiera usar una relación de asociación, pero la composición enfatiza la pertenencia exclusiva del objeto cliente al objeto persona que lo contiene. De este modo se debe asegurar que el objeto cliente de un objeto persona no se comparta con otro objeto del sistema. Los lenguajes actuales no suministran esta cualidad, si se quiere hay que implementarla. 113 Curso de OO dirigido por la introducción de ambigüedad 4.7 La herencia, más aumento de la ambigüedad El principio de sustitución de Liskov Después del análisis de los muchos problemas de la herencia, el principio de sustitución de Liskov, formulado hace casi dos décadas, ofrece un camino útil y confiable para aprovechar los favores de la herencia. Literalmente el principio define, en términos de una sustitución segura, cuando una subclase es un subtipo de una superclase. “Si para cada objeto O1 de tipo S hay un objeto O2 de tipo T tal que para todos los programas P definidos en términos de T, el comportamiento [interno] de P no cambia cuando O1 es sustituido por O2, entonces S es un subtipo de T.” [Liskov 86] La Figura 4. 5 ilustra la aplicación del principio. Cuando O1:S es sustituido por O2:T ningún objeto, por ejemplo :N, que espera a O2:T se altera si recibe a O1:S. Principio de sustitución de Liskov T :N O1:S m() m() S :N O2:T m() m() S es subtipo de T sii O2:T es sustituido por O1:S, y :N no cambia Figura 4. 5 Ejemplo de aplicación del principio de sustitución de Liskov 114 Curso de OO dirigido por la introducción de ambigüedad 4.8 La herencia, más aumento de la ambigüedad La evolución segura del software Pero lo interesante es ver el principio de sustitución desde otra perspectiva. La herencia permite sustituir un objeto por otro, es decir cambiar una tarea por otra, sin riesgo, siempre que las subclases sean subtipos de las superclases. Si tenemos una clase S que hereda de la clase T, para usar un objeto de la clase S dondequiera que se espere un objeto de la clase T, y que el sistema siga funcionando correctamente, la condición es que S debe ser subtipo de T. Si S no es subtipo de T no se puede asegurar cuál será la consecuencia de la sustitución. La relación de subtipo a tipo admite diseñar un tipo y después especializarlo sin alterar los vínculos del sistema con el tipo. Se facilita la evolución del sistema. Por ejemplo, primero diseñar un tipo A pensando en una tarea general y después, inventar un subtipo B que particularice o especialice esa tarea. O al revés, diseñar un tipo y más tarde convertirlo en un subtipo de otro tipo. La relación de subtipo a tipo facilita un antes y un después sin daños colaterales. Figura 4. 6 P Antes A v es A a1 Op { v ← O:A v.m2 } m2 Evolución P A v es A a1 Op { v ← O:B v.m2 } m2 Después Figura 4. 6 Evolución del software 115 B m2 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La Figura 4. 6 ilustra una línea de evolución de un sistema software. Inicialmente, la clase P está asociada con la clase A (el atributo v es de la clase A) y sus objetos utilizan el servicio m2, a través del mensaje v.m2, dirigido a objetos de A. Después, se quiere que m2 haga otra tarea, relacionada con la anterior pero distinta y se inventa la clase B que redefine m2. Los objetos de la clase P pueden utilizar este nuevo servicio dirigiendo el mismo mensaje v.m2 a los objetos de B. Si la clase B es subtipo de la clase A, entonces es segura la sustitución de los objetos de la clase A por objetos de la clase B. El único reajuste que requiere la operación cliente Op es cambiar la asignación “v ← O:A” por “v ← O:B”. El comportamiento interno de un programa no cambia cuando se sustituye un objeto de un tipo por un objeto del subtipo. Al programa, por ejemplo a los clientes como P, le es indiferente uno u otro objeto, mantiene una relación ambigua con los objetos de tipos y subtipos. Por tanto, se puede modificar la tarea, la función del programa, sin alterar su trabajo. Una vez más se consolida la dirección de la ambigüedad como línea de diseño software para conseguir acomodo a los cambios, para conseguir plasticidad. La Figura 4. 7 ilustra la relación de ambigüedad (indiferencia) de P hacia A. 116 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad B a1 a2 a3 A P v es A Op { … v ← O:? v.m2 … } a1 a2 a3 m1 m2 m3 m1 m2 m3 E a1 a2 a3 m1 m2 m3 Figura 4. 7 Relación de ambigüedad En la figura AAA, la relación de P con el servicio m2 es ambigua porque se establece a través del mensaje v.m2, que no le importa si va dirigido a objetos de A, de B o de E. La clase P está ligada a la clase A, pero puede usar los servicios de todos los subtipos de A, indiferentemente. Se podría decir que P sólo ve a A, los subtipos quedan ocultos detrás del tipo. Los subtipos son los “detalles” que omite la abstracción A. 4.9 El aporte del principio de sustitución, la ambigüedad El aporte del principio de sustitución es señalar el lado “bueno” de la herencia y ofrecer una forma segura de usarlo. Pero, cuál es el lado “bueno” de la herencia porque antes de estudiar el principio sólo se habían visto lados problemáticos. El propio título del principio ofrece la respuesta: el lado bueno de la herencia es su capacidad de producir elementos distintos, pero sustituibles. Es decir, la cualidad favorable de la herencia es su capacidad para elevar la ambigüedad en el diseño y así facilitar el manejo de la complejidad descriptiva y de incertidumbre. La evolución, expansión o modificación del software a través de la herencia debe cumplir las condiciones del principio de sustitución de Liskov para evitar dificultades. 117 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.10 Condiciones del principio de sustitución El cumplimiento del principio de sustitución exige que los métodos de las clases derivadas deban mantener las siguientes relaciones con los métodos de la clase base: 1. La clase derivada debe tener un método correspondiente a cada método de la clase base. Este método puede heredarse directamente de la clase base o sobrescribirse. 2. Cada método de la clase derivada que se corresponda a un método de la clase base debe requerir lo mismo o menos que la clase base. Es decir, si se sobrescribe un método heredado de la clase base, las precondiciones del método deben ser más débiles o permisivas que las del método de la clase base. Dicho de otro modo, si se sobrescribe en una clase derivada un método heredado de la clase base se debe garantizar que el nuevo método funcione en las mismas condiciones y recibiendo los mismos argumentos que el método heredado. El nuevo método no puede ser más restrictivo que el método heredado. 3. Cada método de la clase derivada que se corresponda a un método de la clase base debe garantizar lo mismo o más que la clase base. Es decir, si se sobrescribe un método heredado de la clase base, las postcondiciones del método de las clases derivada deben ser más fuertes o rigurosas que las heredadas de la clase base. Dicho de otro modo, el método de la clase derivada no debe comprometerse a ofrecer mayores resultados o resultados diferentes; sólo debe comprometerse a hacer lo que hace el método de la clase base, garantizando también las propiedades adicionales. Por ejemplo, si un método de la clase base devuelve un número mayor que el argumento que recibe, un método de una clase derivada podría devolver un número primo mayor que el argumento. Pero no estaría permitido que el método de la clase derivada devolviese un número menor o igual que el argumento. 4. Está permitido que la clase derivada introduzca nuevos métodos adicionales que no aparezcan en la clase base. 118 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Las clases derivadas deben garantizar también que se cumplan todas las restricciones definidas sobre los atributos que hereda de la clase base. Por ejemplo, si en la clase base se define un atributo de tipo entero y se establece una restricción para que el atributo sea mayor o igual que cero, la clase derivada debe garantizar que se cumple esta restricción y que el atributo en la clase derivada también tendrá siempre un valor mayor o igual que cero. Como podemos ver, cumplir con el principio de sustitución de Liskov es fácil, siempre que no modifiquemos ni sobrescribamos los atributos y métodos que las clases hijas heredan de sus madres y nos limitemos sólo a añadir nuevos atributos y métodos adicionales en las clases hijas. Si necesitamos modificar en las clases hijas los comportamientos heredados de sus madres, entonces cumplir con el principio de Liskov puede resultar más complicado. Puede incluso, que al tratar de cumplir con el principio, debamos plantearnos si realmente la clase hija debe heredar de la clase madre. 4.11 Contraejemplo de la herencia. El cuadrado no es un rectángulo Un ejemplo clásico de uso peligroso de la herencia es la relación entre cuadrado y rectángulo, analizada por Barbara Liskov en su trabajo sobre el principio. Desde el colegio, todos sabemos que un rectángulo es una figura geométrica de cuatro lados iguales dos a dos. Y que un cuadrado “es un” rectángulo con todos los lados iguales. La herencia se suele considerar como una relación “es un”, por tanto podría parecernos natural que la clase Cuadrado heredase de la clase Rectángulo, ya que, al fin y al cabo un cuadrado es un caso particular de rectángulo. La clase Rectángulo básicamente podría tener un par de atributos, ancho y largo, que son suficientes para definir el rectángulo. Nuestra clase tendría un método establecerTamaño que recibe como argumentos el ancho y el largo del rectángulo que queremos definir. 119 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Rectangulo -int ancho -int largo +establecerTamaño(int ancho, int largo)() Figura 4. 8. Clase Rectángulo Si la clase Cuadrado hereda de la clase Rectángulo, entonces Cuadrado tendrá dos atributos, largo y ancho, y un método establecerTamaño que recibe como argumentos el largo y el ancho del cuadrado. Ni los atributos ni los métodos heredados de Rectángulo resultan muy útiles para la clase Cuadrado, ya que todos sabemos que en un cuadrado el largo y el ancho son siempre iguales. Para poder utilizar los atributos y el método heredado tendremos que añadir algunas restricciones: 1. el atributo largo tendrá siempre el mismo valor que el atributo ancho. 2. el método establecerTamaño tendrá como precondición que el valor del argumento ancho sea igual al valor del argumento largo. En caso contrario se lanzará una excepción o un mensaje de error. 120 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Rectangulo -int ancho -int largo +establecerTamaño(int ancho, int largo)() Cuadrado -int ancho -int largo=ancho +operación1() //precondicion: ancho=largo +establecerTamaño(int ancho, int largo)() Figura 4. 9 Relación de herencia entre cuadrado y rectángulo Preguntémonos ahora que ocurriría si sustituimos la clase Rectángulo por la clase Cuadrado en algún lugar del código donde se use Rectángulo. ¿Podría un cliente de Rectángulo trabajar con Cuadrado y seguir funcionando? La respuesta es no. A menos que el cliente conozca y cumpla las restricciones de Cuadrado, si tratamos de sustituir Cuadrado por Rectángulo el resultado será impredecible. ¿Qué es lo que está ocurriendo en términos del principio de sustitución de Liskov? El principio no se cumple porque los atributos y métodos de Cuadrado son más restrictivos que los de Rectángulo. Cuadrado no es subtipo de Rectángulo. Por tanto, si no queremos tener resultados inesperados no deberíamos utilizar la herencia de Cuadrado a Rectángulo. Herencia que, por otra parte, no aporta ninguna ventaja al diseño, ya que Cuadrado necesita sobrescribir todo lo que hereda de Rectángulo para funcionar. Nuestra intuición nos ha llevado a pensar que Cuadrado debería heredar de Rectángulo porque conceptualmente un cuadrado es un rectángulo. Sin embargo, hemos visto que esta clasificación, a pesar de ser cierta, no aporta ninguna ventaja a nuestro diseño y sí puede traernos serios inconvenientes. La herencia puede ser contradictoria con la clasificación usual. En los diseños orientados a objetos, copiar la realidad o 121 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad dejarse arrastrar por ella, no siempre es una buena idea. Un cuadrado es un rectángulo en geometría, pero la clase Cuadrado no es un subtipo de la clase Rectángulo en el software. 4.12 Las clases abstractas El ejemplo del cuadrado y el rectángulo demuestra que la relación geométrica de tipo y subtipo entre rectángulo y cuadrado no es aplicable a una relación software entre la clase Rectángulo y la clase Cuadrado. El universo software y el universo ajeno al software son universos distintos. En el universo software, si se quiere conseguir el intercambio seguro de objetos cuadrados y objetos rectángulos, hay que aprovechar la relación tipo – subtipo, pero de otra forma: definiendo a las clases Cuadrado y Rectángulo como subtipos (hermanos) de un tipo que se podría denominar Figura. En la Figura 4. 10 se muestra esta relación, ampliada con la clase Paralelogramo (el “supertipo” geométrico de rectángulo y cuadrado, aquí obligado a ser un subtipo más). Figura color pintar ampliar mover área Paralelogramo Cuadrado P largo pintar ampliar mover área p largo ancho p largo ancho ángulo pintar ampliar mover área pintar ampliar mover área Rectángulo Figura 4. 10 Una relación prudente entre las clases Cuadrado, Rectángulo y Paralelogramo La relación de la Figura 4. 10 permite el uso intercambiable de objetos cuadrado, rectángulo y paralelogramo porque todos son subtipos de un tipo. La dificultad de la 122 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad relación es la definición del código de los métodos del tipo Figura. Pero el enfoque de objetos salva esta dificultad, dando otro paso más en su capacidad de expresar ambigüedad: permite que los métodos del tipo sean sólo nombres, nada de código interior. Es decir, que sean una abstracción de los métodos homónimos de los subtipos. Este hecho da lugar a las siguientes definiciones: Se denomina método abstracto al método que sólo expresa la cabecera y carece de código interno. Dicho de otro modo, un método que está declarado pero no implementado [Booch 94]. Por ejemplo, los métodos pintar, mover, ampliar y área, de la clase Figura. También se les llama métodos virtuales o diferidos. En UML los métodos abstractos se escriben con letra cursiva. En el presente texto, la letra cursiva tiene el objetivo de resaltar, no indica abstracción. Se denomina clase abstracta a la clase que contiene al menos un método abstracto porque refleja la abstracción de ese método. Por ejemplo, la clase Figura. También se les llama clases virtuales o diferidas. Mientras que una clase es la definición de un conjunto de objetos, una clase abstracta es la definición de un conjunto de clases. Es una abstracción de abstracciones que eleva la capacidad de expresar ambigüedad del enfoque estructurado. La carencia de código, en al menos un método, impide que existan objetos de una clase abstracta. En UML las clases abstractas se escriben con letra cursiva. En UML se denomina interfaz a una colección de operaciones que tiene un nombre y que se usa para especificar un servicio de una clase. Es un paso más elevado en el camino de la ambigüedad en forma de abstracción. Una interfaz carece de atributos. UML realza la importancia de este tipo de abstracción al darle un nombre propio, pero la idea es anterior y coincide con la idea de clase abstracta pura que utilizan otros autores. El problema de usar el nombre de interfaz es que aumenta la polisemia de esta palabra. Frecuentemente se utiliza la palabra inglesa “interface” como si fuese española. Pero la traducción de la palabra inglesa es interfaz, puesto que “face” significa cara, faz. Por cierto, como la palabra faz es femenina, la palabra interfaz también es femenina y debe ser precedida por el artículo “la”. 123 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La clase Figura contiene solo el atributo color; prescinde de otros atributos que pueden ser comprometedores (restrictivos), por ejemplo, p (posición) y largo. Si se coloca el atributo largo, la clase Figura se compromete sólo con figuras geométricas que tengan algo recto, para que se pueda definir largo. El atributo p (posición) parece más universal porque cualquier figura geométrica en la pantalla debe tener una posición. Es cierto, pero el punto de la figura que determina la posición puede tomar distintos nombres, por ejemplo “vértice superior izquierdo” en los paralelogramos y similares, y “centro” en los círculos. Para facilitar que la clase Figura sea el tipo de todas las clases de figuras conviene prescindir de atributos restrictivos, conviene elevar su capacidad de expresar ambigüedad. 4.13 Las clases generales, una solución alternativa, pero… Una solución alternativa al problema de los cuadrados y rectángulos es aprovechar la clase Rectángulo para que también opere con cuadrados, pero como rectángulos, sin distinguirlos. La clase Rectángulo no diferencia entre cuadrados y rectángulos, pero los humanos podrían hacerlo. Con una sola clase se ha resuelto el problema de los cuadrados y los rectángulos. La solución anterior se puede extender a la clase Paralelogramo y ahorrarse la clase Rectángulo. Ahora, una sola clase sirve para resolver tres problemas: el cuadrado, el rectángulo y el paralelogramo, que son casos particulares unos de otros desde el punto de vista de la geometría. Figura 4. 11 124 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad ángulo ancho largo P Cuadrado Rectángulo Paralelogramo Figura* Figura 4. 11 Generalización de una clase Un paso más allá conduce a la clase polígono irregular (por qué limitarse al regular) que se consigue con reajustes de atributos y métodos (cada vez más generales). Y continuando con la tentación (para qué tantas clases), se podría disponer de una única clase Figura* (para distinguirla de la otra) capaz de operar con cualquier figura geométrica. La Figura 4. 11 ilustra un posible proceso de generalización que conduce a la clase Figura*. Se ha cambiado la disposición usual para realzar el aumento de los atributos a medida que la clase aumenta en generalidad. La confusión que produce la figura es un efecto colateral, que también se produce cuando se trabaja con clases generales. Pero, ¿para qué todo en una sola clase? Una sola clase no es necesariamente más simple que varias clases. La complejidad no se reduce por empaquetarla en un solo elemento. Generalmente sucede al revés. La complejidad de la clase Figura* es mayor que la complejidad total de las clases separadas y además es más embarazosa de manejar. Por ejemplo, para que la clase Rectángulo opere con cuadrados hay que darle un valor al atributo ancho imprescindible para la clase, pero innecesario para el humano. La clase Paralelogramo exigiría además, el ángulo. Otra variante sería definir los atributos de Rectángulo en términos de los vértices de la diagonal y el método pintar, de la clase Rectángulo, pintaría la figura inscrita en 125 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad esos vértices, sin distinguir cuadrado de rectángulo. La responsabilidad de distinguir se traspasa a la persona, cliente de la clase Rectángulo, que debe calcular las coordenadas del otro vértice, tanto para el rectángulo como para el cuadrado. Si se equivoca en la operación de cálculo la figura sale mal. La situación es peor si la clase Paralelogramo se utiliza también para cuadrados y rectángulos. No obstante, puede ser útil disponer de una clase donde la figura se describa a través de vértices o puntos. Comúnmente, mientras mayor sea la generalidad de la clase que realiza la tarea, más se complica su desarrollo, mantenimiento, manipulación, en fin todo. El desarrollo progresivo de una clase como Figura* significa modificar una y otra vez el interior de la clase y a quienes usan la clase. El desarrollo de una vez obliga a enfrentarse a un problema mayor que el problema de enfrentarse cada vez con una figura simple. El mantenimiento para corregir y perfeccionar una clase como Figura* aborda un mecanismo más complejo montado en una sola pieza donde todo está relacionado con todo. De la manipulación ya se ha hablado. Precisa información redundante cuando opera con casos particulares más simples, muchas veces exige trabajo extra de quienes usan la clase y por estas dos causas es más susceptible a equivocaciones. La clase Figura* es una clase concreta que implementa una herramienta general. Es como una piedra que sirve para clavar, cortar, lanzar, dibujar, calzar,…según se use. Hay que soportar un peso innecesario cuando se usa para cortar y hay que tener cuidado de no cortarse, al usar la piedra para clavar. Además, cuando se trate de mejorar alguno de sus usos, se puede perjudicar otro porque todos ellos están muy ligados, en la misma piedra. Los humanos fueron especializando los distintos usos de la piedra en instrumentos particulares. También conviene hacerlo en el software. 4.14 Las clases particulares, beneficios y problema Los instrumentos particulares sólo exigen la información específica que necesitan; facilitan el desarrollo porque se pueden construir y probar uno a uno, o al unísono distribuyendo el trabajo de desarrollo de los distintos instrumentos; se puede modificar cualquiera de ellos sin afectar a los restantes. Incluso, se puede llegar a construir un instrumento muy general, como Figura*, para resolver situaciones muy generales, pero como un instrumento más de uso específico en esas situaciones. Sin 126 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad embargo, a veces todas estas ventajas quedan opacadas por el problema de la diversidad. Sucede con frecuencia en el software. 4.15 La ambigüedad, solución al problema de la diversidad El enfoque de objetos facilita la resolución del problema de la diversidad a través de sus recursos para expresar y manejar ambigüedad. La clase Figura es una abstracción que oculta u omite la diversidad (las clases) pero que viabiliza el acceso a esa diversidad. Figura 4. 12 cliente cliente Figura Figura* ,y3,… X1,y1,x2,y2,x3 Herramienta general Herramientas especializadas Figura 4. 12 Contraste entre soluciones La Figura 4. 12 contrasta del diseño de Figura y sus subtipos con el diseño de la clase Figura*. Ambos diseños muestran una cara uniforme a sus elementos clientes. El primero por ser una abstracción y el segundo por ser una implementación de una herramienta general. La cara de la abstracción es simple porque no se compromete con los detalles, muestra sólo la esencia. La cara de Figura* es compleja porque tiene que expresar toda la información que necesita para resolver un problema complejo: ocuparse de (casi) cualquier figura. La clase Figura es el contexto que permite dirigir la tarea al objeto de clase encargada de una figura específica. El objeto de la clase Figura* es el encargado de realizar la tarea, cualquiera que sea la figura. La clase Figura es una abstracción, mientras que la clase Figura* es un elemento concreto, particular, aunque implemente un mecanismo general. 127 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.16 La ambigüedad es la clave, no la división El contraste de ambos diseños puede sugerir que los beneficios de Figura y sus subtipos deriva de aplicar el principio de divide y vencerás o de su versión software: modularizar. Pero no es así, ni tampoco está asociado con cohesión y acoplamiento. La simplificación se obtiene mediante la especificidad y la introducción de ambigüedad. La clase Figura* es un módulo cohesivo con bajo acoplamiento. Es un módulo bien diseñado. Su división en partes más pequeñas no daría lugar a los subtipos porque la división de un mecanismo general en trozos no produce mecanismos particulares. Incluso, la propia clase Figura* podría ser un subtipo más de Figura. Coexistiría el supuesto todo y las partes al mismo tiempo. La fuente de los subtipos es la especificidad, lo individual y particular de cada figura, que existe posiblemente antes de lo general de cualquier figura. El cuadrado, el círculo y el triángulo existieron como concepto antes que el concepto general de figura. La fuente de la clase Figura es la abstracción, la expresión de lo común a todas las figuras del sistema software. La introducción de este elemento ambiguo es la decisión de diseño que permite homogeneizar la diversidad, disponer de un acceso común a todas las clases particulares. La ambigüedad que expresa la abstracción simplifica la complejidad descriptiva porque reduce la cantidad de elementos que se necesitan para describir el diseño. La existencia de la clase Figura describe que el sistema se ocupa de figuras. Sin esta clase habría que decir que el sistema se ocupa de cuadrados, rectángulos, … Además, la ambigüedad simplifica también la complejidad de incertidumbre porque simplifica el trabajo de reajuste presente y futuro del sistema. 4.17 La herencia múltiple Hasta ahora se ha estudiado la herencia simple de un solo progenitor (partenogenética), pero también existe la herencia de varios progenitores. 128 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Se denomina herencia múltiple a la herencia donde una subclase tiene más de una superclase directa. La Figura 4. 13 ilustra la herencia múltiple. A B a b ma mb C a b ma mb Figura 4. 13 Herencia múltiple Haciendo un guiño a la biología, la herencia es como una reproducción sexuada donde cada progenitor aporta sus cualidades a los hijos. En el caso del modelo orientado a objetos los hijos heredan el contenido completo sus progenitores. Por ejemplo, refiriéndonos a la Figura 4. 13, la clase C hereda todos los atributos y métodos de la clase A y, también, todos los atributos y métodos de la clase B. Algunos textos utilizan este mecanismo para obtener elementos software híbridos. Otros, como recurso de clasificación mixta. Pero, si la herencia simple es peligrosa, la herencia múltiple es múltiplemente peligrosa. Por ejemplo, a menudo se ilustra la herencia múltiple a través de las superclases Barco y Automóvil para obtener la clase Anfibio. Un problema es la distorsión del sujeto que se pretende representar porque el anfibio es barco cuando está en el agua y automóvil cuando está en tierra, mientras que la clase Anfibio es Barco y Automóvil a la vez porque la herencia es un mecanismo estático (en tiempo de compilación). Si el mecanismo hereditario fuese dinámico, en tiempo de ejecución, un objeto pudiera ser de una clase ahora y de otra después. No habría confusiones porque no sería de dos clases en el mismo momento. Aunque los lenguajes comerciales de 129 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad programación orientada a objetos todavía no incorporan una herencia dinámica, es posible conseguir efectos semejantes con algunas técnicas, referidas por [Fowler 97]. Otro problema es solapamiento del contenido “genético” heredado. Por ejemplo, la Figura 4. 14 muestra el resultado de la herencia múltiple de las clases Gallina y Tiburón para conseguir una clase con propiedades de correr y nadar. Gallina Tiburón boca boca correr nadar Gallirón boca boca correr nadar Figura 4. 14 Herencia múltiple con problema. La dificultad de la clase Gallirón está en el atributo boca porque tiene dos especificaciones distintas. Una manera de resolver el solapamiento genético es con la herencia dinámica. Otra, sería poder “marcar” la propiedad que se desea heredar, pero no está permitido en los lenguajes comerciales actuales. Una tercera manera de evitar el solapamiento es evitar que las superclases aporten elementos en posible conflicto. El lenguaje Java adopta esta última solución de forma radical. Sólo permite la herencia múltiple de las denominadas interfaces (clases abstractas puras que contienen porque suprime los atributos y reduce las operaciones a simples declaraciones. La implementación de las operaciones, en clase que hereda, resuelve las colisiones potenciales. Esta modalidad de la herencia múltiple es una forma segura y útil para dotar a una clase con más de una cara (interfaz). 130 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.18 Aproximación al patrón adaptador La doble cara de una clase facilita adaptar sus operaciones a la cara que necesitan otras clases para usar esas operaciones. El denominado patrón adaptador aprovecha el efecto de la doble cara que otorga la herencia múltiple. Figura 4. 15 A B m n i C M G i n m i i i{ invocar n } Diseño para conseguir que n sea otro i Figura 4. 15 Variante del patrón adaptador En la Figura 4. 15, la clase A contiene un método n que se quiere incluir como una implementación más del método i, declarado en la clase abstracta pura B. Entonces, se diseña una clase C que herede de las clases A y B el método n y la declaración del método i respectivamente. Como ambos elementos se han juntado en la clase C, basta con definir que la implementación del método i consiste en invocar al método n para conseguir que n sea una i más. Esta última técnica se denomina envolver. 4.19 La herencia vista desde el código, un ejemplo En capítulos anteriores se diseñaron pequeños sistemas para trabajar con triángulos, círculos y rectángulos. Ahora se quiere integrar estos sistemas en uno solo. A continuación se muestran los diseños de cada clase. 131 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Circulo -int centrox -int centroy -int radio -Color color -boolean haydatos +Circulo() +paint(entrada Graphics g) +mover(entrada int desplazamientox, entrada int desplazamientoy) +ampliar(entrada int zoomout) +reducir(entrada int zoomin) +borrar() FormularioCirculo Figura 4. 16 Clase Círculo Triangulo -int verticesuperiorx -int verticesuperiory -int base -int altura -Color color -boolean haydatos +Triangulo() +paint(entrada Graphics g) +mover(entrada int desplazamientox, entrada int desplazamientoy) +ampliar(entrada int zoomout) +reducir(entrada int zoomin) +borrar() FormularioTriangulo Figura 4. 17 Clase Triángulo Rectangulo -int origenx -int origeny -int base -int altura -Color color -boolean haydatos +Rectangulo() +paint(entrada Graphics g) +mover(entrada int desplazamientox, entrada int desplazamientoy) +ampliar(entrada int zoomout) +reducir(entrada int zoomin) +borrar() Figura 4. 18 Clase Rectángulo 132 FormularioRectangulo Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Las clases Círculo, Rectángulo y Triángulo son distintas entre sí porque tienen atributos distintos y comportamientos distintos: un círculo se pinta de forma distinta que un rectángulo y que un triángulo; también la forma de ampliarlo, reducirlo o moverlo es distinta a la del rectángulo y el triángulo. Pero todas ellas son figuras, por tanto se podrá diseñar una abstracción que exprese la esencia de interés y omita los detalles que las diferencian. La clase Figura, discutida antes, puede ser un punto de partida para este diseño particular. Las cabeceras de las operaciones de cada figura coinciden porque han sido diseñadas con una disciplina, manteniendo un estilo. Si las cabeceras no coincidieran sería posible aplicar una técnica de adaptación para uniformarlas. La coincidencia es necesaria para conseguir que las operaciones sean polimórficas cuando se relacionen con las operaciones homónimas de la clase Figura. Figura 4. 19 Figura -Color color -boolean haydatos +Figura() +paint(entrada Graphics g) +mover(entrada int desplazamientox, entrada int desplazamientoy) +ampliar(entrada int zoomout) +reducir(entrada int zoomin) +borrar() +borrar() Figura 4. 19 Clase abstracta Figura En la nueva clase Figura se ha incluido el método concreto borrar() porque, de inicio, se considera que será común a todos los subtipos, independientemente de la figura. Una figura se borra pidiéndole que se pinte con el color de fondo. La implementación de borrar() estará en la clase madre y las hijas heredarán el código de ella. La presencia de este método hace que la clase Figura sea abstracta, pero no abstracta pura o interfaz. Además, en esta clase se han añadido los atributos color y haydatos que son comunes a todas las figuras. En el diagrama el nombre de la clase Figura está escrito en letra cursiva porque UML usa este tipo de letra para indicar que un elemento es abstracto. También está 133 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad escritos con la misma letra los métodos abstractos o virtuales paint(), mover(), ampliar() y reducir(). La Figura 4. 20 muestra el diseño del diagrama de clases del sistema. Figura 4. 20 Diagrama de clases de Figura polimórfica Entre la clase Ventana y la clase Figura existe una relación de agregación porque la clase Ventana contiene un atributo Figura de tipo Figura. Este atributo será en un momento dado un objeto Círculo, Triángulo o Rectángulo. Pero como todos comparten la misma especificación, la clase Ventana puede tratarlos a todos por igual utilizando la especificación de Figura, sin necesidad de conocer el tipo de figura, excepto en el instante de la creación de los objetos. Un constructor está ceñido a crear objetos de una clase específica, no puede crear objetos de otras clases por su especificidad. La creación de un objeto no es una operación polimórfica. De manera que el objeto creador establece una fuerte relación unívoca hacia el objeto creado. Para debilitar esta relación se utilizan diversas técnicas según sea el caso. Algunas de ellas serán vistas en el curso. La clase Ventana no puede crear objetos de tipo Figura porque Figura es una clase abstracta. La clase Ventana creará objetos de tipo Círculo, Rectángulo y Triángulo según elija, en la pantalla, el usuario del sistema. Por eso hay una relación de 134 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad dependencia de Ventana a cada una de las clases Círculo, Triángulo y Rectángulo. Una vez que el usuario haya elegido la figura se asignará al atributo figura el objeto creado y Ventana podrá trabajar con él a través de la especificación de Figura. Se abre la puerta al uso del polimorfismo. Volviendo al principio de sustitución de Liskov, observamos que en este caso Círculo, Triángulo y Rectángulo son subtipos de Figura ya que podemos sustituir Figura por cualquiera de ellos en el código de Ventana y el resultado será correcto. 135 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.20 El polimorfismo en Java Veamos cómo se implementa el polimorfismo en el lenguaje Java. A continuación mostramos el código de las clases Ventana, Figura, Círculo, Triángulo y Rectángulo del ejemplo anterior. El código del programa completo puede verse en el anexo. import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Ventana extends JFrame implements ActionListener{ Declaramos en ventana el atributo figura de la clase Figura private Figura figura; private JPanel paneloperaciones, panelfiguras; private JButton circulo, rectangulo, triangulo, ampliar, reducir, arriba, abajo, izqda, dcha; public Ventana() { //Pintar la ventana vacía setTitle("Pintar Figuras"); asignarLookAndFeel(); setCloseClick(); setExtendedState(MAXIMIZED_BOTH); configurarGUI(); //Repintar la ventana con la figura pack(); setExtendedState(MAXIMIZED_BOTH); setVisible(true); } private void asignarLookAndFeel() { //Forzar el Look and Feel de la ventana al del sistema String laf = UIManager.getSystemLookAndFeelClassName(); try { UIManager.setLookAndFeel(laf); } catch (UnsupportedLookAndFeelException exc) {System.err.println("Unsupported: " + laf);} catch (Exception exc) {System.err.println("Error cargando: " + laf);} } private void setCloseClick() { //Controlar el cierre de la ventana addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} }); } private void configurarGUI(){ //Crear los paneles de botones de figuras y operaciones 136 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad panelfiguras = new JPanel(); panelfiguras.setLayout(new GridLayout()); paneloperaciones = new JPanel(); paneloperaciones.setLayout(new GridLayout()); //Crear los botones de figuras y añadirlos al panel de figuras circulo=new JButton("Pintar Circulo"); circulo.addActionListener(this); panelfiguras.add(circulo); rectangulo=new JButton("Pintar Rectangulo"); rectangulo.addActionListener(this); panelfiguras.add(rectangulo); triangulo=new JButton("Pintar Triangulo"); triangulo.addActionListener(this); panelfiguras.add(triangulo); //Crear los botones de operaciones y añadirlos al panel de operaciones //Tienen que estar inhabilitados hasta que se haya elegido una figura ampliar=new JButton("Ampliar"); ampliar.addActionListener(this); ampliar.setEnabled(false); paneloperaciones.add(ampliar); reducir=new JButton("Reducir"); reducir.addActionListener(this); reducir.setEnabled(false); paneloperaciones.add(reducir); arriba=new JButton("Mover arriba"); arriba.addActionListener(this); arriba.setEnabled(false); paneloperaciones.add(arriba); abajo=new JButton("Mover abajo"); abajo.addActionListener(this); abajo.setEnabled(false); paneloperaciones.add(abajo); izqda=new JButton("Mover izqda"); izqda.addActionListener(this); izqda.setEnabled(false); paneloperaciones.add(izqda); dcha=new JButton("Mover dcha"); dcha.addActionListener(this); dcha.setEnabled(false); paneloperaciones.add(dcha); //Añadir los paneles de botones: figuras en la parte superior y //operaciones en la parte inferior de la ventana getContentPane().add(panelfiguras,BorderLayout.NORTH); getContentPane().add(paneloperaciones,BorderLayout.SOUTH); } /** Manejador de eventos para controlar los botones */ public void actionPerformed(ActionEvent e) { int zoom=2; int desplazamiento=20; Object boton=e.getSource(); if (boton == circulo){ figura = new Circulo(); ampliar.setEnabled(true); reducir.setEnabled(true); arriba.setEnabled(true); abajo.setEnabled(true); izqda.setEnabled(true); dcha.setEnabled(true); 137 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad } if (boton == rectangulo){ figura = new Rectangulo(); ampliar.setEnabled(true); reducir.setEnabled(true); arriba.setEnabled(true); abajo.setEnabled(true); izqda.setEnabled(true); dcha.setEnabled(true); } if (boton == triangulo){ figura = new Triangulo(); ampliar.setEnabled(true); reducir.setEnabled(true); arriba.setEnabled(true); abajo.setEnabled(true); izqda.setEnabled(true); dcha.setEnabled(true); } if (boton == reducir) figura.reducir(zoom); if (boton == ampliar) figura.ampliar(zoom); if (boton == arriba) figura.mover(0,-desplazamiento); if (boton == abajo) figura.mover(0,desplazamiento); if (boton == izqda) figura.mover(-desplazamiento,0); if (boton == dcha) figura.mover(desplazamiento,0); this.repaint(); } public void paint (Graphics g) { super.paint(g); if (figura!=null) figura.paint(g); } public static void main(String[] args) { new Ventana(); } } Declaramos la abstracta Figura public abstract class Figura extends JPanel{ clase protected Color color; protected boolean haydatos=false; public Figura() { El método paint debería ser abstracto, pero para poder utilizar el API swing necesitamos que esté implementado en todas las clases. } public void paint(Graphics g){ super.paint(g); 138 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad } public abstract void mover (int desplazamientox, int desplazamientoy); public abstract void ampliar (int zoomin); Declaramos los métodos abstractos mover, ampliar y reducir public abstract void reducir (int zoomout); public void borrar(){ //Pintarme del color del fondo de la ventana color= this.getBackground(); repaint(); } } Declaramos Círculo que Figura public class Circulo extends Figura{ //Coordenada x del centro private int centrox; la clase hereda de //Coordenada y del centro private int centroy; //Radio private int radio; //Crea una nueva instancia de Circulo public Circulo() { // Mostrar el formulario para obtener los datos del circulo FormularioCirculo formulario= new FormularioCirculo(); //JDialog dialog = new JDialog(this, "Introduzca los datos del circulo", true); JDialog dialog =new JDialog(); dialog.setTitle("Introduzca los datos del circulo"); dialog.setModal(true); dialog.setContentPane(formulario); dialog.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE); dialog.pack(); dialog.show(); // Obtener los datos introducidos por el usuario centrox=formulario.obtenerCentrox(); centroy=formulario.obtenerCentroy(); radio=formulario.obtenerRadio(); color=formulario.obtenerColor(); haydatos=true; } public void paint (Graphics g) { super.paint(g); //Si el usuario ha introducido los datos pinta el circulo if (haydatos){ g.setColor(color); g.drawOval(centrox-radio, centroy-radio,2*radio,2*radio); g.fillOval(centrox-radio, centroy-radio,2*radio,2*radio); g.dispose(); } } 139 Implementamos el método paint Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad public void mover (int desplazamientox, int desplazamientoy){ Implementamos el método mover centrox=centrox+desplazamientox; centroy= centroy+desplazamientoy; } } public void ampliar (int zoomin){ Implementamos el método ampliar if (zoomin > 0){ radio=radio*zoomin; } } Implementamos el método reducir public void reducir (int zoomout){ if (zoomout > 0){ radio=radio/zoomout; } } Declaramos la clase Rectángulo que hereda de Figura public class Rectangulo extends Figura{ //Coordenada x del vertice superior izquierdo private int origenx; //Coordenada y del vertice superior izquierdo private int origeny; //Base private int base; //Altura private int altura; //Crea una nueva instancia de Rectangulo public Rectangulo() { // Mostrar el formulario para obtener los datos del rectangulo FormularioRectangulo formulario= new FormularioRectangulo(); JDialog dialog =new JDialog(); dialog.setTitle("Introduzca los datos del rectangulo"); dialog.setModal(true); dialog.setContentPane(formulario); dialog.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE); dialog.pack(); dialog.show(); // Obtener los datos introducidos por el usuario origenx=formulario.obtenerOrigenx(); origeny=formulario.obtenerOrigeny(); base=formulario.obtenerBase(); altura=formulario.obtenerAltura(); color=formulario.obtenerColor(); haydatos=true; } Implementamos el método paint public void paint(Graphics g) { super.paint(g); //Si el usuario ha introducido los datos pinta el rectangulo if (haydatos){ g.setColor(color); 140 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad g.drawRect(origenx,origeny,base,altura); g.fillRect(origenx,origeny,base,altura); g.dispose(); } } public void mover (int desplazamientox, int desplazamientoy){ origenx=origenx+desplazamientox; origeny=origeny+desplazamientoy; Implementamos el método mover } public void ampliar (int zoomin){ Implementamos el método ampliar if (zoomin > 0){ base= base * zoomin; altura=altura*zoomin; } } public void reducir (int zoomout){ Implementamos el método reducir if (zoomout > 0){ base= base / zoomout; altura=altura / zoomout; } } } Declaramos la clase Triángulo que hereda de Figura public class Triangulo extends Figura{ //Coordenada x del vertice superior private int verticesuperiorx; //Coordenada y del vertice superior private int verticesuperiory; //Base private int base; //Altura private int altura; // Crea una nueva instancia de Triangulo public Triangulo () { // Mostrar el formulario para obtener los datos del triangulo FormularioTriangulo formulario= new FormularioTriangulo(); JDialog dialog =new JDialog(); dialog.setTitle("Introduzca los datos del triangulo"); dialog.setModal(true); dialog.setContentPane(formulario); dialog.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE); dialog.pack(); dialog.show(); // Obtener los datos introducidos por el usuario verticesuperiorx=formulario.obtenerVerticeSuperiorx(); verticesuperiory=formulario.obtenerVerticeSuperiory(); base=formulario.obtenerBase(); altura=formulario.obtenerAltura(); color=formulario.obtenerColor(); 141 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad haydatos=true; } Implementamos el método paint public void paint(Graphics g) { int [] coordenadasx=getCoordenadasX(); int [] coordenadasy=getCoordenadasY(); super.paint(g); //Si el usuario ha introducido los datos pinta el triangulo if (haydatos){ g.setColor(color); g.drawPolygon(coordenadasx, coordenadasy, 3); g.fillPolygon(coordenadasx, coordenadasy, 3); g.dispose(); } } private int [] getCoordenadasX(){ int [] coordenadasx = new int [3]; coordenadasx[0]=verticesuperiorx; coordenadasx[1]=verticesuperiorx-(base/2); coordenadasx[2]=verticesuperiorx+(base/2); return coordenadasx; } private int [] getCoordenadasY(){ int [] coordenadasy= new int[3]; coordenadasy[0]=verticesuperiory; coordenadasy[1]=verticesuperiory+altura; coordenadasy[2]=verticesuperiory+altura; return coordenadasy; } public void mover (int desplazamientox, int desplazamientoy){ Implementamos el método mover verticesuperiorx = verticesuperiorx + desplazamientox; verticesuperiory = verticesuperiory + desplazamientoy; } public void ampliar (int zoomin){ Implementamos el método ampliar if (zoomin > 0){ base= base * zoomin; altura=altura*zoomin; } } Implementamos el método reducir public void reducir (int zoomout){ if (zoomout > 0){ base = base / zoomout; altura = altura / zoomout; } } } 142 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.21 Ejercicio Reutilice el la solución de figuras geométricas para dibujar una cara. El aspecto de la cara se muestra a continuación. Puede suponer que el contorno de la cara es un círculo, que los ojos también son círculos y que la boca es un rectángulo muy estrecho. 143 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Una solución Pensar en objetos es asociar a un objeto la tarea de dibujar la cara. Sería un objeto compuesto de otros objetos; uno por cada componente de la cara. Por ejemplo: contorno, es un objeto de la clase Círculo asociado con el contorno de la cara. ojoderecho y ojoizquierdo, son dos objetos de la clase Círculo asociados con los ojos. boca, es un objeto de la clase Rectángulo asociado con la boca. Además conviene considerar como atributos del objeto Cara centrox y centroy, son dos números enteros para indicar las coordenadas del centro de la cara. Servirán para situar la cara en la pantalla. tamanyo, un número entero para indicar el tamaño de la cara. color, un atributo de tipo Color (tipo predefinido en Java) que indica el color de la cara. La cara estará contenida en una ventana, igual que en las figuras geométricas, porque se está trabajando en un entorno de ventanas (windows). La Figura 4. 21 muestra el diagrama de clases. 144 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Cara Ventana -Cara cara +Ventana() -asignarLookAndFeel() -configurarGUI() -setCloseClick() +paint(entrada Graphics g) +main(entrada String[] args) -Circulo contorno -Circulo ojoderecho -Circulo ojoizquierdo -Rectangulo boca -int centrox -int centroy -int tamanyo -Color color +Cara() +paint(entrada Graphics g) Figura Circulo Rectangulo Triangulo Figura 4. 21 Diagrama de clases de Pintar Cara Las relaciones de agregación entre las clases Cara, Círculo y Rectángulo expresan que los objetos de la clase Cara están compuestos por objetos de las clases Círculo y Rectángulo. Estas relaciones reflejan el tipo de los atributos contorno, ojoderecho, ojoizquierdo y boca. Veamos a continuación el código de la clase Cara escrito en el lenguaje Java public class Cara extends JPanel{ //extendsJPanel es necesario para usar el APi swing private Circulo ojoderecho, ojoizquierdo; //ojos private Rectangulo boca; //boca private Circulo contorno; //contorno private int tamanyo=200; //tamaño de la cara private int centrox=500; //coordenada x del centro de la cara private int centroy=350; //coordenada y del centro de la cara private Color color=Color.YELLOW; //color de fondo de la cara // Crea una nueva instancia de Cara public Cara() { //crear el contorno contorno = new Circulo(centrox, centroy, tamanyo, color); //crear los ojos ojoizquierdo = new Circulo(centrox-(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); ojoderecho = new Circulo(centrox+(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); //crear la boca boca = new Rectangulo(centrox-(tamanyo/4), centroy+(tamanyo/2), tamanyo/2, 2, Color.BLACK); } 145 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad public void paint (Graphics g) { super.paint(g); //necesario para utilizar el API swing //pintar el contorno contorno.paint(g); //pintar los ojos ojoderecho.paint(g); ojoizquierdo.paint(g); //pintar la boca boca.paint(g); } } La solución pinta la cara reutilizando la estructura de figuras desarrollada anteriormente. La Figura 4. 22 muestra el resultado de la ejecución del programa. Figura 4. 22 Resultado de Pintar Cara 146 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad 4.22 Otro ejercicio La cara que pintamos no les gustó a los niños y propusieron que el contorno fuese un cuadrado. Haga las modificaciones necesarias en el diseño para satisfacer la nueva solicitud. De paso, evalúe la facilidad que ofrece su diseño anterior para ajustarse a este cambio. Una solución posible, consiste en reutilizar el diseño anterior modificando el atributo contorno de la clase Cara para que sea un objeto de la clase Rectángulo en lugar de Círculo. Como ambas clases, Rectángulo y Círculo, tienen un método llamado paint(), no sería necesario modificar el método paint() de Cara, ya que el mensaje contorno.paint() sigue siendo válido. 147 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La búsqueda de la plasticidad, un cambio del diseño El cambio o reajuste de requisitos es un fenómeno muy frecuente en el software, tanto que resulta ser uno de los problemas más importantes (y también rentables) del desarrollo de software. La solución anterior al ejercicio de las caras resolvió la situación de modo directo, sin pensar más allá, porque lo que no se sabe, no se sabe. De lo contrario en el software se contratarían oráculos, videntes, cualquiera que sea capaz de anticiparse (con exactitud), pero no dan resultado. La solución anterior, sin otro conocimiento, está bien. Ahora han variado las condiciones y ya se sabe que se quiere modificar el contorno. Pero, si hay cambios en el contorno podrá haber cambios en cualquier otro atributo de la cara. Entonces, conviene diseñar una solución de mayor plasticidad (deformable), que admita como componente de la cara cualquier figura. Se trata de introducir ambigüedad en el diseño. Un paso en esa dirección es asignar los atributos contorno, ojoizquierdo, ojoderecho y boca de tipo Figura, usando la palabra tipo en el sentido estricto de Liskov. La intención es aprovechar la posibilidad de polimorfismo que ofrecen las operaciones de la clase Figura. El diseño de los componentes de la cara como objetos de la clase Figura introduce suficiente ambigüedad para componer la cara con cualquier figura de las disponibles actualmente o en el futuro. Otro paso que aumenta la plasticidad del diseño es agrupar los componentes de la cara en un vector (facciones) con la finalidad de facilitar la uniformidad del tratamiento. El uso de un vector de componentes añade ambigüedad al diseño porque los nombres de los componentes se sustituyen por ordinales: el primer componente, el segundo, etc. Gracias a esta ambigüedad se gana en igualdad de proceso y en facilidad de modificación. Pero la ambigüedad también puede dificultar la comprensión por un efecto de desconcierto. Casi nada es gratis. Para reducir el posible efecto negativo de la ambigüedad se han utilizado variables temporales con los nombres de los componentes. Sin embargo, a pesar de toda la ambigüedad introducida en el diseño, se mantienen dependencias directas de Cara hacia Círculo y Rectángulo a causa de las 148 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad operaciones de creación de objetos. Sólo cuando los creamos necesitamos declarar su tipo específico Círculo y Rectángulo (recordemos que no pueden crease objetos de clases abstractas como Figura). En la aproximación a los patrones se verá una solución que reducen estas ligaduras. Una vez creados los objetos podemos tratarlos a todos por igual usando los métodos de la clase Figura. Por eso ahora, en el método paint() de Cara utilizamos un objeto de tipo Figura para recorrer el vector facciones y pintar cada uno de sus elementos. Como todos los elementos de facciones son de tipo Figura siempre podremos llamar al método paint() del elemento concreto utilizando la cabecera que heredan todos de Figura. La Figura 4. 23 muestra el diseño del diagrama de clases de esta otra nueva solución de más plasticidad. Ventana -Cara cara +Ventana() -asignarLookAndFeel() -configurarGUI() -setCloseClick() +paint(entrada Graphics g) +main(entrada String[] args) Cara -Figura [ ] facciones -int centrox -int centroy -int tamanyo -Color color +Cara() +paint(entrada Graphics g) Figura Circulo Rectangulo Triangulo Figura 4. 23 Diagrama de clases de Pintar Cara polimórfico El diseño de la Figura 4. 23 aumenta la facilidad de extensión y modificación del sistema (plasticidad) pero conserva la función del sistema; todavía los contornos de las caras son círculos. Este ejemplo ilustra la siguiente definición: 149 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad Se denomina factorización a las modificaciones que se realizan en los diseños para mejorar alguna cualidad interna del sistema sin variar sus funciones. Veamos el código Java de esta nueva clase Cara. public class Cara extends JPanel{ private Vector facciones; //Vector de objetos Figura que contiene las facciones de la cara: //contorno, ojoderecho, ojoizquierdo y boca private int tamanyo=200; //tamaño de la cara private int centrox=500; //coordenada x del centro de la cara private int centroy=350; //coordenada y del centro de la cara private Color color=Color.YELLOW; //color de fondo de la cara /** Crear una nueva instancia de Cara */ public Cara() { Figura contorno, ojoizquierdo, ojoderecho, boca; facciones = new Vector(); //crear el contorno y añadirlo a las facciones contorno = new Circulo(centrox, centroy, tamanyo, color); facciones.add(contorno); //crear los ojos y añadirlo a las facciones ojoizquierdo = new Circulo(centrox-(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); facciones.add(ojoizquierdo); ojoderecho = new Circulo(centrox+(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); facciones.add(ojoderecho); //crear la boca y añadirlo a las facciones boca = new Rectangulo(centrox-(tamanyo/4), centroy+(tamanyo/2), tamanyo/2, 2, Color.BLACK); facciones.add(boca); } public void paint (Graphics g) { Figura figura; super.paint(g); //lo utilizamos para pintar la ventana de Windows //pintamos los elementos del vector facciones Iterator i=facciones.iterator(); while (i.hasNext()){ figura= (Figura) i.next(); figura.paint(g); } } } 150 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La solución del rectángulo Dada la plasticidad del diseño nuevo, el cambio del contorno de la cara se resuelve sólo modificando la línea en la que creamos el objeto contorno para crear un Rectángulo en lugar de un Círculo. Veamos este cambio sobre el código. public class Cara extends JPanel{ private Vector facciones; //Vector de objetos Figura que contiene las facciones de la cara: //contorno, ojoderecho, ojoizquierdo y boca private int tamanyo=200; //tamaño de la cara private int centrox=500; //coordenada x del centro de la cara private int centroy=350; //coordenada y del centro de la cara private Color color=Color.YELLOW; //color de fondo de la cara /** Crear una nueva instancia de Cara */ public Cara() { Figura contorno, ojoizquierdo, ojoderecho, boca; facciones = new Vector(); //crear el contorno y añadirlo a las facciones contorno = new Rectangulo(centrox-tamanyo,centroy-tamanyo,2*tamanyo,2*tamanyo,color); facciones.add(contorno); //crear los ojos y añadirlo a las facciones ojoizquierdo = new Circulo(centrox-(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); facciones.add(ojoizquierdo); ojoderecho = new Circulo(centrox+(tamanyo/3), centroy-(tamanyo/4), 10, Color.BLACK ); facciones.add(ojoderecho); //crear la boca y añadirlo a las facciones boca = new Rectangulo(centrox-(tamanyo/4), centroy+(tamanyo/2), tamanyo/2, 2, Color.BLACK); facciones.add(boca); } public void paint (Graphics g) { Figura figura; super.paint(g); //lo utilizamos para pintar la ventana de Windows //pintamos los elementos del vector facciones Iterator i=facciones.iterator(); while (i.hasNext()){ figura= (Figura) i.next(); figura.paint(g); } } } 151 Curso de OO dirigido por la introducción de ambigüedad La herencia, más aumento de la ambigüedad La Figura 4. 24 muestra el resultado de la ejecución del programa. Figura 4. 24 Resultado de Pintar Cara Bibliografía [Booch 94] Grady Booch, “Object Oriented Analysis and Design” Ed. Benjamin Cummings Publishing, 1994 [Rumbaugh 95] James Rumbaugh et al. “Modelado y Diseño Orientado a Objetos” Ed. Prentice Hall, 1995 [Liskov 86] Barbara Liskov et al. “Abstraction and Specification in Program Development” Cambridge, MA: The MIT Press, 1988 [Fowler 97] Martin Fowler, “Analysis Patterns” Ed. Addison-Wesley, 1997 152