3.1 Comprendiendo la Programación Orientada a Objetos

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:

Number
   Float
      BoxedFloat64
      SmallFloat64
   Fraction
   Integer
      LargePositiveInteger
         LargeNegativeInteger
      SmallInteger

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.

 note Si selecciona el texto abs en un navegador o espacio de trabajo y hace clic con el botón derecho para abrir el menú contextual, encontrará una entrada llamada Implementors 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