Está en la página 1de 30

Capı́tulo 2

Conceptos y mecanismos de la
programación orientada a objetos

Tradicionalmente, la orientación a objetos se ha presentado como un estilo de programa-


ción donde el diseño se realiza alrededor de los datos a ser manipulados, en vez de centrarlo
en las operaciones que debe realizar el sistema objeto del diseño. Las ventajas de este enfo-
que frente a la programación “tradicional” radica en la mayor estabilidad en el tiempo de los
datos frente a las operaciones, lo que repercute en mejores posibilidades de reutilización y
extensión. Como consecuencia de este enfoque surge el concepto de clase como una forma de
organizar código (es decir, como un criterio de modularización), y al mismo tiempo como una
forma de declarar tipos de datos. Esta visión dual de la noción de clase (módulo + tipo) es la
que distingue los lenguajes orientados a objetos (como Smalltalk, Eiffel, C++, Java o C#) de
lenguajes orientados a tipos abstractos de datos (como Modula o Ada), que a veces han sido
denominados lenguajes basados en objetos. Profundizando en esta caracterización de la orien-
tación a objetos, y para distinguirla de la programación basada en tipos abstractos de datos,
siempre se ha considerado como indispensable el mecanismo de reutilización por excelencia
en los lenguajes orientados a objetos: la herencia.
La herencia, que no ha sido el único mecanismo de reutilización propuesto en el contex-
to de la POO1 , supuso un salto cualitativo importante en la reutilización de código, frente a
mecanismos previos de importación de módulos o paquetes. La diferencia fundamental en-
tre herencia e importación tiene que ver precisamente con la doble interpretación de una clase.
Mientras que la importación corresponderı́a a una reutilización de la clase vista como módulo,
la herencia considera el valor añadido de la reutilización a nivel de tipos.
No obstante, no serı́a acertado limitar los aciertos y ventajas del paradigma de programa-
ción por excelencia de los 90 a los conceptos de clase, como forma de organizar el código, y
herencia, como vı́a para reutilizarlo. Efectivamente, las ideas realmente interesantes (sin qui-
tarle importancia a las previamente descritas) hay que buscarlas en otros mecanismos que
complementan a los ya mencionados. De este modo, la combinación de capacidades como el
polimorfismo de datos, la vinculación dinámica o el uso de clases abstractas, hacen de la programa-
ción orientada a objetos la herramienta más adecuada para implementar software de tamaño
mediano o grande.
1 La delegación, de la que hablaremos en la sección 2.3.4, es el mecanismo de reutilización alternativo a la herencia

más utilizado después de ésta.

15
16 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

En este capı́tulo introduciremos estos conceptos fundamentales de la POO en más detalle,


y lo haremos independientemente de ningún lenguaje de programación. Estos conceptos apa-
recen de una forma u otra en los distintos lenguajes de programación orientada a objetos, y
de hecho, veremos cómo aparecen en Smalltalk, Eiffel, C++ y Java. En capı́tulos sucesivos nos
centraremos en Java.

2.1. Clases y objetos


La forma de organizar el código en un lenguaje de programación tiene repercusiones im-
portantes en las estrategias de diseño que el programador puede aplicar al desarrollar sof-
tware. En los lenguajes de programación orientada a objetos, la noción de clase constituye el
mecanismo básico de organización de código, agrupando en un mismo módulo datos y proce-
dimientos para manipularlos. Asimismo, este concepto responde a una evolución natural de
la idea de tipo abstracto de datos. En efecto, un tipo de datos, como por ejemplo los enteros, se
caracteriza no sólo por los valores que representa (cada uno de los números enteros), sino tam-
bién el tipo de operaciones que pueden realizarse sobre ellos (suma, resta, producto, división
entera, . . . ).
Todo lenguaje de programación proporciona ciertos tipos de datos predefinidos (enteros,
reales, booleanos, caracteres) que pueden ser particularizados a valores concretos. Si tenemos
en cuenta que la noción de objeto no es más que una reinterpretación del concepto de dato,
y consideramos que los valores enteros o reales son un tipo especial (simple) de objetos, un
lenguaje orientado a objetos debe proporcionar alguna forma para que los programadores
puedan definir e instanciar sus propios objetos, del mismo modo que se definen e instancian
datos utilizando los tipos predefinidos. El método más extendido, y presente en la mayorı́a de
los lenguajes orientados a objetos comerciales, lo constituye la posibilidad de definir clases de
objetos.
Sintácticamente, una clase especifica un conjunto de variables y un conjunto de operacio-
nes que determinan, respectivamente, el estado y el comportamiento de un grupo de objetos.
Cada instancia de una clase, u objeto, posee sus propias variables de estado, pero comparte las
operaciones con el resto de instancias de la clase. Este hecho va a ser de singular importancia
en la incorporación de concurrencia y la distribución de objetos en arquitecturas paralelas.
El uso de clases como forma de describir objetos es recogido en lenguajes bien conocidos
como Smalltalk, Eiffel, C++ o Java, pero existe una alternativa al uso de clases para describir
objetos y su instanciación. En efecto, lenguajes como Self [US87] y Act1 [Lie87] utilizan la
noción de prototipo en sustitución de la de clase. Con esta solución no existe una distinción
explı́cita entre clase y objeto, sino que sólo se mantiene un nivel de abstracción, dado por el
prototipo del objeto. A diferencia del modelo basado en clases, en el que el estado de cada
objeto era disjunto e independiente del resto, con el uso de prototipos el estado de dos objetos
puede poseer partes comunes.
La identificación entre clase y tipo (el objeto es a la clase lo que el valor es al tipo) va aún
más lejos: usualmente, los lenguajes orientados a objetos (tengan comprobación estricta de
tipos o no) identifican completamente ambas nociones. De este modo, la noción de clase no
sólo corresponde a una unidad modular, sino que también determina el tipo de los objetos.
Volveremos a esta discusión en la sección 2.5.
A modo de ejemplo, y para ilustrar los conceptos que se van a ir introduciendo en las sec-
ciones que siguen, vamos a considerar la representación en un lenguaje orientado a objetos
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 17

de la abstracción “punto en el plano”. Todos los objetos que puedan ser identificados como
puntos en el plano estarán caracterizados por un estado, determinado por sus coordenadas
(abscisa y ordenada), y un comportamiento, fijado por las operaciones que pueden ser aplica-
das sobre esos objetos (trasladar un punto, calcular la distancia entre dos puntos, obtener la
abscisa u ordenada de un punto, cambiar la abscisa u ordenada, etc.).
El listado 2.1 muestra la descripción en Java de la clase Punto, caracterizada por un es-
tado (las variables x e y) y un comportamiento (las operaciones trasladar, distancia,
abscisa y ordenada). Definiciones similares en otros lenguajes como Smalltalk, C++ e Eif-
fel, pueden consultarse en los listados 2.2, 2.3 y 2.4, respectivamente2 . Obsérvese que, aunque
la sintaxis es diferente (especialmente la utilizada en el lenguaje Smalltalk), los elementos des-
critos en todos los casos son los mismos. Por un lado, encontramos las variables que definen el
estado de los objetos (denominadas variables de instancia en Smalltalk y Java, datos miembro en
C++ y atributos en Eiffel), las operaciones que determinan su comportamiento (denominadas
métodos en Smalltalk y Java, funciones miembro en C++ y rutinas en Eiffel) y las operaciones que
definen la construcción de objetos (denominadas constructores).
Es importante hacer notar, sobre todo a los programadores habituados a lenguajes estruc-
turados (no orientados a objetos), que el número de argumentos de los métodos suele ser uno
menos en los lenguajes orientados a objetos. Ası́, la operación trasladar posee dos argumentos
(los incrementos de las coordenadas), y el método distancia sólo uno, en vez de tener tres y
dos argumentos, respectivamente, como ocurrirı́a en un lenguaje no orientado a objetos. Esto
se debe a que uno de los argumentos coincide con el objeto representado por la clase defini-
da: el método distancia calcula el espacio entre el punto representado por la clase, que recibe el
mensaje, y el punto pasado como argumento. Esto se entenderá mejor en la sección 2.2.
Con objeto de representar clases independientemente del lenguaje en el que se implemen-
ten, utilizaremos el lenguaje unificado de modelado (Unified Model Language –UML), que es
una notación diagramática de comprensión intuitiva. A modo de ejemplo, la clase Punto se re-
presentarı́a según se muestra en la figura 2.1. Aunque no sea objetivo de esta sección, iremos
introduciendo algunas caracterı́sticas de la notación UML. Las clases se representan median-
te cajas divididas (habitualmente) en tres partes verticalmente: la parte superior que contiene
el nombre de la clase, la parte intermedia para contener las variables que definen el estado
y la parte inferior que contiene los métodos que caracterizan el comportamiento. Tanto las
variables como los métodos pueden venir precedidos por un sı́mbolo, indicando el nivel de
protección de la información: público (+), protegido (#), a nivel de paquete (˜) y privado (-).
Volveremos sobre el tema de la ocultación de información en las secciones siguientes.
De este modo, una clase no es más que la abstracción de un grupo de objetos que exhiben
un comportamiento común. Los objetos correspondientes a una misma clase difieren entre
sı́ por su identidad y, posiblemente, por los valores de sus variables de instancia (es decir, su
estado).
La creación de objetos (también denominados instancias) a partir de una clase depende
de las construcciones que el lenguaje proporcione para ello. Existen distintas variaciones al
respecto: desde el uso de una primitiva especial (make instance, new) aplicada a clases, como
ocurre en CLOS, hasta el uso de una primitiva similar (create) aplicada a objetos, como ocurrı́a
en las primeras versiones de Eiffel, pasando por la solución de Smalltalk (adoptada por la
2 Lo que nos interesa de estos códigos es dar una idea intuitiva de cómo aparecen los distintos conceptos de la POO

en los distintos lenguajes. No nos preocupamos de momento de algunas de las particularidades de estos lenguajes,
como las palabras reservadas public o private de Java para establecer niveles de visibilidades, uso de funciones
matématicas como pow o sqrt, etc.
18 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class Punto {
/ / Coordenadas d e l punto
p r i v a t e double x , y ;

/ / C o n s t r u c t o r para i n i c i a l i z a r a 0 ambas coordenadas


public Punto ( ) {
x = y = 0;
}

/ / C o n s t r u c t o r para i n i c i a l i z a r ambas coordenadas


public Punto ( double a , double b ) {
x = a;
y = b;
}

/ / Incrementa l a s coordenadas d e l punto


public void t r a s l a d a r ( double a , double b ) {
x = x + a;
y = y + b;
}

/ / Devuelve l a d i s t a n c i a e n t r e e s t e punto y e l argumento


public double d i s t a n c i a ( Punto p t o ) {
r e t u r n Math . s q r t ( Math . pow ( x − p t o . x , 2 )
+ Math . pow ( y − p t o . y , 2 ) ) ;
}

/ / Devuelve e l v a l o r de l a coordenada x
public double a b s c i s a ( ) {
return x ;
}

/ / Devuelve e l v a l o r de l a coordenada y
public double ordenada ( ) {
return y ;
}

/ / Cambia e l v a l o r de l a coordenada x
public void a b s c i s a ( double nuevaX ) {
x = nuevaX ;
}

/ / Devuelve e l v a l o r de l a coordenada y
public void ordenada ( double nuevaY ) {
y = nuevaY ;
}
}

Listado 2.1: Versión Java de la clase Punto.


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 19

O b j e c t s u b c l a s s : # Punto
instanceVariableNames : ’ x y ’
classVariableNames : ’ ’
poolDictionaries : ’ ’

! Class methods

new
” M étodo de c l a s e para c r e a r un punto con coordenadas ( 0 , 0 ) ”
ˆ ( super new) a b s c i s a : 0 ordenada : 0

x : unNumero y : otroNumero
” M étodo de c l a s e para c r e a r un punto con coordenadas i n i c i a l e s ”
ˆ ( s e l f new) a b s c i s a : unNumero ordenada : otroNumero

! I n s t a n c e methods

a b s c i s a : unNumero ordenada : otroNumero


” M o d i f i c a l a s coordenadas con l o s argumentos c o r r e s p o n d i e n t e s ”
x : = unNumero .
y : = otroNumero

abscisa
” Devuelve l a a b s c i s a d e l punto ”
ˆx

ordenada
” Devuelve l a ordenada d e l punto ”
ˆy

a b s c i s a : unNumero
” Cambia l a a b s c i s a d e l punto ”
x : = unNumero

ordenada : unNumero
” Cambia l a ordenada d e l punto ”
y : = unNumero

t r a s : unNumero l a d a r : otroNumero
” Incrementa l a s coordenadas d e l punto ”
x : = x + unNumero .
y : = y + otroNumero

d i s t a n c i a : unPunto
” C a l c u l a l a d i s t a n c i a e n t r e e s t e punto y e l argumento ( unPunto ) ”
ˆ ( ( x − unPunto a b s c i s a ) squared +
( y − unPunto ordenada ) squared ) ) s q r t

Listado 2.2: Versión Smalltalk de la clase Punto.


20 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class Punto {
private :
/ / Coordenadas d e l punto
double x , y ;

public :

/ / C o n s t r u c t o r para i n i c i a l i z a r a 0 ambas coordenadas


Punto ( ) {
x = y = 0;
}

/ / C o n s t r u c t o r para i n i c i a l i z a r ambas coordenadas


Punto ( double a , double b ) {
x = a;
y = b;
}

/ / Incrementa l a s coordenadas d e l punto


void t r a s l a d a r ( double a , double b ) {
x = x + a;
y = y + b;
}

/ / Devuelve l a d i s t a n c i a e n t r e e s t e punto y e l argumento


double d i s t a n c i a ( Punto p t o ) {
r e t u r n Math . s q r t ( Math . pow ( x − p t o . x , 2 ) +
Math . pow ( y − p t o . y , 2 ) ) ;
}

/ / Devuelve e l v a l o r de l a coordenada x
double a b s c i s a ( ) {
return x ;
}

/ / Devuelve e l v a l o r de l a coordenada y
double ordenada ( ) {
return y ;
}

/ / Cambia e l v a l o r de l a coordenada x
void a b s c i s a ( double nuevaX ) {
x = nuevaX ;
}

/ / Cambia e l v a l o r de l a coordenada y
void ordenada ( double nuevaY ) {
y = nuevaY ;
}
}

Listado 2.3: Versión C++ de la clase Punto.


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 21

class PUNTO f e a t u r e −− A t r i b u t o s que d e f i n e n l a s coordenadas


abscisa , ordenada : REAL;

c r e a t i o n −− C o n s t r u c t o r e s
origen ;
nuevo ;

f e a t u r e −− R u t i n a s
origen is
−− C o n s t r u c t o r para i n i c i a l i z a r a 0 ambas coordenadas
abscisa := 0;
ordenada : = 0 ;
end ;

nuevo ( a : REAL, b : REAL) i s


−− C o n s t r u c t o r para i n i c i a l i z a r ambas ordenadas
do
abscisa := a ;
ordenada : = b ;
end ;

t r a s l a d a r ( a : REAL, b : REAL) i s
−− Incrementa l a s coordenadas d e l punto
do
x := x + a ;
y := y + b ;
end ;

d i s t a n c i a ( p t o : PUNTO) : REAL i s
−− C a l c u l a l a d i s t a n c i a e n t r e e s t e punto y e l argumento
do
Result : = s q r t ( ( x − p t o . x ) ˆ 2 + ( y − p t o . y ) ˆ 2 ) ;
end ;

a b s c i s a ( nuevaX :REAL) i s
−− Cambia e l v a l o r de l a coordenada x
do
x : = nuevaX ;
end ;

ordenada ( nuevaY :REAL) i s


−− Cambia e l v a l o r de l a coordenada y
do
y : = nuevaY ;
end ;

end −− c l a s e PUNTO

Listado 2.4: Versión Eiffel de la clase Punto.


22 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

Figura 2.1: Descripción UML de la clase Punto.

ONML
HIJK

(1, 3)

Figura 2.2: Creación de un objeto punto p con coordenadas 1 y 3.

última versión de Eiffel, que consiste en considerar, dentro de la descripción de la clase, una
familia especial de métodos para crear instancias, denominados métodos de clase en Smalltalk
y rutinas de creación en Eiffel. Algo similar se da en lenguajes como C++ y Java, pero con la
restricción de que dichos métodos (denominados constructores) se han de denominar como las
clases, y la creación de objetos ha de realizarse invocándolos mediante el operador new. Ası́,
atendiendo a las diferentes versiones de la clase Punto la creación de un objeto se realizarı́a en
Java del modo siguiente:
Punto p = new Punto(1, 3);
donde en la misma instrucción declaramos una variable p de tipo Punto, creamos una instancia
de esta clase con coordenadas 1 y 3 y dejamos a p referenciando a dicho objeto. En C++ se harı́a
de forma muy similar, mientras que en Eiffel la secuencia de instrucciones serı́a:

p: PUNTO;
!!p.nuevo(1, 3);

y en Smalltalk:
p := Punto abscisa: 1 ordenada: 3
Gráficamente, la situación después de cualquiera de estas instrucciones podrı́a represen-
tarse como se muestra en la figura 2.2.

2.2. Métodos y mensajes


Como hemos mencionado antes, la terminologı́a utilizada para hacer referencia a las varia-
bles de estado y operaciones de una clase es distinta dependiendo del lenguaje que se esté uti-
lizando. Básicamente, la literatura distingue habitualmente tres conjuntos de términos, corres-
pondientes a los tres lenguajes orientados a objetos más extendidos. Ası́, en Smalltalk se uti-
lizan las expresiones variable de instancia y método, heredadas por Java, mientras que en Eiffel,
para referirnos a lo mismo, es usual encontrar términos como atributo y rutina; por último, en
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 23

p p

_ _−1)_ _ _/ HIJK
ONML ONML
HIJK
 
_ _ _ _ trasladar
_ _ _ (1, (1, 3) (2, 2)

Figura 2.3: Mensaje trasladar a un punto (izqda.) y cambio de estado resultante (dcha.).

C++ lo normal es hablar de dato y función miembros. En lo que sigue utilizaremos indistin-
tamente cualquiera de estas formas para hacer referencia a los componentes básicos de una
clase.
En general, a la hora de tratar la forma en que se invocan las operaciones de una clase dada,
se utiliza la metáfora del paso de mensajes. Ası́, la invocación de un método de una clase se en-
tiende como la emisión de un mensaje adecuado sobre un objeto de la clase, pero el significado
real no difiere mucho de la llamada a procedimiento, caracterı́stica de cualquier lenguaje de
programación imperativo. Una de las consecuencias del uso de esta metáfora es que el número
de argumentos de un método en un lenguaje orientado a objetos es uno menos que el número
de argumentos de una operación equivalente en un lenguaje estructurado no orientado a obje-
tos: el argumento adicional se corresponde con el receptor del mensaje. Por ejemplo, mientras
que en un lenguaje como C o Pascal, el cálculo de la distancia entre dos puntos se realiza con
una invocación como distancia (pto1,pto2), en un lenguaje como Java, la misma acción se realizarı́a
enviando a uno de los puntos un mensaje del modo siguiente: pto1.distancia (pto2).
El acceso a los datos y operaciones que agrupa una clase puede o no estar protegido. La
forma y criterios para ocultar información depende del lenguaje que se considere. Ası́, por
ejemplo, mientras en Smalltalk el criterio que se sigue está predeterminado3 , otros lenguajes,
como C++, Eiffel o Java, establecen diversos mecanismos de privatización y exportación de
rutinas, que limitan el conjunto de mensajes admisibles por los objetos de la clase, y permiten
una mejor ocultación de información.
El comportamiento de un objeto está totalmente determinado por la respuesta de los men-
sajes que acepta, y es independiente de la representación de datos utilizada para definir sus
variables de instancia. Además, el único conocimiento que el resto de objetos tiene del mis-
mo viene dado por los mensajes que puede recibir. La metáfora del paso de mensajes en la
programación orientada a objetos facilita la abstracción a dos niveles: los emisores tienen una
visión abstracta de los receptores (dada por la interfaz de la clase a la que éstos pertenecen) y,
al mismo tiempo, éstos no tienen conocimiento explı́cito de aquéllos.
Las operaciones de un objeto comparten su estado, de manera que todos los cambios que
un mensaje produce en las variables de instancia afectarán a mensajes posteriores. Ası́, las
variables de instancia no son locales a las operaciones, pero sı́ al objeto. Por ejemplo, si el
punto p previamente creado recibe un mensaje del modo siguiente:
p.trasladar(1, -1);
sus coordenadas cambiarán a (2, 2). La figura 2.3 ilustra el cambio en el estado del objeto al
que referencia p ante la recepción del dicho mensaje.
Los mensajes que se envı́an a un objeto deben corresponder a alguno de los métodos defini-
dos en la clase a la que éste pertenece. Esta correspondencia se debe reflejar en la signatura del
método: nombre (o selector) del método, número de argumentos y sus tipos. En los lenguajes
3 En Smalltalk las variables de instancia se mantienen privadas a la clase, de forma que el acceso y modificación de

éstas sólo se puede realizar a través de métodos, que siempre se consideran públicos.
24 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

Figura 2.4: Descripción UML de la clase Particula y su relación con Punto.

orientados a objetos con comprobación estática de tipos, la emisión incorrecta de un mensaje


a un objeto se detecta en tiempo de compilación. En caso contrario, los errores en tiempo de
ejecución pueden tener consecuencias inesperadas.

2.3. Herencia
La herencia permite reutilizar el comportamiento de una clase en la definición de otras nue-
vas. Las subclases de una clase heredan las operaciones y variables de ésta y pueden añnadir
nuevas operaciones y nuevas variables de instancia. La herencia es una relación entre clases,
no entre objetos, y caracterizan la programación orientada a objetos.
La herencia juega un papel muy importante en los lenguajes orientados a objetos debido a
que captura una forma de abstracción de nivel superior al de la abstracción de datos, comple-
mentándolo. Esta afirmación no contradice sin embargo la opinión de muchos autores que no
reconocen en este mecanismo el aspecto más relevante de la orientación a objetos.
La herencia puede expresar distintas relaciones entre comportamientos: clasificación, espe-
cialización, generalización y aproximación.
Por ejemplo, a partir de la clase Punto considerada en la sección anterior es posible definir
una nueva clase Partı́cula , que represente objetos que incluyen las propiedades de los puntos
en el plano, añadiendo además nuevas caracterı́sticas como, por ejemplo, la posesión de masa
y la posibilidad de calcular la fuerza de atracción entre partı́culas. Se puede observar la des-
cripción en UML de esta clase en la figura 2.4. Entre los nuevos elementos de la notación UML
que aparecen, obsérvese que la relación de herencia se representa mediante una flecha sólida
orientada desde la clase heredera hacia la heredada, y las variables estáticas se representan
subrayándolas.
El listado 2.5 muestra la declaración en Java de una clase como la descrita4 . Definiciones
similares en C++, Smalltalk y Eiffel se ofrecen en los listados 2.6, 2.7 y 2.8, respectivamente.
Conceptualmente, la relación de herencia responde a una relación de especialización entre
clases de objetos, de forma que los objetos de la clase heredera (subclase o hija) se pueden
considerar también instancias de la clase heredada (superclase o padre). En efecto, en el ejemplo
toda partı́cula es también un punto, y hereda su comportamiento; es decir, una partı́cula posee
coordenadas y es posible calcular la distancia entre dos partı́culas. La diferencia radica en que,
además, poseen masa y es posible calcular la fuerza de atracción entre dos partı́culas.
4 Obsérvese que se ha definido una variable etiquetada como final y estática para representar la constante de gravi-

tación universal. En posteriores capı́tulos se definirán los conceptos de entidad estática (o de clase) y final. Ahora nos
basta saber que una variable final y estática es una constante.
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 25

class P a r t ı́ c u l a extends Punto {


/ / Constante de g r a v i t a c i ó n u n i v e r s a l
public f i n a l s t a t i c double G = 6.67 e−11;

/ / Masa de l a p a r t ı́ c u l a
p r i v a t e double masa ;

/ / C o n s t r u c t o r para i n i c i a l i z a r a 0 coordenadas y masa


public P a r t ı́ c u l a ( ) {
masa = 0 ;
}

/ / C o n s t r u c t o r para i n i c i a l i z a r l a s coordenadas y l a masa


public P a r t ı́ c u l a ( double a , double b , double m) {
super ( a , b ) ;
masa = m;
}

/ / C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e e s t a p a r t ı́ c u l a ( t h i s )
/ / y l a p a r t ı́ c u l a que se pasa como argumento
public double a t r a c c i ó n ( P a r t ı́ c u l a p a r t ) {
r e t u r n G∗masa∗ p a r t . masa / Math . pow ( t h i s . d i s t a n c i a ( p a r t ) , 2 ) ;
}

/ / Devuelve e l v a l o r de l a masa
public double masa ( ) {
r e t u r n masa ;
}
}

Listado 2.5: Versión Java de la clase Partı́cula .


26 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class P a r t i c u l a : public Punto {


public :
/ / Constante de g r a v i t a c i ó n u n i v e r s a l
f i n a l s t a t i c double G = 6.67 e−11;

/ / Masa de l a p a r t ı́ c u l a
double masa ;

public :
/ / C o n s t r u c t o r para i n i c i a l i z a r a 0 coordenadas y masa
P a r t i c u l a ( ) : Punto ( ) {
masa = 0 ;
}

/ / C o n s t r u c t o r para i n i c i a l i z a r l a s coordenadas y l a masa


P a r t i c u l a ( double a , double b , double m) : Punto ( a , b ) {
masa = m;
}

/ / C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s
double a t r a c c i o n ( P a r t i c u l a p a r t ) {
r e t u r n G∗masa∗ p a r t . masa / pow ( t h i s . d i s t a n c i a ( p a r t ) , 2 ) ;
}

/ / Devuelve e l v a l o r de l a masa
double masa ( ) {
r e t u r n masa ;
}
}

Listado 2.6: Versión C++ de la clase Particula .


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 27

Punto s u b c l a s s : # P a r t i c u l a
instanceVariableNames : ’ masa ’
classVariableNames : ’G ’
poolDictionaries : ’ ’

! Class methods

x : unNumero y : otroNumero masa : m


” M étodo de c l a s e para c r e a r un punto con coordenadas i n i c i a l e s ”
G = 6.67 e−11.
ˆ ( s e l f new) a b s c i s a : unNumero ordenada : otroNumero ; masa : m

! I n s t a n c e methods

masa : unNumero
” M o d i f i c a l a masa de l a p a r t ı́ c u l a ”
masa : = unNumero

masa
” Devuelve l a masa de l a p a r t ı́ c u l a ”
ˆ masa

atraccion : unaParticula
” C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s ”
ˆ G∗masa ∗ ( u n a P a r t i c u l a masa ) / ( s e l f d i s t a n c i a : u n a P a r t i c u l a ) squared

Listado 2.7: Versión Smalltalk de la clase Particula .

La relación de herencia se puede dividir en distintos grupos dependiendo de sus carac-


terı́sticas. Ası́, podemos hablar de herencia estricta y no estricta, selectiva y no selectiva, o de
herencia simple y herencia múltiple. Discutimos en las secciones siguientes los distintos tipos
de herencia, y terminamos la sección con una discusión sobre las diferencias entre herencia y
delegación.

2.3.1. Herencia estricta y no estricta


En términos de su comportamiento con respecto a los métodos que se heredan, podemos
hacer la distinción entre herencia estricta y no estricta, refiriéndonos a la imposibilidad o po-
sibilidad de redefinir la implementación de un método determinado, respectivamente. Los
lenguajes orientados a objetos usuales proporcionan una herencia no estricta, es decir, permi-
ten redefinición. Por ejemplo, una clase PuntoAcotado, como la de la figura 2.5, que represente
puntos en el plano pero acotados dentro de una región rectangular, podrı́a definirse como
heredera de la clase Punto, donde las coordenadas deben mantenerse dentro del rectángulo
correspondiente. Para determinar los lı́mites de un punto “acotado” se definen dos variables
internas de tipo Punto para almacenar la esquina inferior izquierda y la esquina superior de-
recha. Además de estas variables definidas adicionalmente, la relación de herencia hace que
todo objeto de la clase PuntoAcotado contenga también las variables de la clase heredada (Punto)
y herede su comportamiento; ası́, a cualquier objeto de esta nueva clase es posible enviarle
mensajes para conocer el valor de su abscisa u ordenada, o mensajes para calcular la distancia
con otros puntos; obviamente, dadas las restricciones a las que deben acogerse los objetos de
esta clase, la versión heredada del método trasladar debe ser redefinido para que el cálculo se
28 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class PARTICULA i n h e r i t s PUNTO f e a t u r e


G: REAL i s 6.67 e−11; −− Cte . g r a v i t a c i ó n u n i v e r s a l
masa : REAL; −− A t r i b u t o que d e f i n e l a masa

c r e a t i o n −− C o n s t r u c t o r e s
cero ;
nuevo ;

f e a t u r e −− R u t i n a s
cero i s
−− C o n s t r u c t o r para i n i c i a l i z a r a 0 coordenadas y masa
do
super . o r i g e n ;
masa : = 0 ;
end ;

nuevo ( a : REAL, b : REAL, m: REAL) i s


−− C o n s t r u c t o r para i n i c i a l i z a r l a s ordenadas y l a masa
do
super . nuevo ( a , b ) ;
masa : = m;
end ;

a t r a c c i o n ( p a r t : PARTICULA ) : REAL i s
−− C a l c u l a l a f u e r z a de a t r a c c i ó n e n t r e dos p a r t ı́ c u l a s
do
Result : = G∗masa∗ p a r t . masa / Current . d i s t a n c i a ( p a r t ) ˆ 2 ;
end ;

end −− c l a s e PARTICULA

Listado 2.8: Versión Eiffel de la clase Particula .


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 29

Figura 2.5: Descripción UML de la clase PuntoAcotado.

realice de forma adecuada y garantizando que las coordenadas no superan los lı́mites estable-
cidos. En la implementación en Java de la clase PuntoAcotado que se muestra en el listado 2.9 se
ha considerado una redefinición del método que considera la zona rectangular homeomorfa a
un toro; es decir, trasladar un punto más allá de alguna de las paredes que lo acotan hace que
éste aparezca por la pared opuesta.

2.3.2. Herencia selectiva y no selectiva


También se pueden distinguir otros dos tipos de herencia, dependiendo de la capacidad o
no para decidir qué métodos se heredan y cuáles no. Mientras que la herencia selectiva (también
denominada parcial) lo permite, la no selectiva fuerza a que todos los métodos sean heredados.
Por ejemplo, una clase que representase objetos de tipo Cola podrı́a definirse como heredera de
una clase Lista, donde podrı́amos suprimir algunas operaciones, como insertar, y añadir
una operación que añadiese elementos por el final.
La herencia selectiva complica sintácticamente la definición de una clase, lo que hace que
sean pocos los lenguajes que la adoptan, y aquéllos que la incorporan, lo hagan de forma muy
limitada. En general, la idea de herencia selectiva consiste en permitir que una subclase elija
qué métodos necesitará heredar. El concepto proviene de la importación selectiva de lengua-
jes como Modula-2 o Ada. No obstante, hay lenguajes que implementan un tipo de herencia
selectiva en el que quien decide lo que se va o no a heredar es la clase base; éste es el caso de
C++ y Java, en los que la privatización de métodos y variables de instancia impide su utiliza-
ción por parte de las clases derivadas. Otros lenguajes, como Eiffel, permiten un mecanismo
de selección para resolver conflictos en presencia de herencia múltiple, como se verá en la
sección 2.3.3.

2.3.3. Herencia múltiple


Existen situaciones del mundo real en las que una misma instancia podrı́a beneficiarse del
hecho de pertenecer a dos clases distintas. Por ejemplo, los puntos coloreados poseen las carac-
terı́sticas de un punto y también de un color; de este modo, si las clases Punto y Color estuvieran
definidas, serı́a interesante poder definir un punto coloreado como instancia de ambas cla-
ses. Sin embargo, conceptual (a nivel semántico) y fı́sicamente (a nivel de implementación),
es difı́cil abarcar dentro del marco de la programación basada en clases situaciones como ésas
manteniendo una uniformidad en la relación instancia/clase. Por ello, los casos similares al
30 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class PuntoAcotado extends Punto {


/ / V a r i a b l e s que determinan e l a l t o y ancho por d e f e c t o
p r i v a t e s t a t i c double ALTO = 100 , ANCHO = 100;

/ / esquinas i n f e r i o r i z q d a ( e s q u i n a I ) y s u p e r i o r dcha ( esquinaD ) .


/ / Determinan e l r e c t á n g u l o que acota l o s puntos .
p r i v a t e Punto e s q u i n a I , esquinaD ;

/ / C o n s t r u c t o r que crea un punto con coordenadas ( 0 , 0 )


/ / y esquinas (−ANCHO/ 2 , −ALTO / 2 ) y (ANCHO/ 2 , ALTO / 2 )
public PuntoAcotado ( ) {
super ( ) ;
e s q u i n a I = new Punto(−ANCHO/ 2 , ALTO / 2 ) ;
esquinaD = new Punto (ANCHO/ 2 , ALTO / 2 ) ;
}

/ / C o n s t r u c t o r que crea un punto con coordenadas ( x , y )


/ / y esquinas ( x − ANCHO/ 2 , y − ALTO / 2 ) y ( x + ANCHO/ 2 , y + ALTO / 2 )
public PuntoAcotado ( double x , double y ) {
super ( x , y ) ;
e s q u i n a I = new Punto ( x − ANCHO/ 2 , y − ALTO / 2 ) ;
esquinaD = new Punto ( x + ANCHO/ 2 , y + ALTO / 2 ) ;
}

/ / C o n s t r u c t o r que crea un punto con coordenadas ( x , y )


/ / y esquinas ( x − ancho / 2 , y − a l t o / 2 ) y ( x + ancho / 2 , y + a l t o / 2 )
public PuntoAcotado ( double x , double y , double ancho , double a l t o ) {
super ( x , y ) ;
e s q u i n a I = new Punto ( x − ancho / 2 , y − a l t o / 2 ) ;
esquinaD = new Punto ( x + ancho / 2 , y + a l t o / 2 ) ;
}

/ / Devuelve e l ancho d e l r e c t á n g u l o
double ancho ( ) {
r e t u r n esquinaD . a b s c i s a ( ) − e s q u i n a I . a b s c i s a ( ) ;
}

/ / Devuelve e l a l t o d e l r e c t á n g u l o
double a l t o ( ) {
r e t u r n esquinaD . ordenada ( ) − e s q u i n a I . ordenada ( ) ;
}

/ ∗ La v e r s i ó n d e l m étodo t r a s l a d a r heredada debe s e r r e d e f i n i d a


para g a r a n t i z a r que e l punto no supera l o s l ı́ m i t e s . S i e s t o
ocurre , se c o n s i d e r a que e l punto c o n t i n u a por e l l ı́ m i t e opuesto .
∗/
public void t r a s l a d a r ( double a , double b ) {
double excesoX = ( a b s c i s a ( ) + a − e s q u i n a I . a b s c i s a ( ) ) % ancho ( ) ;
double excesoY = ( ordenada ( ) + b − e s q u i n a I . ordenada ( ) ) % a l t o ( ) ;
a b s c i s a ( excesoX + ( excesoX > 0 ? e s q u i n a I . a b s c i s a ( ) :
esquinaD . a b s c i s a ( ) ) ) ;
ordenada ( excesoY + ( excesoY > 0 ? e s q u i n a I . ordenada ( ) :
esquinaD . ordenada ( ) ) ) ;
}
}

Listado 2.9: Versión Java de la clase PuntoAcotado.


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 31

Figura 2.6: Definición de la clase PuntoColoreado utilizando herencia múltiple.

anterior se resuelven definiendo una clase que herede de forma simultánea de distintas clases
(figura 2.6), y creando instancias de la clase resultante.
La herencia múltiple constituye un mecanismo muy importante en la programación orien-
tada a objetos, debido al interés que suscita sobre todo en el campo de la Inteligencia Artificial
por su gran poder de clasificación, y gracias a que facilita la modularidad, reutilización y di-
seño incremental. A pesar de todo, plantea muchos problemas, tanto su implementación como
su interpretación conceptual. Ello se debe a la variedad de formas en que se puede combinar el
comportamiento de clases distintas. Los inconvenientes más importantes de la herencia múlti-
ple se presentan en presencia de lo que se denominan conflictos y herencia repetida de atributos.
Los conflictos se presentan a la hora de decidir qué versión de una operación se da cuando
una clase hereda dos o más versiones de una rutina con una misma signatura proveniente de
distintos ancestros. Ası́, por ejemplo, en la clase PuntoColoreado, ilustrada en la figura 2.3.3, apa-
rece una ambigüedad que debe ser resuelta pues el método distancia es heredado de las clases
Punto y Color. En efecto, es necesario decidir qué versión del método distancia es heredada en la
clase PuntoColoreado. En este caso, parece claro que es más interesante considerar la versión de
Punto.

Se han propuesto muchas y variadas soluciones para resolver el problema de los conflictos
en herencia múltiple; desde fijar estrategias de búsqueda en el grafo de herencia (primero en
profundidad o en amplitud), hasta proporcionar mecanismos explı́citos para evitarlos. Aun-
que lo más cómodo, desde el punto de vista del programador, es la primera solución, ésta
no resulta adecuada de forma general —fijada una estrategia de búsqueda siempre es posi-
ble encontrar situaciones que no se resuelven adecuadamente—, por lo que la tendencia de
los lenguajes orientados a objetos actuales es la de evitar la producción de conflictos. Ası́, por
ejemplo, Eiffel introduce un mecanismo de renombramiento de rutinas y de herencia selec-
tiva, mientras que C++ exige al programador el uso de un operador de ámbito para evitar
ambigüedades. En Eiffel, el método distancia procedente de la clase Color se podrı́a heredar
renombrándolo como distanciaCromática, eliminando de esta forma la ambigüedad an-
terior y manteniendo ambas versiones del método heredado. Alternativamente, Eiffel permite
utilizar herencia selectiva para seleccionar cuál de las versiones desea heredar (por ejemplo,
32 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

Figura 2.7: Duplicación de atributos en herencia repetida.

el de la clase Punto) desechando las demás. De forma similar, C++ permite la coexistencia de
ambas versiones de distancia en el contexto de la clase PuntoColoreado, pero cualquier invocación
al método debe ir precedida por el identificador de la clase de la que procede: Punto::distancia
y Color :: distancia . Otros lenguajes optan por eliminar el problema permitiendo sólo herencia
simple (Smalltalk, Java).
Otro problema existente en lenguajes que permiten herencia múltiple se da cuando se pro-
duce herencia repetida; es decir, una clase hereda de otra a través de caminos distintos. El
problema sobreviene en estos casos cuando la creación de objetos no tiene en cuenta este tipo
de situaciones (generalmente, por motivos de eficiencia) y las variables de instancia correspon-
dientes a la clase heredada por diferentes caminos son duplicadas. Este problema se produce
en C++ cuando, por ejemplo, se definen jerarquı́as de herencia como las de la figura 2.7, donde
la clase Pixel representa puntos en una pantalla que tienen asociado un determinado color. Al
crearse una instancia de esta clase en C++ se reserva memoria para las variables de instancia
heredadas de cada una de sus dos superclases directas (PuntoAcotado y PuntoColoreado, por ejem-
plo, las esquinas que delimitan el rectángulo —pantalla—), pero también para dos copias de
las variables x e y heredadas de Punto. El problema radica en que estas dos copias pueden llegar
a almacenar valores inconsistentes. Lenguajes como Eiffel no presentan este inconveniente,
y en C++ es posible resolverlo con una variante de la herencia que implementa por defecto,
denominada herencia virtual, y que es algo menos eficiente.
Obsérvese que mientras que el problema de los conflictos por ambigüedad es conceptual,
el ocasionado por la herencia repetida está motivado por una decisión de implementación, que
en el caso de C++ se toma por la constante búsqueda de la eficiencia en tiempo de ejecución.
En cualquier caso, hemos de resaltar que la herencia múltiple, aunque en útil en casos muy
especı́ficos, no es esencial como mecanismo lingüı́stico, y siempre es posible encontrar una
alternativa de diseño combinando la herencia simple y la composición (ver sección 2.4).
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 33

La herencia de clases es esencialmente una forma estática de herencia. De ese modo, las cla-
ses heredan nuevas propiedades cuando son definidas, y no en tiempo de ejecución. Una vez
que la clase se ha definido, las propiedades de sus instancias están totalmente determinadas.
Existen sistemas, sobre todo en el contexto de las bases de datos orientadas a objetos, en los
que es posible la modificación de clases en tiempo de ejecución. Eso hace que las instancias
de una clase puedan presentar diferentes propiedades en distintos momentos. No obstante,
esta posibilidad no puede confundirse con algún tipo de herencia dinámica, debido a que la
redefinición de clases no es una operación que actúe sobre los objetos, del mismo modo que
la modificación del esquema de una base de datos no puede considerarse una transacción de
la base de datos. Esto significa que los cambios dinámicos de la jerarquı́a de herencia deben
realizarse actuando sobre los objetos.

2.3.4. Delegación frente a herencia


Como alternativa a la herencia, existen lenguajes que proporcionan un mecanismo de po-
tencia expresiva similar que se denomina delegación. En términos matemáticos, los sistemas
basados en herencia consideran la diferencia entre elemento y conjunto, identificando la rela-
ción instancia/clase con la relación de pertenencia, y la relación subclase/clase con la inclu-
sión entre conjuntos. Por el contrario, los basados en delegación sólo mantienen un nivel de
abstracción: el prototipo. Este tipo de sistemas no distingue entre valores y comportamiento
a nivel del lenguaje, sino que al heredar un prototipo (delegar en él) se comparten tanto su
comportamiento como el valor de su estado.
Los lenguajes basados en la delegación eliminan la necesidad de las clases en los sistemas
de herencia a costa de incrementar su complejidad semántica. La conveniencia de uno y otro
mecanismo ha sido ampliamente discutido, y existen posturas que defienden la idea de que la
delegación tiene mayor poder expresivo que la herencia, probando cómo ésta puede ser im-
plementada por medio de aquélla, y mostrando que lo contrario no es posible. Ası́ mismo, se
ha afirmado que la eficiencia en la gestión de mensajes dada en la herencia frente a la dele-
gación queda contrarrestada con el menor gasto de memoria conseguido en los sistemas con
delegación (objetos pequeños), frente a los sistemas basados en la herencia (objetos grandes).
No obstante, otros autores han demostrado formalmente que ambos mecanismos son equiva-
lentes y han presentado optimizaciones de compilación, inherentes al mecanismo de herencia,
que convierten en ventajas lo que, en otras ocasiones, se habı́an argumentado como inconve-
nientes. Sin entrar en los detalles técnicos que determinan cuál de las dos opciones es más
adecuada, es importante observar que los productos que se han comercializado con éxito has-
ta el momento incorporan un mecanismo de herencia basado en clases, en vez de delegación
basada en prototipos.

2.4. Composición de objetos


En la mayorı́a de los ejemplos que hemos visto hasta ahora, el estado de los objetos estaba
constituido por datos de tipo básico. Sin embargo, en algunas ocasiones los objetos están a su
vez compuestos por otros objetos (éste era el caso, por ejemplo, de las variables que definı́an
los lı́mites del rectángulo en la clase PuntoAcotado). Si quisiéramos modelar vectores en el
plano, por ejemplo, podrı́amos definir una clase Vector cuyo estado estuviese compuesto por
una referencia a un objeto de tipo Punto representando el extremo del vector (el origen serı́a
34 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

Figura 2.8: Diagrama UML que define la clase Vector a partir de la clase Punto

el origen de coordenadas). En la figura 2.8 se proporciona el diagrama UML de esta clasey en


el listado 2.10 una descripción en Java correspondiente. Obsérvese en el diagrama UML de la
figura 2.8 que la relación entre las clases Vector y Punto se representa mediante una flecha que
tiene su origen en la primera y su destino en la segunda. Esta relación se denomina asociación
en UML, y suele venir etiquetada por un identificador en el destino. Existen distintos tipos de
asociación (agregación, composición) que no vamos a detallar.
Obsérvese que se podrı́a haber considerado como alternativa a esta solución una definición
basada en herencia, en la que la clase Vector heredase de la clase Punto. Esto podrı́a suponer, en
determinados casos, cierta comodidad y un aparente alto grado de reutilización. Por ejemplo,
en esta situación no hubiese sido necesario definir los métodos que calculan las proyecciones
sobre los ejes de coordenadas, pues se podrı́an haber utilizado las operaciones heredadas para
acceder a la abscisa y la ordenada de un punto. Sin embargo, este uso de la herencia (denomi-
nado herencia de implementación) es inadecuado y no debe ser considerado, pues no responde
a la relación conceptual a la que hacı́amos referencia en la sección anterior: “todo objeto de la
clase derivada debe poder considerarse también objeto de la clase heredada”. En efecto, las
caracterı́sticas de un vector no coinciden con las de un punto; en particular, la traslación de un
vector no se corresponde con la traslación de su extremo, ni tiene sentido calcular la distancia
entre dos vectores como la distancia entre sus extremos.

2.5. Polimorfismo sobre los datos y vinculación dinámica


El concepto de polimorfismo ha sido tradicionalmente asociado a las funciones, de forma
que una función polimorfa es aquélla capaz de ser aplicada uniformemente a una variedad de
objetos. En el contexto de los lenguajes orientados a objetos, el polimorfismo adquiere una rele-
vancia especial, debido a que también los datos pueden presentar cierto tipo de polimorfismo.
En efecto, un identificador puede hacer referencia a objetos de distintas clases dependiendo
del contexto, del mismo modo que un identificador de función puede representar distintas
secciones de código. Cuando el lenguaje posee esta capacidad, es decir, un identificador de
variable puede hacer referencia a objetos de distintas clases, hablamos de polimorfismo sobre los
datos. Habitualmente, cuando el lenguaje es estrictamente tipado (toda variable tiene asociado
un tipo en tiempo de compilación) esta posibilidad está restringida por la herencia, de forma
que una variable declarada de cierto tipo (es decir, clase) sólo podrá hacer referencia a objetos
de esa clase o de cualquiera de sus descendientes.
El polimorfismo facilita la reutilización de código, haciendo posible implementar software
extensible, que se pueda aplicar no sólo a objetos existentes, sino también a objetos que sean
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 35

class V e c t o r {
/ / Extremo d e l v e c t o r
p r i v a t e Punto extremo ;

/ / C o n s t r u c t o r para i n i c i a l i z a r l a s coordenadas d e l extremo


public V e c t o r ( double a , double b ) {
extremo = new Punto ( a , b ) ;
}

/ / C o n s t r u c t o r para i n i c i a l i z a r e l extremo
public V e c t o r ( Punto p t o ) {
extremo = p t o ;
}

/ / C a l c u l a e l m ódulo d e l v e c t o r
public double m ódulo ( ) {
r e t u r n (new Punto ( 0 , 0 ) ) . d i s t a n c i a ( extremo ) ;
}

/ / Devuelve l a p r o y e c c i ó n sobre e l e j e X
public double componenteX ( ) {
r e t u r n extremo . a b s c i s a ( ) ;
}

/ / Devuelve l a p r o y e c c i ó n sobre e l e j e Y
public double componenteY ( ) {
r e t u r n extremo . ordenada ( ) ;
}

/ / Devuelve e l v e c t o r o r t o g o n a l a e s t e v e c t o r
public V e c t o r o r t o g o n a l ( ) {
r e t u r n new V e c t o r (−componenteY ( ) , componenteX ( ) ) ;
}

/ / Determina s i dos v e c t o r e s son p a r a l e l o s


public boolean p a r a l e l o ( V e c t o r v ) {
r e t u r n componenteX ( ) ∗ v . componenteY ( )
== componenteY ( ) ∗ v . componenteX ( ) ;
}
}

Listado 2.10: Descripción en Java de la clase Vector.


36 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

añadidos en etapas futuras del desarrollo.


Por ejemplo, la expresión a. abrir () , siendo a una variable de una clase Archivo, puede hacer
referencia a la operación definida en dicha clase Archivo, o a su versión (posiblemente redefini-
da) en el contexto de la clase ArchivoSecuencial, heredera de la anterior. Esto se debe a
que el polimorfismo sobre los datos permite que a pueda hacer referencia a objetos de ambas
clases en distintos momentos de la ejecución. Si volvemos al ejemplo de la jerarquı́a de la clase
Punto, la declaración en Java de una variable como:
Punto pto;
no restringe la capacidad de la misma a almacenar sólo objetos de esa clase, sino que permite
cualquiera de las situaciones siguientes:
p t o = new Punto ( 1 , −1) ;
p t o = new P a r t ı́ c u l a ( 1 , −1, 100) ;
p t o = new PuntoAcotado ( ) ;

En esto consiste el polimorfismo sobre los datos: una variable declarada inicialmente para
almacenar puntos en el plano, puede también almacenar partı́culas o puntos limitados a un
rectángulo. Aunque en el ejemplo las distintas referencias se producen como consecuencia de
una asignación explı́cita, efectos similares se pueden obtener mediante el paso de parámetros o
la devolución de un resultado por parte de una función. En efecto, la función distancia definida
en la clase Punto con un parámetro formal de tipo Punto podrı́a ser invocada con una partı́cula
como parámetro real:
Punto p t o = new Punto ( ) ;
P a r t ı́ c u l a p a r t = new P a r t ı́ c u l a ( 1 , −1, 100) ;
double d i s t = p t o . d i s t a n c i a ( p a r t ) ;

La capacidad polimorfa de un lenguaje puede o no incluir sobrecargas en tiempo de eje-


cución, dependiendo de si éste permite o no vinculación dinámica. Si todos los objetos fueran
asociados estáticamente a una clase, y no se tuvieran en cuenta los posibles cambios de referen-
cia en tiempo de ejecución, el método a aplicar podrı́a determinarse en tiempo de compilación,
sin que el sistema sufriera ningún tipo de sobrecarga durante la ejecución. Sin embargo, en la
mayorı́a de los casos, es interesante que los cambios de referencia producidos en tiempo de
ejecución influyan en el cómputo. Por su utilidad, los lenguajes orientados a objetos suelen in-
cluir un mecanismo de vinculación dinámica —este es el caso de Smalltalk, Eiffel y Java—; no
obstante, dado que esto siempre introduce un coste adicional, otros lenguajes (como C++) dan
la posibilidad al programador de decidir si desea una resolución dinámica frente al comporta-
miento estático por defecto. Para ilustrar las nociones de polimorfismo de datos y vinculación
dinámica, consideremos la situación siguiente en Java:
Punto p t o ;
...
/ / Punto (0, 0) en un rectángulo de 100x100
p t o = new PuntoAcotado ( ) ;
...
double d = p t o . t r a s l a d a r ( 0 , 200) ;

La decisión sobre qué versión se ejecuta es lo que distingue la vinculación estática de la


vinculación dinámica. En el primer caso, sólo se tiene en cuenta la información conocida en
tiempo de compilación: pto está declarado como Punto y la versión del método que se considera
es la de esta clase. Esto harı́a que el efecto de trasladar llevase el punto a las coordenadas
(0, 200), fuera de los lı́mites establecidos. En el caso de la vinculación dinámica se retrasa la
decisión al momento de la ejecución: entonces se sabe que pto referencia a un objeto de la clase
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 37

PuntoAcotado y el resultado de la traslación hace que el punto vuelva a su posición inicial (0, 0),
después de rebotar sobre la pared derecha del rectángulo.
Como hemos mencionado, en lenguajes como Java, Smalltalk e Eiffel la vinculación dinámi-
ca es el mecanismo que se utiliza por defecto para resolver las llamadas a los métodos, respon-
diendo a la forma natural del comportamiento de los objetos. Otros lenguajes, como C++,
aplican por defecto vinculación estática, y necesitan declarar las funciones de una forma es-
pecial (funciones virtuales) para que se resuelvan mediante vinculación dinámica. El problema
es que esta decisión se debe tomar en la raı́z de la jerarquı́a (en el ejemplo, en la clase Punto)
cuando aún el diseñador puede no conocer la existencia de las clases herederas ( Partı́cula o
PuntoColoreado); esto es lo que en la literatura se conoce como el problema de la clarividencia en
C++.

2.6. Clases abstractas


Hay ocasiones en que no se posee información suficiente para definir de forma completa
el comportamiento de una clase. Este tipo de situaciones se produce cuando el nivel de abs-
tracción de la clase es tan alto que no es posible especificar alguna de sus operaciones. Este
tipo de clases, en las que existen operaciones cuya especificación (implementación) es par-
cial, se denominan clases abstractas y los métodos cuya implementación se desconoce métodos
abstractos5 . Por ejemplo, podrı́amos definir una clase cuyas instancias representen magnitu-
des, es decir, susceptibles de ser comparadas unas con otras. Una clase de estas caracterı́sticas
(que podrı́amos denominar Magnitud) incluirı́a métodos como menorQue, mayorQue, igualQue,
menorIgualQue o mayorIgualQue, que no podrı́an ser implementados totalmente. La clase Magnitud
presenta un nivel de abstracción excesivamente alto, y el criterio de comparación no está defi-
nido. En efecto, si se tratase de magnitudes numéricas, la forma de comparación vendrı́a dada
por el orden natural, si se tratase de fechas, la ordenación responderı́a a un criterio cronológi-
co y si se tratase de horas, el orden serı́a temporal. Sin embargo, no necesariamente todos los
métodos de una clase abstracta tienen que ser abstractos; por el contrario, puede darse el caso
de que algunos métodos puedan implementarse sin ningún problema. En el listado 2.11 puede
observarse cómo el método menorQue es abstracto, mientras que los demás se implementan en
función de éste.
Obsérvese que la clase Magnitud no define ningún constructor. Esto se debe a que no tiene
sentido (y tampoco está permitido) crear instancias de clases abstractas, por lo que el cons-
tructor no es necesario. La utilidad de este tipo de clases se manifiesta cuando otras clases la
heredan e implementan los métodos abstractos. Por ejemplo, la clase Fecha podrı́a heredar de
la clase Magnitud implementando el método menorQue de forma adecuada (en términos del año,
mes y dı́a) y heredando la definición del resto de métodos; del mismo modo, se podrı́a definir
la clase Hora, heredando también de Magnitud, donde el método abstracto se implementarı́a uti-
lizando las horas, los minutos y los segundos. Incluso podrı́amos haber considerado la clase
Vector como heredera de esta clase abstracta, considerando un vector menor que otro cuando
sus módulos presentan esa relación de orden.
Ahora bien, qué aportan las clases abstractas frente a la alternativa de definir las clases sin
implementar ni declarar los métodos abstractos. La clase Magnitud anterior ofrece una respuesta
inmediata en el caso de los lenguajes con comprobación de tipos. Efectivamente, la implemen-
5 Para referirse a este concepto, también se utilizan diferentes nombres dependiendo de los lenguajes; por ejemplo,

en C++ se denominan funciones virtuales puras, en Eiffel rutinas “diferidas” (deferred).


38 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

a b s t r a c t class Magnitud {
/ / M étodo a b s t r a c t o para comparar magnitudes .
a b s t r a c t public boolean menorQue ( Magnitud magn ) ;

/ / Una magnitud es mayor que o t r a


/ / s i é s t a es menor que l a a n t e r i o r .
public boolean mayorQue ( Magnitud magn ) {
r e t u r n magn . menorQue ( t h i s ) ;
}

/ / Una magnitud es menor o i g u a l que o t r a


/ / s i é s t a no es mayor que l a p r i m e r a .
public boolean menorIgualQue ( Magnitud magn ) {
r e t u r n ! t h i s . mayorQue ( magn ) ;
}

/ / Una magnitud es mayor o i g u a l que o t r a


/ / s i é s t a no es menor que l a p r i m e r a .
public boolean mayorIgualQue ( Magnitud magn ) {
r e t u r n ! t h i s . menorQue ( magn ) ;
}

/ / Una magnitud es i g u a l que o t r a cuando


/ / es a l a vez mayor o i g u a l y menor o i g u a l .
public boolean igualQue ( Magnitud magn ) {
r e t u r n t h i s . menorIgualQue ( magn ) && t h i s . mayorIgualQue ( magn ) ;
}
}

Listado 2.11: Definición en Java de la clase abstracta Magnitud.


D UR ÁN , G UTI ÉRREZ Y P IMENTEL 39

tación de los métodos que no son abstractos, como mayorQue, no serı́a posible si la clase no
incluyese una declaración (aunque fuese sin implementación) del método menorQue, pues el
compilador no aceptarı́a la expresión this.menorQue(magn).
Otra alternativa al uso de métodos abstractos serı́a incluirlos en la clase e implementarlos
con el cuerpo vacı́o. Por ejemplo:
public boolean menorQue ( Magnitud magn ) { }

En este caso el problema radicarı́a en la imposibilidad de garantizar que las clases Fecha
y Hora incluyan una redefinición adecuada de los métodos abstractos. Por el contrario, la eti-
queta abstract permite que el compilador avise de situaciones en las que olvidemos dar una
implementación de estos métodos en las clases herederas. Toda clase declarada subclase de
una clase heredera ha de proporcionar implementaciones de todos los métodos abstractos, o
en su defecto ser declarada abstracta.
Gracias al polimorfismo sobre los datos (capacidad que tiene una variable tipada para hacer
referencia a instancias de distintas clases en tiempo de ejecución), la existencia de clases abs-
tractas adquiere una utilidad extraordinaria en los lenguajes orientados a objetos. Por ejemplo,
serı́a posible definir un método (en alguna clase) para comprobar si un array de magnitudes
está ordenado, sin necesidad de saber si el array es de fechas, horas o vectores:
public boolean est áOrdenado ( Magnitud [ ] magnitudes ) {
boolean ordenado = t r u e ;
int i = 0;
while ( i < magnitudes . l e n g t h && ordenado )
ordenado = magnitudes [ i ] . menorIgualQue ( magnitudes [++ i ] ) ;
r e t u r n ordenado ;
}

Obsérvese que de no estar definida la clase abstracta Magnitud habrı́amos tenido que imple-
mentar una versión del método para cada tipo de magnitud. Estas situaciones, y otras que
surgirán en capı́tulos posteriores hacen de las clases abstractas una herramienta muy útil en el
diseño orientado a objetos.
La noción de interfaz de Java lleva la idea de clase abstracta al lı́mite, de forma que una
interfaz puede considerarse como una clase “completamente” abstracta; es decir, con todos sus
métodos abstractos. El beneficio de tratar con interfaces radica en que es posible definir clases
que implementen (proporcionen una definición de) los métodos de múltiples interfaces. Esto
solventa, en cierta medida, la deficiencia expresiva de la herencia simple frente a la herencia
múltiple.

2.7. Soluciones genéricas


La utilidad de las clases abstractas e interfaces se hace aún más interesante en lenguajes
donde la variabilidad sobre los tipos se explota mediante el uso de lo que se denominan clases
genéricas, cuya finalidad es la de proporcionar soluciones genéricas a determinados proble-
mas. Mientras que con el mecanismo de herencia se garantiza la reutilización factorizando las
propiedades comunes de las clases en sus ancestros, las clases genéricas obtienen ventajas si-
milares describiendo parcialmente una clase, y “parametrizando” lo que se desconoce. Estas
clases permiten describir el comportamiento de objetos cuyas operaciones dependen de otras
clases (o tipos), pero donde esta dependencia no influye en la forma de implementarlas. Por
ejemplo, la inserción en una lista no depende del tipo de elementos que contenga ésta, lo que
40 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

class I n t e r v a l o {

/ / V a r i a b l e s que d e f i n e n e l i n i c i o y e l f i n a l d e l i n t e r v a l o
Object i n i c i o , f i n ;

/ / C o n s t r u c t o r que i n i c i a l i z a l o s extremos d e l i n t e r v a l o
public I n t e r v a l o ( O b j e c t i , O b j e c t f ) {
inicio = i ;
fin = f ;
}

/ / Devuelve e l i n i c i o d e l i n t e r v a l o
Object i n i c i o ( ) {
return i n i c i o ;
}

/ / Devuelve e l f i n a l d e l i n t e r v a l o
Object f i n ( ) {
return f i n ;
}

/ / M étodo para cambiar e l f i n a l d e l i n t e r v a l o


public void c a m b i a r F i n a l ( O b j e c t n u e v o F i n a l ) {
f i n = n u ev o F i n al ;
}

/ / M étodo para cambiar e l i n i c i o d e l i n t e r v a l o


public void c a m b i a r I n i c i o ( O b j e c t n u e v o I n i c i o ) {
i n i c i o = nuevoInicio ;
}
}

Listado 2.12: Clase genérica para definir intervalos.

deberı́a permitir parametrizar una clase que especifique el tipo abstracto de datos Lista con el
tipo base de los elementos que vaya a contener. Esta parametrización puede ser explı́cita cuan-
do el lenguaje permite la definición de clases parametrizadas (Eiffel, C++ —templates— o Java
desde la versión 1.5), o simulada mediante el uso de interfaces suficientemente genéricos.
Para ilustrar la noción de clase genérica, consideremos el listado 2.12 donde se define una
clase Intervalo cuyos objetos representan intervalos que van desde un inicio hasta un final , sin
determinar el tipo de éstos. La forma de generalizar el tipo de los extremos del intervalo es
utilizar en la declaración de las variables la clase más general posible: la clase Object, de la que
todas las clases heredan.
Esto permitirı́a definir intervalos de distintos tipos (fechas, horas, puntos, . . . ). El inconve-
niente de esta forma de implementar la genericidad es que no existe manera de distinguir el
tipo de un intervalo de fechas de uno de horas, por ejemplo. Esto implica que una secuencia
de instrucciones como la siguiente:
Hora nueveAM , medioDı́a , s a l i d a N i ñ o s D e l C o l e ;
...
I n t e r v a l o r e u n i ó n = new I n t e r v a l o ( nueveAM , medioDı́a ) ;
...
boolean l l e g o T a r d e = ( r e u n i ó n . f i n ( ) ) . mayorQue ( s a l i d a N i ñ o s D e l C o l e ) ;

no sea correcta desde el punto de vista del compilador, a pesar de que todo hace pensar que
D UR ÁN , G UTI ÉRREZ Y P IMENTEL 41

class I n t e r v a l o <T> {

/ / V a r i a b l e s que d e f i n e n e l i n i c i o y e l f i n a l d e l i n t e r v a l o
T inicio , fin ;

/ / C o n s t r u c t o r que i n i c i a l i z a l o s extremos d e l i n t e r v a l o
public I n t e r v a l o ( T i , T f ) {
inicio = i ;
fin = f ;
}

/ / Devuelve e l i n i c i o d e l i n t e r v a l o
T inicio () {
return i n i c i o ;
}

/ / Devuelve e l f i n a l d e l i n t e r v a l o
T fin () {
return f i n ;
}

/ / M étodo para cambiar e l f i n a l d e l i n t e r v a l o


public void c a m b i a r F i n a l ( T n u e v o F i n a l ) {
f i n = n u ev o F i na l ;
}

/ / M étodo para cambiar e l i n i c i o d e l i n t e r v a l o


public void c a m b i a r I n i c i o ( T n u e v o I n i c i o ) {
i n i c i o = nuevoInicio ;
}
}

Listado 2.13: Versión parametrizada de la clase Intervalo .

sı́ lo es. Ello es debido a que la expresión reunión. fin () , según la definición de la clase Intervalo ,
es de tipo Object, y el mensaje mayorQue sólo puede ser recibido por una magnitud 6 . Este
inconveniente podrı́a resolverse parcialmente si en vez de la clase Object se hubiese utilizado
la clase abstracta Magnitud. El problema, en este caso, serı́a la imposibilidad de crear intervalos
de objetos que no fuesen magnitudes; por ejemplo, puntos.
Una forma alternativa de implementar este tipo de comportamiento genérico es el uso de
clases parametrizadas (disponibles en Eiffel, C++ y Java desde la versión 1.5). En el listado 2.13
se muestra una versión parametrizada de la clase Intervalo . En este caso, la forma de generalizar
el tipo de los extremos del intervalo es introducir en la definición de la clase un parámetro for-
mal (T en el ejemplo) y utilizarlo como si fuese el nombre de una clase: para definir variables,
declarar argumentos, devolver resultados, etc.
Los parámetros formales en las clases parametrizadas pueden instanciarse cuando se de-
claran variables de la clase genérica. Por ejemplo, podrı́amos definir intervalos horarios, en
cuyo caso el parámetro formal es instanciado con la clase Hora, o intervalos temporales, utili-
zando la clase Fecha, o segmentos en el plano, en cuyo caso los extremos serı́an objetos de la
clase Punto:
6 Aunque en tiempo de ejecución el objeto devuelto por la expresión reunión.fin() es medioDı́a, objeto de la

clase Hora y, por lo tanto, una magnitud, en tiempo de compilación esta información no se conoce.
42 L ABORATORIO DE T ECNOLOG ÍA DE O BJETOS

I n t e r v a l o <Hora> r e u n i ó n ;
I n t e r v a l o <Fecha> semanaSanta ;
I n t e r v a l o <Punto> segmento ;

Ahora sı́, una expresión como reunión. fin () es un objeto de tipo Hora, y como tal puede recibir
el mensaje mayorQue.
La instanciación de clases genéricas permite que el compilador garantice el buen uso de las
variables. Por ejemplo, una vez definida la variable reunión como un intervalo horario, sólo
es posible cambiar el inicio o el final del intervalo utilizando horas como argumentos. En otras
palabras, el compilador no admitirı́a un mensaje como:
r e u n i ó n . c a m b i a r F i n a l (new Punto ( 1 , 2 ) ) ;

debido a que el parámetro formal del método cambiarFinal en el contexto del objeto reunión tiene
como tipo la clase Hora.
Sin embargo, como uno se puede imaginar, no es ésta la única utilidad de este tipo de
clases. En efecto, como se verá en el capı́tulo 6 las ventajas ofrecidas por las clases genéricas
van mucho más allá.
Bibliografı́a

[Lie87] H. Lieberman. Concurrent Object-Oriented Programming in Act1. In A. Yonezawa


and M. Tokoro, editors, Object-Oriented Concurrent Programming, pages 9–36. The MIT
Press, 1987.

[US87] D. Ungar and R.B. Smith. Self: The power of simplicity. In N. Meyrowitz, editor, ACM
Conference on Object-Oriented Systems, volume 21 of SIGPLAN Notices, pages 227–242.
ACM Press/Addison-Wesley, 1987.

43

También podría gustarte