Pero, ¿qué es un objeto?
Sencillamente, un objeto tiene dos componentes:
El nombre del método se denomina selector porque se utiliza
para seleccionar qué comportamiento se invoca. Por ejemplo, en 'hello'
at: 1 put: $B, el método invocado tiene el selector #at:put: y los
argumentos 1 y $B. Todos los selectores son símbolos.
Las instancias de objetos se crean (se instancian) siguiendo un modelo o plantilla. Este modelo se conoce como su Class (clase). Todas las instancias de una clase comparten los mismos métodos y, por lo tanto, reaccionan de la misma manera.
Por ejemplo, hay una clase Fraction, pero muchas fracciones
(1/2, 1/3, 23/17, ...) que se comportan tal y como esperamos que lo
hagan las fracciones. La clase Fraction y las clases de las
que hereda definen este comportamiento común, tal y como describiremos a
continuación.
Una clase determinada declara sus variables internas –estados– y el comportamiento mediante la implementación de los métodos. Una variable es básicamente un casillero con nombre que puede contener cualquier objeto. Cada variable de instancia de una clase obtiene su propio casillero con el nombre común.
Veamos cómo se declara la clase Fraction:
Number subclass: #Fraction instanceVariableNames: 'numerator denominator' classVariableNames: '' poolDictionaries: '' category: 'Kernel-Numbers'
Como era de esperar, hay dos variables, –denominadas variables de
instancia–, para definir el numerator (numerador) y el
denominator (denominador) de una fracción. Cada instancia de
fracción tiene su propio numerador y su propio denominador.
A partir de esta declaración, observamos que existe una jerarquía en la
definición de la clase: Fraction es un tipo de
Number. Esto significa que una fracción hereda el estado interno
(variables) y el comportamiento (métodos) definidos en la clase
Number. Fraction se denomina subclase de
Number, por lo que, naturalmente, denominamos a Number
superclase de Fraction.
Una clase especifica el comportamiento de todas sus instancias. Es útil poder decir este objeto es como aquel objeto, pero con estas diferencias. En Smalltalk lo conseguimos haciendo que las clases hereden el estado y el comportamiento de las instancias de su clase padre. Esta clase hija, o subclase, sólo necesita especificar el estado y el comportamiento de sus instancias que difieren de su clase padre, conservando todos los comportamientos no modificados.
Este aspecto de la programación orientada a objetos se denomina herencia. En Cuis-Smalltalk, cada clase hereda de una clase padre.
En Smalltalk, decimos que cada objeto decide por sí mismo cómo responder a un mensaje. Esto se denomina polimorfismo. El mismo selector de mensajes puede enviarse a objetos de diferentes clases. La forma (morf) del cálculo será diferente dependiendo de la clase específica de las muchas (poli) clases posibles de objeto que entienden el mensaje.
Diferentes tipos de objetos responden al mismo mensaje #printString de
maneras diferentes, pero adecuadas.
Ya hemos visto las fracciones. Esas fracciones son objetos llamados
instancias de la clase Fraction. Para crear una instancia escribimos
5 / 4, el mecanismo se basa en el envío de mensajes y el
polimorfismo. Veamos cómo funciona.
El número 5 es un entero que recibe el mensaje #/, por lo tanto, al
observar el método / en la clase Integer, podemos ver cómo se
instancia la fracción. Vea parte de este método:
/ aNumber "Refer to the comment in Number / " | quoRem | aNumber isInteger ifTrue: ../.. ifFalse: [^ (Fraction numerator: self denominator: aNumber) reduced]]. ../..
A partir de este código fuente, apreciamos que, en algunas situaciones,
el método devuelve una fracción reducida. Podemos esperar que, en otras
situaciones, se devuelva un número entero, por ejemplo, 6 / 2.
En el ejemplo, observamos que el mensaje #numerator:denominator: se
envía a la clase Fraction, dicho mensaje hace referencia a un método
de clase que solo entiende la clase Fraction. Se espera que dicho
método devuelva una instancia de Fraction.
Prueba esto en un workspace:
Fraction numerator: 24 denominator: 21 ⇒ 24/21
Fíjate cómo la fracción resultante no se reduce. Sin embargo, sí se
reduce cuando se instancia con el mensaje #/:
24 / 21 ⇒ 8/7
Con frecuencia se utiliza un método de clase para crear una nueva
instancia a partir de una clase. En el Ejemplo 4.7, se envía el
mensaje #new a la clase OrderedCollection para crear una nueva
colección vacía; new es un método de clase.
En el Ejemplo 4.8, el mensaje #newFrom: se envía a la clase
OrderedCollection para crear una nueva colección llena de elementos de
la matriz dada en el argumento; newFrom: es otro método
de clase.
Ahora observa la jerarquía de la clase Number:
NumberFloatBoxedFloat64SmallFloat64FractionIntegerLargePositiveIntegerLargeNegativeIntegerSmallInteger
Float, Integer y Fraction son descendientes
directos de la clase Number. Ya hemos visto el mensaje
#squared enviado a instancias de enteros y fracciones:
16 squared ⇒ 256 (2 / 3) squared ⇒ 4/9
Como el mensaje #squared se envía a instancias Integer y Fraction,
el método asociado se denomina método de instancia. Este método se
define tanto en la clase Number como en la clase Fraction.
Veamos este método en Number:
Number>>squared "Answer the receiver multiplied by itself." ^ self * self
En el código de un método de instancia, self se refiere al propio
objeto, en este caso el valor del número. El símbolo ↑ (también ^)
indica que se debe devolver el siguiente valor self * self. Se podría
pronunciar ^ como «return» (devolver).
Veamos ahora este mismo método en Fraction:
Fraction>>squared
^ Fraction
numerator: numerator squared
denominator: denominator squared
Aquí se instancia una nueva fracción con el numerador y el denominador
de la instancia original al cuadrado. Este método squared
alternativo garantiza que se devuelva una instancia de fracción.
Cuando se envía el mensaje #squared a un número, se ejecutan
diferentes métodos dependiendo de si el número es una fracción u otro
tipo de número. El polimorfismo significa que la clase de cada instancia
decide cómo responderá a un mensaje concreto. Aquí, la clase Fraction
está sobrescribiendo (overriding) el método squared, definido
anteriormente en la jerarquía de clases. Si un método no se sobrescribe,
se invoca un método heredado para responder al mensaje.
Sin salir de la jerarquía Number, veamos otro ejemplo de
polimorfismo con el mensaje #abs:
-10 abs ⇒ 10 5.3 abs ⇒ 5.3 (-5 / 3) abs ⇒ 5/3
La implementación en Number no necesita mucha
explicación. Hay un #ifTrue:ifFalse: que aún no hemos comentado,
pero el código se explica por sí mismo:
Number>>abs
"Answer a Number that is the absolute value (positive magnitude) of the
receiver."
self < 0
ifTrue: [^ self negated]
ifFalse: [^ self]
Esta implementación funcionará perfectamente para las subclases de
Number. Sin embargo, hay varias clases que la sobrescriben
para casos especializados u optimizados.
Por ejemplo, en lo que respecta a los números enteros positivos grandes,
abs está vacío. De hecho, en ausencia de un valor devuelto
explícitamente, el valor devuelto por defecto es la propia instancia,
en nuestro caso la instancia LargePositiveInteger:
LargePositiveInteger>>abs
El LargeNegativeInteger sabe que es negativo y que su valor
absoluto es él mismo, pero con el signo invertido, es decir,
negated (negado):
LargeNegativeInteger>>abs ^ self negated
Estos dos métodos sobrescritos son más eficientes, ya que evitan comprobaciones innecesarias y ramificaciones ifTrue/ifFalse. El polimorfismo se utiliza a menudo para evitar comprobaciones innecesarias y ramificaciones de código.
Si selecciona el texto
absen un navegador o espacio de trabajo y hace clic con el botón derecho para abrir el menú contextual, encontrará una entrada llamadaImplementors of it. Puede seleccionarla o utilizar Ctrl-m (iMplementors) para ver cómo los distintos métodos de ‘#abs‘ utilizan el polimorfismo para especializar su respuesta y producir el resultado esperado de forma natural.
Dado que una instancia de objeto se modela mediante su clase, es posible
preguntar a cualquier objeto cuál es su clase con el mensaje
#class. Fíjate atentamente en la clase devuelta en las líneas 2 y
3:
1 class ⇒ SmallInteger
(1/3) class ⇒ Fraction
(6/2) class ⇒ SmallInteger
(1/3) asFloat class ⇒ SmallFloat64
(1.0/3) class ⇒ SmallFloat64
'Hello' class ⇒ String
('Hello' at: 1) class ⇒ Character
Ejemplo 3.1: Preguntando la clase de una instancia