Subtipos e Subclasses

Anuncio
Subtipado
Clase 15 del curso 6.170
15 de octubre de 2001
Sumario
1 Subtipos
2 Ejemplo: bicicletas
3 Ejemplo: cuadrado y rectángulo
4 Principio de sustitución
5 Subclases y subtipos Java
6 Interfaces Java
Lecturas necesarias: capítulo 7 del libro Program Development in Java de Bárbara Liskov.
Consulte su libro de texto de Java para obtener detalles sobre este lenguaje como, por
ejemplo, tipos abstractos (no todos los métodos se implementan y ningún objeto se puede
instanciar) y detalles sobre interfaces y modificadores de acceso (public, private, protected,
default). Estos temas no se tratarán en esta clase.
1 Subtipos
Decimos que A es B si todo objeto A es también un objeto B. Por ejemplo, todo automóvil
es un vehículo y toda bicicleta es un vehículo, incluso unos zancos son un vehículo: todo
vehículo es un medio de transporte, así como todo animal de carga. Representamos esta
relación de subconjuntos en un diagrama de dependencia modular:
Esta relación de subconjunto es condición necesaria, pero no suficiente, para una relación
de subtipificación. El tipo A es un subtipo del tipo B cuando la especificación de A implica
la especificación de B. Esto es, cualquier objeto (o clase) que satisfaga la especificación de
A también satisfará la especificación de B, ya que la especificación de B es más débil.
Otra manera de explicar esto es que en cualquier lugar del código, si se espera un objeto B,
es admisible un objeto A. Se garantiza que el código escrito para funcionar con los objetos
B (y para depender de sus propiedades) continua funcionando si se suministran objetos A
en su lugar; además, el comportamiento será el mismo, si se consideran sólo los aspectos
del comportamiento de A que también están incluidos en el comportamiento de B. (Es
posible que A introduzca nuevos comportamientos que B no tenga, pero esto sólo puede
modificar los comportamientos existentes de B en ciertas maneras; que veremos
enseguida).
2 Ejemplo: bicicletas
Suponga que tenemos una clase para representar bicicletas. He aquí una implementación
parcial de esa clase:
class Bicycle{
private int framesize;
private int chainringGears;
private int freewheelGears;
...
// devuelve el número de marchas de la bicicleta
public int gears() { return chainringGears *
freewheelGears; }
// devuelve el precio de la bicicleta
public float cost() { ... }
// devuelve el impuesto de venta que incide sobre la
bicicleta
public float salesTax() { return cost() * .0825; }
// ejecución: transporta al ciclista del trabajo a casa
public void goHome() { ... }
...
}
Una nueva clase que representa bicicletas con luces delanteras para poderse adaptar a la
falta de luz.
class LightedBicycle{
private int framesize;
private int chainringGears;
private int freewheelGears;
private BatteryType battery;
...
// devuelve el número de marchas de la bicicleta
public int gears() { return chainringGears *
freewheelGears; }
// devuelve el precio de la bicicleta
float cost() { ... }
// devuelve el impuesto de venta que incide sobre la
bicicleta
public float salesTax() { return cost() * .0825; }
// ejecución: transporta al ciclista del trabajo a
casa
public void goHome() { ... }
// ejecución: sustituye la pila existente por el
argumento b
public void changeBattery(BatteryType b);
...
}
Copiar todo el código resulta trabajoso y aumenta la posibilidad de incurrir en errores. (El
error puede provenir de un fallo en la copia o en la realización de una modificación
requerida). Además, si se encuentra un error en una versión, es fácil olvidarse de extender
el arreglo a todas las versiones del código. Por último, es muy difícil comprender la
distinción de las dos clases observando únicamente las diferencias en un cúmulo de
similitudes.
Java y otros lenguajes de programación utilizan el concepto de subclase para superar esas
dificultades. Este concepto permite reutilizar las implementaciones y sobrescribir los
métodos.
A continuación presentamos una implementación mejor de la clase LightedBicycle:
class LightedBicycle extends Bicycle{
private BatteryType battery;
...
// devuelve el precio de la bicicleta
float cost() { return super.cost() + battery.cost();
}
// ejecución: transporta al ciclista del trabajo a
casa
public void goHome() { ... }
// ejecución: sustituye la pila existente con el
argumento b
public void changeBattery(BatteryType b);
...
}
LightedBicycle no necesita implementar métodos y campos que aparecen en su
superclase Bicycle; las versiones de Bicycle son automáticamente utilizadas por Java
cuando no son sobrescritas en la subclase.
Considere la siguiente implementación del método goHome (junto con especificaciones
más completas). Si éstos son los únicos cambios, ¿son las clases LightedBicycle y
RacingBicycle subtipos de Bicycle? (De momento trataremos el concepto de
subtipos; más tarde volveremos a las diferencias entre las subclases Java, los subtipos Java,
y los verdaderos subtipos).
class Bicycle{
...
// requiere: velocidad_del_viento < 20mph && luz_del_dia
// ejecución: transporta al ciclista del trabajo a casa
public void goHome() { ... }
}
class LightedBicycle{
...
// requiere: velocidad_del_viento < 20mph
// ejecución: transporta al ciclista del trabajo a casa
void goHome() { ... }
}
class RacingBicycle{
...
// requiere: velocidad_del_viento < 20mph && luz_del_dia
// ejecución: transporta al ciclista del trabajo a casa
// en un período de tiempo < 10 minutos
// && hace al ciclista sudar
void goHome() { ... }
}
Para responder a esa pregunta, recuerde la definición de subtipificación: ¿puede un objeto
del subtipo ser sustituido en cualquier lugar donde el código espera un objeto del supertipo?
Si es así, la relación de subtipificación es válida.
En este caso, tanto LightedBicycle como RacingBicycle son subtipos de
Bicycle. En el primer caso, las condiciones son relajadas; en el segundo caso, la
ejecución se refuerza de una manera que aún satisface la ejecución de la superclase.
El método cost de LightedBicycle muestra otra capacidad de especialización de
clases en Java. Los métodos pueden ser sobrescritos para facilitar una nueva
implementación en una subclase. Esto permite una mayor reutilización del código; en
particular, LightedBicycle puede reutilizar el método salesTax de Bicycle.
Cuando se llama a salesTax en una LightedBicycle, la versión de Bicycle es la
que se utiliza. Entonces, la llamada de cost dentro de salesTax llama a la versión
basada en el tipo de tiempo de ejecución del objeto (LightedBicycle), con lo que se
utiliza la versión LightedBicycle. Independientemente del tipo declarado de un objeto,
la implementación de un método con muchas implementaciones (de la misma firma)
siempre se selecciona basándose en el tipo de tiempo de ejecución.
De hecho, un cliente externo no tiene manera de llamar a la versión de un método,
especificado por el tipo declarado o cualquier otro tipo, que no sea el tipo de tiempo de
ejecución. Ésta es una propiedad atractiva y muy importante de Java (y de otros lenguajes
orientados a objetos). Suponga que la subclase mantiene algunos campos adicionales que se
mantienen sincronizados con los campos de la superclase. Si los métodos de la superclase
pudieran llamarse directamente, posiblemente modificando campos de la superclase sin que
los campos de la subclase sean alterados también, entonces se violaría el invariante de
representación de la subclase.
De cualquier modo, una subclase puede llamar métodos de sus padres mediante la
utilización de super. A veces es útil cuando el método de la subclase necesita hacer un
poco más de trabajo; recuerde la implementación de LightedBicycle para cost:
class LightedBicycle extends Bicycle{
// devuelve el precio de la bicicleta
float cost() { return super.cost() + battery.cost();
}
}
Suponga que la clase Rider modela las personas que montan en bicicleta. En ausencia de
especializaciones de clase y de subtipos, el diagrama de dependencia modular tendría el
siguiente aspecto:
El código para Rider también tendría que probar que tipo de objeto se ha pasado, lo que
resultaría feo, ampuloso y propenso a errores.
Con la subtipificación, las dependencias de MDD se parecerían a esto:
Las diversas dependencias se han reducido a una.
Cuando se añaden las fechas de subtipo, el diagrama resulta apenas un poco más
complicado:
Aunque existan varias flechas, este diagrama es más sencillo que el original: las
restricciones de dependencia complican el diseño y la implementación más que otros tipos
de restricciones.
3 Ejemplo: cuadrado y rectángulo
Desde la escuela primaria sabemos que todo cuadrado es un rectángulo. Suponga que
queremos hacer del cuadrado Square un subtipo de Rectangle que incluya un método
setSize:
class Rectangle{
...
// ejecución: define la anchura width y la altura height
como los valores
// especificados (esto es, this.width’ = w &&
this.height’ = h)
void setSize(int w, int h);
}
class Square extends Rectangle{
...
}
¿Cuál de los siguientes métodos es adecuado para Square?
// requiere: w = h
void setSize(int w, int h);
void setSize(int edgeLenght);
// arroja la excepción BadSizeException si w != h
void setSize(int w, int h) throws BadSizeException;
El primero no resulta acertado porque el método de subclase exige más que el método de
superclase. Así, los objetos de subclase no se pueden sustituir por objetos de superclase, ya
que puede existir alguna parte del código que llame al método setSize con argumentos
diferentes.
El segundo no está en lo cierto (completamente), ya que la subclase aún debe especificar un
comportamiento para setSize(int, int); ésta es una definición de un método
diferente (cuyo nombre es el mismo pero cuya firma es diferente).
El tercero no es correcto porque arroja una excepción que la superclase no menciona. Así,
de nuevo, posee un comportamiento diferente y de esta manera Square no puede ser
sustituido por Rectangle. (Si la excepción BadSizeException es una excepción no
verificada, entonces Java permitirá la compilación del tercer método; pero, de nuevo,
también permitirá la compilación del primer método. La noción de Java de subtipo es más
débil que la propia noción de subtipo del curso 6.170. Sin ninguna arrogancia, llamaremos
a estos últimos “subtipos verdaderos” para distinguirlos de los subtipos de Java).
No hay forma de salir de este dilema sin modificar el supertipo. Algunas veces los subtipos
no están de acuerdo con nuestra intuición. O bien nuestra intuición sobre lo que es un buen
subtipo es errónea.
Una solución plausible sería modificar Rectangle.setSize para especificar que él
arroja la excepción; esta claro que, en la práctica, solamente Square.size lo haría. Otra
solución sería eliminar setSize y en su lugar tener el método
void scale(double scaleFactor);
que disminuye o aumenta una figura. Otras soluciones también son posibles.
4 Principio de sustitución
El principio de sustitución es la base teórica de los subtipos: ofrece una definición precisa
de cuándo dos tipos son subtipos. Informalmente, afirma que los subtipos deben ser
sustituibles por supertipos. Esto garantiza que el comportamiento del sistema no se verá
afectado cuando el código dependa de (cualquier aspecto de) un supertipo, pero habiéndose
sustituido un objeto de un subtipo. (El compilador de Java también requiere que las
cláusulas extends o implements nombren al padre para que los subtipos se utilicen
en lugar de los supertipos).
Los métodos de un subtipo deben mantener ciertas relaciones con los métodos del
supertipo, y el subtipo debe garantizar que las propiedades del supertipo (como los
invariantes de representación o las restricciones de especificación) no sean violadas por el
subtipo.
Métodos. Existen dos propiedades necesarias:
1. El subtipo debe tener un método correspondiente para cada método del supertipo.
(Esta permitido que el subtipo introduzca nuevos métodos adicionales que no
aparezcan en el supertipo).
2. Cada método del subtipo que corresponde a un método del supertipo:
• requiere menos (tiene una condición previa más débil)
- existen menos cláusulas “requires” y cada una de ellas es menos rigurosa
que la del método del supertipo.
- los tipos de argumentos pueden ser uno de los supertipos del supertipo.
Esto se llama contravarianza, y puede dar la impresión de ser un paso hacia
atrás, ya que los argumentos del método subtipo son supertipos de los
argumentos de los métodos del supertipo. Sin embargo, esto tiene sentido,
porque así se garantiza que cualquier argumento pasado al método del
supertipo es un argumento válido para el método del subtipo.
• garantiza más (tiene una condición posterior más fuerte)
- no existen más excepciones
- existen menos variables modificadas
- en la descripción del resultado o en el estado resultante, existen más
cláusulas, y éstas describen propiedades más fuertes.
- el tipo de resultado debe ser uno de los subtipos del supertipo. Esto se
llama covarianza: el tipo de retorno del método del subtipo es un subtipo del
tipo de retorno del método del supertipo.
(Todas las descripciones anteriores deberían permitir la uniformidad; por ejemplo,
“requerir menos” debería ser “no requerir más”, y “menos rigurosa” debería ser “no
más rigurosa”. Se han expresado de esta forma para facilitar su lectura).
El método de subtipo no debe comprometerse a ofrecer mayores resultados o
resultados diferentes; sólo debe comprometerse a hacer lo que ha hecho el método
del supertipo, garantizando también las propiedades adicionales. Por ejemplo, si un
método de un supertipo devuelve un número mayor que su argumento, un método
de subtipo devolvería un número primo mayor que su argumento. Como ejemplo de
las restricciones de tipo, si A es un subtipo de B, entonces la siguiente redefinición
(que es lo mismo que sobrescribir) sería válida:
Bicycle B.f(Bicycle arg);
RacingBicycle A.f(Vehicle arg);
El método B toma una bicicleta Bicycle como su argumento, pero A.f puede
aceptar cualquier vehículo (lo que incluye todas las bicicletas). El método B.f
devuelve una bicicleta Bicycle como resultado, pero A.f devuelve una bicicleta de
carreras RacingBicycle (que es una bicicleta propiamente dicha).
Propiedades Toda propiedad garantizada por un supertipo, como las restricciones sobre los
valores que aparezcan en los campos de especificación, debe estar también
garantizada por el subtipo. (Está permitido que el subtipo refuerce esas
restricciones).
Como un ejemplo sencillo del libro de texto, considere FatSet, que siempre está no
vacío.
class FatSet{
// restricciones de especificación: el objeto this debe
// contener siempre por lo menos un elemento
...
// ejecución: si el objeto this contiene x y this.size
> 1,
// elimina x de this
void remove(int x);
}
El tipo SuperFatSet con un método adicional
// ejecución: elimina x del objeto this
void reallyRemove(int x)
no es un subtipo de FatSet. Aunque no hay ningún problema con los métodos de
FatSet –reallyRemove es un método nuevo, por lo que las reglas sobre métodos
correspondientes no se aplican: se trata de un método que viola la restricción.
Si el objeto del subtipo se considera meramente como un objeto del supertipo (esto es, sólo
se consultan los métodos y campos del supertipo), entonces el resultado debería ser el
mismo que si se hubiese gestionado en su lugar un objeto del supertipo.
En la sección 7.9, el libro de texto describe el principio de sustitución como la colocación
de restricciones en
• firmas: se trata esencialmente de las reglas de contravarianza y de
covarianza explicadas arriba. (La firma de un procedimiento se compone de
su nombre, tipos de argumentos, tipos de devolución y excepciones).
• métodos: se trata de restricciones del comportamiento, o de todos los
aspectos de una especificación que no se pueden explicar en una firma.
• propiedades: como arriba.
5 Subclases y subtipos Java
Los tipos de Java son clases, interfaces o primitivas. Java posee su propia noción de subtipo
(que comprende sólo las clases y las interfaces). Se trata de una noción más débil que la de
subtipos verdaderos anteriormente descrita: los subtipos Java no satisfacen necesariamente
el principio de substitución. Además, es posible que una definición de subtipo que satisfaga
el principio de sustitución no se permita en Java, por lo que no compilará.
Para que un tipo sea un subtipo de Java de otro tipo, la relación debe declararse (mediante
la sintaxis Java extends o implements), y los métodos deben satisfacer dos
propiedades similares, aunque más débiles, a las de los subtipos verdaderos:
1. El subtipo debe tener un método correspondiente para cada método del
supertipo. (Está permitido que el subtipo introduzca nuevos métodos adicionales
que no aparezcan en el supertipo).
2. Para cada método del subtipo que corresponda a un método del supertipo:
• los argumentos deben tener los mismos tipos
•
•
el resultado debe tener el mismo tipo
no deben existir más declaraciones de excepciones.
Java no posee ninguna noción de especificación de conducta, por lo tanto no realiza
verificaciones y no puede dar ninguna garantía en cuanto al comportamiento. La exigencia
de uniformidad de tipos para los argumentos y los resultados es más fuerte que lo
estrictamente necesario para garantizar la protección del tipo. Esto prohíbe algunas partes
de código que nos gustaría escribir; lo que, sin embargo, simplifica la sintaxis y la
semántica del lenguaje Java.
La especialización de clases posee varias ventajas, todas ellas provenientes de la
reutilización:
•
•
•
las implementaciones de subclases no precisan repetir los campos y métodos
no alterados, pero pueden utilizar los de la superclase
los clientes (aquellos que ejecutan las llamadas) no precisan modificar el
código cuando se añaden nuevos subtipos, pero pueden reutilizar el código
existente (la parte que no menciona los subtipos, sólo el supertipo)
el diseño resultante posee una modularidad mejorada y una complejidad
reducida, porque los diseñadores, los programadores y los usuarios
solamente tienen que entender el supertipo, no cada subtipo: esto se llama
reutilización de la especificación.
Un mecanismo clave que permite obtener estas ventajas es la redefinición, que especializa
el comportamiento para algunos métodos. En ausencia de redefinición, cualquier alteración
del comportamiento (aunque se trate de una alteración compatible) podría forzar una
reimplementación completa. La redefinición permite que parte de una implementación se
altere sin modificar otras partes que dependen de ella, haciendo posible una mayor
reutilización del código y de la especificación por parte de la implementación y del cliente.
Una posible desventaja de la especialización de clases es que suponen un riesgo de
reutilización inapropiada. Es posible que las subclases y las superclases dependan unas de
otras (explícitamente por el nombre del tipo o implícitamente por el conocimiento de la
implementación), especialmente porque las subclases tienen acceso a las partes protegidas
de la implementación de la superclase. Estas dependencias extras complican el MDD, el
diseño y la implementación, haciendo que sea más difícil codificar, entender y modificar.
6 Interfaces Java
Algunas veces el usuario desea tener garantía sobre el comportamiento sin tener que
compartir el código. Por ejemplo, tal vez necesite ordenar los elementos de un contenedor
específico o que éstos acepten una determinada operación sin facilitar una implementación
por defecto (porque toda relación de orden posee una implementación diferente). Java
ofrece interfaces que permiten resolver estas necesidades y garantizar que no se reutilizará
el código. Otra de sus ventajas es que una clase puede implementar múltiples interfaces y
una interfaz puede ampliar otras muchas. En oposición a esto, una clase sólo puede ampliar
una clase. En la práctica, la implementación de múltiples interfaces y la extensión de una
superclase única proporcionan la mayoría de los beneficios de la herencia arbitraria, pero
con una semántica y una implementación más simples. Una desventaja de las interfaces es
que no facilitan el modo de especificar la firma (o el comportamiento) de un constructor.
Descargar