Considerando que la lección anterior introduce los conceptos fundamentales de la programación orientada a objetos, esta lección presenta más detalles acerca de la idea de orientación a objetos. Esta sección es adoptada principalmente de [2].
En el ejercicio 3.6.5 tu ya has investigado relaciones entre los tipos de datos abstractos y sus instancias, y las has descrito en tus propias palabras. Vayamos aquí más al detalle.
Considera que has escrito un programa para dibujar. Este programa debería permitir el dibujo de variados objetos tales como puntos, rectángulos, triángulos y muchos más. Por cada objeto, tu provees una definición de clase. Por ejemplo, la clase Point define un punto por sus coordenadas :
class Point { attributes: int x, y methods: setX(int newX) getX() setY(int newY) getY() }
Tú continúas definiendo clases de tu programa de dibujo con una clase para describir círculos. Un círculo define un punto central y un radio :
class Circle { attributes: int x, y, radius methods: setX(int newX) getX() setY(int newY) getY() setRadius(newRadius) getRadius() }
Comparando ambas definiciones de clase, podemos observar lo siguiente :
Conociendo las propiedades de la clase Point podemos describir un círculo como un punto más un radio más métodos para accesarlo. Así, un círculo es "de-la-especie" Point. Sin embargo, un círculo es algo más "especializado". Ilustramos ésto gráficamente en la Figura 5.1.
En ésta y en las siguiente figuras, las clases se dibujan usando rectángulos. Su nombre siempre empieza con una letra mayúscula. Las flechas indican la dirección de la relación, de ahí que se deba leer como "Circle es de-la-especie Point".
La relación anterior se usa al nivel de clase para describir las relaciones entre dos clases similares. Si creamos objectos de tales clases, nos referimos a su relación como una relación "es-un(a)".
Desde el momento que la clase Circle es de la especie de la clase Point, una instancia de Circle, digamos acircle, es un point. Consecuentemente, cada círculo se comporta como un punto. Por ejemplo, tú puedes mover puntos en la dirección x al alterar el valor de x. Similarmente, tú mueves círculos en ésta dirección al alterar su valor de x.
La Figura 5.2 ilustra esta relación. En ésta y en las siguientes figuras, los objetos se dibujan usando rectángulos con las esquinas redondeadas. Su nombre consiste solamente de letras minúsculas.
Algunas veces necesitas poder construir objetos haciendo una combinación de otros. Tú ya sabes ésto por la programación procedimental, donde tú tienes la estructura o registro para juntar variados tipos de datos.
Regresemos a nuestro programa de dibujo. Tú ya has creado varias clases para las figuras disponibles. Ahora decides que quieres tener una figura especial que representa tu propio logotipo que consite en un círculo y un triángulo. (Asumamos que tú ya tienes definida un clase Triangle.) De este modo, tu logo consiste en dos partes o, el círculo y el triángulo son parte-de tu logotipo:
class Logo { attributes: Circle circle Triangle triangle methods: set(Point where) }
Ilustramos ésto con la Figura 5.3.
Esta relación es justamente la inversa de la relación parte-de. Por lo tanto, podemos fácilmente añadir esta relación a la ilustración parte-de añadiendo flechas en la otra dirección (Figura 5.4).
Con la herencia podemos hacer uso de las relaciones de-la-especie y es-un(a). Como se describió anteriormente, las clases que son de-la-especie de otra clase comparten propiedades de esta última. En nuestro ejemplo con el punto y el círculo, podemos definir un círculo, el cuál hereda de punto:
class Circle inherits from Point { atrributes: int radius methods: setRadius(int newRadius) getRadius() }
La clase Circle hereda todos los elementos de datos y métodos de la clase Point. No hay necesidad de definirlos dos veces : Solamente usamos los ya existentes (y familiares) datos y definiciones de métodos.
Al nivel de objeto ahora podemos usar un círculo justamente como habríamos usado un punto, debido a que un círculo es-un(a) punto. Por ejemplo, podemos definir un objeto círculo y establecer sus coordenadas del punto central :
Circle acircle acircle.setX(1) /* Heredado de Point */ acircle.setY(2) acircle.setRadius(3) /* Añadido por Circle */
"Es-un(a)" también implica que podemos usar un círculo en cualquier circunstancia donde se pueda usar un punto. Por ejemplo, se puede escribir una función o un método, digamos move(), el(la) cuál debe mover un punto en la dirección x:
move(Point apoint, int deltax) { apoint.setX(apoint.getX() + deltax) }
Debido a que círculo hereda de punto, tu puedes usar esta función con un argumento círculo para mover su punto central y, a partir de ahí, todo el círculo :
Circle acircle ... move(acircle, 10) /* Mover el círculo al mover */ /* su punto central */
Tratemos de formalizar el término "herencia" :
Definición (Herencia)
Herencia es el mecanismo que permite que un clase A herede
propiedades de una clase B. Decimos "A hereda de B". Objetos de la clase A
tienen así acceso a los atributos y métodos de la clase B sin necesidad de redefinirlos.
Definición (Superclase/Subclase) Si la clase A hereda de la clase B, entonces
B es la superclase
de A. A es subclase de B.
En la literatura también se pueden encontrar otros términos para "superclase" y para "subclase". Las superclases también son llamadas clases padres. Las subclases pueden ser llamadas también clases hijas o simplemente clases derivadas.
Por supuesto, también se puede heredar de una subclase, haciendo que esta clase sea la superclase de la nueva subclase. Esto conduce a una jerarquía de relaciones superclase/subclase. Si dibujas esta jerarquía, se obtiene una gráfica de herencia.
Un esquema común consiste en usar flechas para indicar la relación de herencia entre clases u objetos. En nuestros ejemplos hemos usado "hereda-de". Consecuentemente, la flecha empieza desde la subckase hacia la superclase, como se ilustra en la Figura 5.5.
En la literatura también puedes encontrar ilustraciones donde las flechas se dibujan del modo contrario. La dirección en la que se usan las flechas dependen de como el autor correspondiente las haya decidido entender.
De cualquier manera, en este tutorial, las flechas simpre apuntan hacia la superclase.
En las secciones siguientes, las flechas indican "hereda-de".
Un mecanismo importante de orientación a objetos es la herencia múltiple. La herencia múltiple no significa que mútiples subclases compartan la misma superclase. Tampoco significa que una subclase herede de una clase que es a su vez subclase de otra clase.
La herencia múltiple significa que una subclase puede tener más de una superclase. Esto permite que la subclase herede propiedades de más de una superclase y "mezclar" sus porpiedades.
Considérese por ejemplo nuevamente nuestro programa de dibujo. Suponiendo que ya tenemos una clase String que nos permite el manejo adecuado de texto. Podría tener, por ejemplo, un método append para añadir otro texto. En nuestro programa, nos gustaría usar esta clase para añadir texto a los objetos que se pudieran dibujar. También sería bueno usar rutinas ya existentes tales como move() para mover el texto a donde fuera necesario. Por consecuencia, es razonable permitir que un texto para dibujarse tenga un punto que defina su localización dentro del área de dibujo. Por lo tanto derivamos una nueva clase DrawableString que hereda propiedades de Point y de String como se ilustra en la Figura 5.6.
En nuestro pseudo lenguaje, escribimos ésto, simplemente separando las diferentes superclases con comas :
class DrawableString inherits from Point, String { attributes: /* Todos heredados de superclases */ methods: /* Todos heredados de superclases */ }
Podemos usar objetos de la clase DrawableString como ambos puntos y strings. Debido a que drawablestring es-un(a) point podemos mover dichos objetos
DrawableString dstring ... move(dstring, 10) ...
Desde el momento que son string, podemos añadirles otro texto:
dstring.append("La zorra color marrón ...")
Es el momento para la definición de la herencia múltiple :
Definición (Herencia Múltiple)
Si la clase A hereda de más de una clase, p.ej. A hereda de B1,
B2, ..., Bn, hablamos de herencia múltiple. Esto puede presentar
conflictos de nomenclatura en A si al menos dos de sus superclases definen propiedades con el mismo nombre.
La definción de arriba presenta conflictos de nomenclatura los cuáles ocuren si más de una superclase de una subclase usan el mismo nombre para ambos, atributos o métodos. Por ejemplo, supongamos que la clase String define un método setX() que pone el string en una secuencia de "X" caracteres. Se produce la pregunta ¿Que debería ser heredado por DrawableString? ¿La versión de Point, de String o ninguna de las dos?
Estos conflictos pueden ser resueltos de al menos dos maneras :
La primera solución no es muy conveniente ya que presentan consecuencias implícitas dependiendo del orden en el cuál las clases heredan unas de otras. Para el segundo caso, las subclases deben redefinir explícitamente las propiedades que están involucradas en conflictos de nomenclatura.
Un tipo especial de conflicto de nomenclatura se presenta si una clase D hereda en forma múltiple de las superclases B y C que a su vez son derivadas de una superclase A. Esto conduce a una gráfica de herencia como se muestra en la Figura 5.7.
Cabe la pregunta acerca de que propiedades hereda realmente la clase D de sus superclases B y C. Algunos lenguajes de programación existentes resuelven esta gráfica de herencia especial derivando D con
Consecuentemente, D no puede presentar conflictos de nomenclatura con los nombres en la clase A. Sin embargo, si B y C añaden propiedades con el mismo nombre, D entra en un conflicto de nomenclatura.
Otra posible solución es que D herede de ambas trayectorias de herencia. En esta solución, D tiene dos copias de las propiedades de A: una heredada de B y otra de C.
Aunque la herencia múltiple es un poderoso mecanismo en orientación a objetos, los problemas que se presentan con los conflictos de nomenclatura ha llevado a varios autores a "condenarla". Debido a que los resultados de la herencia múltiple siempre puede ser lograda usando herencia simple, algunos lenguajes orientados a objetos no permiten siquiera su uso. Sin embargo, usada con cuidado, bajo algunas condiciones la herencia múltiple provee una manera eficiente y elegante de formular cosas.
Con la herencia nosotros podemos forzar a una sublcase para que ofrezca las mismas propiedades de sus superclases. Por consecuencia, los objetos de una subclase se comportan como los objetos de sus superclases.
Algunas veces tiene sentido solamente describir las propiedades de un conjunto de objetos sin saber el comportamiento real de antemano. En nuestro ejemplo del programa de dibujo, cada objeto debía proveer un método para dibujarse a si mismo en el área de dibujo. Sin embargo, los pasos necesarios para dibujar objetos dependen de su forma representada. Por ejemplo, la rutina de dibujo de un círculo es diferente de la rutina de dibujo de un rectángulo.
Llamemos al método de dibujo, print(). Para forzar a que cada objeto desplegable incluya dicho método, definimos una clase DrawableObject de la cuál cada una de las otras clases en nuestro ejemplo hereda propiedades generales de objetos desplegables :
abstract class DrawableObject { attributes: methods: print() }
Introducimos aquí la nueva palabra clave abstract. Se usa para expresar el hecho de que las clases derivadas deben "redefinir" las propiedades para cumplir con la funcionalidad deseada. Así, desde el punto de vista de la clase abstracta, las propiedades son únicamente specificadas pero no completamente definidas. La definición completa incluyendo la semántica de las propiedades debe ser provista por las clases derivadas.
Ahora, todas las clases en nuestro ejemplo de programa de dibujo heredan las propiedades de la clase objeto general desplegable. Por lo tanto, las clase Point cambia a:
class Point inherits from DrawableObject { attributes: int x, y methods: setX(int newX) getX() setY(int newY) getY() print() /* Redefinir para Point */ }
Podemos ahora forzar a que cada objeto desplegable tenga un método llamado print el cuál debería proveer funcionalidad para dibujar el objeto dentro del área de dibujo. La superclase de todos los objetos desplegables, la clase DrawableObject, no provee ninguna funcionalidad para dibujar por sí misma. No es la intención el crear objetos a partir de ella. Esta clase más bien especifica las propiedades que deben ser definidas por cada clase derivada. Nos referimos a este tipo especial de clases como abstract classes:
Definición (Calse Abstracta)
Una clase A se llama clase abstracta si es usada solamente como una superclase para otras clases.
La Clase A solamente especifica propiedades. No se usa para crear objetos. Las clases derivadas deben definir las propiedades de A.
Las clases abstractas nos permiten estructurar nuestra gráfica de herencia. Sin embargo, nosotros no deseamos realmente crear objetos a partir de ellas : solamente queremos expresar características comunes de un conjunto de clases.
Una definición correspondiente podría verse así:
class Sphere inherits from Circle { attributes: int z /* Añade la 3a dimensión */ methods: setZ(int newZ) getZ() }
Da razones de la ventaja/desventaja de esta alternativa.
¿Qué conflictos de nomenclatura pueden ocurrir ? Trata de definir casos jugando con ejemplos de clases simples.