12 de abril de 2020

Implementación del ADT racional.

   El Ejemplo Racional muestra una posible implementación de ADT racional definido en la entrada Tipos de Datos Abstractos (ADT).

   Las líneas 7 y 8 definen los atributos de un número racional (p / q). Los detalles relacionados con los métodos de tipo set y get (líneas 25-41), ya han sido comentados en la sección Métodos y atributos de la entrada POO (mensajes y métodos) y no se repetirán aquí.

   Por otro lado, los constructores definidos (líneas 10-23) requieren de una ampliación en su explicación, ya que difieren un poco de lo hasta ahora comentado para los constructores.

   El constructor principal o base es el definido en las líneas 18-23. Observe que dicho constructor recibe dos argumentos, mismos que representan el numerador (n) y el denominador (d) del número racional que se desea inicializar. En la línea 19, el constructor realiza una verificación del denominador (cláusula de condición), de tal forma que si éste es cero, se crea (new) y lanza (throw) una excepción (consulte la sección de Excepciones de la entrada Ejemplos selectos de transición) para indicar el error a través de la clase ArithmeticException. Note también que el mismo comportamiento es considerado en las líneas 30 y 31 para el método estableceDenominador. Una posible salida del programa al presentarse la situación anteriormente planteada, se muestra en la siguiente figura:

Una salida de la excepción generada en el Ejemplo PruebaRacional al intentar crear un número racional cuyo denominador sea cero.
 
   Los constructores de las líneas 10-12 y 14-16 se apoyan del constructor base de las líneas 18-23 para realizar la inicialización del objeto a través del uso de la cláusula this. La cláusula this es una referencia que tienen todos los objetos hacia sí mismos, por lo que en el contexto de los constructores invoca al constructor sobre cargado que corresponda con el tipo y número de argumentos que envía, que para el caso de las líneas 11 y 15 corresponden con el constructor base de las líneas 18-23.

   La implementación de las operaciones de suma y producto aparecen en las líneas 43-50 y 52-59 respectivamente (la implementación de las operaciones resta y división se dejan como ejercicio para el lector). Observe que en las líneas 46 y 47 (55 y 56) se accede directamente a los atributos p y q a través del operador punto (.), es decir, sin hacer uso de los métodos de acceso set y get; ésto es así debido a que el objeto s (m) es de la misma clase que define al método suma (multiplica), por lo que el acceso es concedido sin ningún tipo de inconveniente (recuerde lo planteado en el Ejercicio 2 de la entrada Ejercicios selectos (POO) y analice la diferencia).

   Por otro lado, note también que para el objeto (this) que recibe el mensaje suma (multiplica), los atributos p y q de las líneas 46 y 47 (55 y 56) son accedidos sin ningún tipo de operador ni mensaje, es decir, son accedidos por contexto, ya que el objeto que recibe el mensaje conoce cuáles son sus propios atributos y métodos por lo que dentro del método la expresión:

p * r.obtenDenominador( )

es equivalente a la expresión:

this.p * r.obtenDenominador( )

   Por último en lo que respecta a los método suma y multiplica, es importante resaltar que ambos métodos utilizan un objeto local al método (s y m respectivamente) para almacenar el resultado que generan y poder regresarlo como valor de retorno del método, por lo que es posible deducir que si un método requiere de variables u objetos para realizar su labor, éstos pueden declarase locales al método. En este sentido, sería un error respecto al paradigma declarar dichas variables u objetos como atributos de la clase, ya que no describen alguna propiedad o característica inherente a los objetos que deriven de la clase, sino que son sólo elementos necesarios para el almacenamiento del resultado y la realización del cálculo correspondiente a la responsabilidad (comportamiento) del método. Lo anterior se aplica aún cuando se utilizara una sola variable para almacenar el resultado de cualquiera de las cuatro operaciones aritméticas.

   Finalmente el método toString (líneas 61-63) requiere una mención aparte, ya que este método sobre escribe (razón por la cual se preservó el nombre del método) y en consecuencia re define el comportamiento previamente establecido en el método la clase Object del API de Java. Dicho método tiene como responsabilidad el regresar una representación en cadena del objeto que recibe el mensaje.

   La clase de prueba del Ejemplo Racional es la del Ejemplo PruebaRacional. Una posible salida corresponde a la mostrada en la siguiente figura:

Una posible salida de la ejecución del Ejemplo PruebaRacional. 
 
   Por último, aunque el Ejemplo PruebaRacional se explica a sí mismo, se resaltarán los siguientes aspectos:
  • Las líneas 8 y 9 crean los números racionales r1 y r2, donde r1 = 1 / 3 y r2 = 2 / 5.
  • Las líneas 13 y 14 envían los mensajes suma y multiplica respectivamente al objeto r1. Note que el argumento de ambos métodos es r2 y que al valor de retorno de dichos métodos (un número racional), le es enviado de manera implícita el mensaje toString para obtener la representación en cadena de los resultados correspondientes. Esto último sucede también con las líneas 11 y 12 pero para los objetos r1 y r2 respectivamente.

Mensajes y métodos (C++).

Mensajes y métodos (C++).
   Una de las principales inquietudes que expresan los estudiantes acerca del paradigma orientado a objetos está relacionada con los mensajes y los métodos. En este sentido, se iniciará con un ejemplo sumamente sencillo el cual irá evolucionando progresivamente con la finalidad de ilustrar dichos conceptos.

   Métodos sin argumentos.
   El Ejemplo Parvulo1 muestra la definición de la clase parvulo1, misma que no contiene atributos pero sí un método cuyo identificador o nombre es "mensaje" (línea 10).

   El método "mensaje" se encuentra definido en las líneas 13-15, y tiene como única responsabilidad la impresión en la salida estándar de una cadena (los detalles generales del funcionamiento de cout << son presentados en la entrada Un vistazo a C++). La definición de un método en C++ tiene en general la siguiente estructura:

tipo_de_retorno clase::metodo();

donde : : es el operador de resolución de alcance, y esencialmente le indica al compilador que "metodo" pertenece o está en el alcance de "clase".

   Con base en base a lo anterior, la clase parvulo1 es una especie de plantilla capaz de generar objetos con una única responsabilidad o servicio. Para poder instanciar objetos el Ejemplo Parvulo1 muestra la función main en las líneas 17-21, en donde se crea un objeto "parvulo" de la clase parvulo1 (línea 18) y se le envía el mensaje "mensaje" (línea 20). En general, esta será la estructura de la mayoría de los ejemplos que se utilizarán en el blog.

   Una vez que el objeto existe, es decir, una vez que el objeto ha sido instanciado, es posible entonces interactuar con él por medio de mensajes para solicitarle acciones que correspondan con las responsabilidades o servicios definidos para dicho objeto que, para el caso del objeto parvulo, es sólo una.

   La solicitud del único servicio que puede proporcionar el objeto parvulo se realiza a través del mensaje "mensaje", el cual es enviado (invoca la función miembro correspondiente) al objeto en la línea 20:

parvulo.mensaje();

   La expresión anterior se interpreta como: "se envía el mensaje <<mensaje>> al objeto parvulo". El envío de mensajes no es otra cosa más que la invocación explícita de un método (función miembro de la clase) a través de su identificador; es utilizar el método correspondiente para realizar una acción, misma que está relacionada con el comportamiento o las responsabilidades definidas en la clase del objeto en cuestión.

   La salida del Ejemplo Parvulo1 se muestra a continuación. Asegúrese de comprender lo descrito hasta aquí antes de continuar.

Salida del Ejemplo PruebaParvulo1.

   Métodos con argumentos.
   En esta sección se presenta una versión ligeramente distinta del Ejemplo Parvulo1, en el cual se presentó el envío de mensajes sin argumentos. Tómese el tiempo necesario para comparar el Ejemplo Parvulo2 de esta sección con el Ejemplo Parvulo1 de la sección anterior, y compruebe que son esencialmente iguales.

   El parámetro "nombre" en el método mensaje constituye la diferencia de los dos ejemplos anteriormente mencionados. En el Ejemplo Parvulo2 el método mensaje (línea 10) define ahora la capacidad de recibir un argumento de tipo cadena (un objeto de la clase string) referido por el objeto "nombre". El método "mensaje" imprime en la salida estándar un cadena conformada por un texto predefinido (línea 14), y la concatenación de la cadena referida por "nombre" y "\n".

   Por otro lado, la función main es también similar a la del Ejemplo Parvulo1 excepto en la forma en que se envía el mensaje al objeto "parvulo" (línea 21). Observe que el mensaje enviado tiene ahora una cadena como argumento, la cual es referida por el objeto "nombre" del método mensaje (línea 13) en el momento en que se le envía el mensaje "mensaje" al objeto "parvulo".

   Asegúrese de realizar una labor analítica al comparar línea a línea tanto las clases parvulo1 y parvulo2 como las respectivas funciones main, así como de comprender sus diferencias con base en lo descrito hasta ahora.

   La salida del Ejemplo Parvulo2 se muestra a continuación:


Salida del Ejemplo PruebaParvulo2.

   Note en las líneas 10 y 13 que el parámetro ha sido declarado como "const string& nombre". Esto es una conveniencia más que una regla, dicha declaración le indica al compilador que el parámetro "nombre" es una referencia constante a una cadena, esto quiere decir, que no se genera una copia de la cadena y que ésta no va a poder ser modificada dentro del método o función miembro. En general, por eficiencia, esta será la estructura que se utilice para la mayoría de los métodos de este tipo.

   Métodos y atributos.
   Por el principio de ocultación de información es conveniente que únicamente se tenga acceso a los atributos de una clase a través de su interfaz. La interfaz de un objeto está representada por sus métodos públicos (public).

   El Ejemplo Parvulo3 hace uso de dicho principio al definir, con un acceso restringido o privado (private), el atributo nombre (línea 10) para la clase parvulo3. En C++, por definición, todo lo que se defina dentro de una clase es privado a no ser que se especifique lo contrario (línea 12), por lo que es muy común omitir la palabra reservada private. Es posible intercalar los modificadores de acceso (public, protected y private), se utilizará el último definico hasta que se encuentre otro o el fin de la clase.

   Observe que la clase del ejemplo en cuestión declara también tres métodos públicos (líneas 13-15), los cuales establecen la interfaz de los objetos que sean instanciados:
  • establece_nombre: este tipo de métodos son utilizados comúnmente para ajustar o establecer el valor de un atributo; y al menos en principio, debería haber un método de este tipo por cada atributo que contenga la clase, siempre y cuando se requiera manipular desde el exterior. Este tipo de métodos son comúnmente referidos como métodos de tipo set.
  • obten_nombre: este tipo de métodos son utilizados comúnmente para recuperar u obtener el valor de un atributo, y al igual que antes, debería haber, al menos en principio, un método de este tipo por cada atributo que contenga la clase siempre y cuando se requiera visualizar desde el exterior. Este tipo de métodos son comúnmente referidos como métodos de tipo get.
  • mensaje: este ha sido descrito con anterioridad. Note que el método está definido como el del Ejemplo Parvulo1 (sin argumentos), pero funciona como el del Ejemplo Parvulo2. Asegúrese de comprender ésto antes de continuar.
   Observe que el método mensaje (líneas 26-29) se vale del método obten_nombre (línea 22) para acceder al atributo nombre pero no necesariamente tiene que ser así, ya que un método puede acceder directamente a los atributos de la clase, siempre y cuando ambos estén definidos dentro de la misma clase (encapsulamiento). Por otro lado, si el atributo tiene un nivel de acceso protegido (protected), los métodos de las clases derivadas por herencia también podrían acceder a los atributos de la clase de la que derivan (clase padre o super clase).

   Los métodos de tipo set sólo deben trabajar sobre un atributo, por lo que habitualmente sólo reciben un argumento, mismo que se corresponde con la clase (tipo) del atributo a modificar (string para el caso del Ejemplo Parvulo3). De manera análoga, los métodos de tipo get no reciben ningún tipo de argumento, y la clase de objetos que regresan está directamente relacionada con la clase (tipo) del atributo al que accederán (string para el caso del Ejemplo Parvulo3).

   Es importante hacer notar también que la clase Parvulo3, a diferencia de las anteriores, establece ya una característica representada y definida por el atributo nombre, de tal forma que los objetos derivados de ella (párvulos) compartirán dicha característica (un párvulo es un niño pequeño en edad preescolar), aunque cada uno poseerá su propia identidad (nombre).

   El Ejemplo PruebaParvulo3 muestra también la función main. Al igual que en los ejemplos anteriores, observe que en la línea 32 se define y crea el objeto parvulo.

   Las líneas 34 y 36 hacen uso del método obten_nombre a través del envío del mensaje correspondiente, mientras que la línea 35 envía el mensaje establece_nombre con un argumento específico. Finalmente, la línea 37 muestra el envío del mensaje mensaje, el cual debería resultarle ya familiar al lector.

   La salida del Ejemplo PruebaParvulo3 se muestra a continuación. Observe que inicialmente el atributo nombre del objeto parvulo aparece en blanco.

Salida del Ejemplo PruebaParvulo3.

   Métodos y constructores.
   Un constructor es un método especial que se invoca implícitamente cuando se crea o instancia un objeto. Cuando se crea un objeto, se genera la memoria necesaria para representarlo y se le inicializa por medio de un constructor. En este sentido, puede haber más de una forma de inicializar un objeto y, en consecuencia, más de un constructor.

   El identificador o nombre de los métodos constructores debe coincidir con el identificador o nombre de la clase que los define; con base en lo anterior, se tiene que puede haber más de un constructor compartiendo el mismo identificador. El mecanismo que tienen los constructores para distinguirse entre sí, es a través del número y clase o tipo de los parámetros que definen, constituyendo con ello una forma de polimorfismo comúnmente conocida como sobrecarga (la sobrecarga se trata con un poco más de detalle en la siguiente sección). En la creación del objeto, se invoca el constructor que coincida con el número y tipo de argumentos proporcionados en su definición.

   Las líneas 14 y 20-23 del Ejemplo Parvulo4 muestran la declaración y la definición respectivamente del constructor parvulo4; observe cómo el nombre del constructor coincide exactamente con el nombre de la clase. Dicho constructor define un único parámetro n el cual es un objeto de la clase string.

   La inicialización que hace el constructor Parvulo4 consiste únicamente de la asignación del objeto n al atributo representado por el objeto nombre (línea 22). Note que en C++ la forma de hacer lo anterior dentro de un constructor, es invocar al constructor correspondiente del parámetro que se desea inicializar (línea 21).

   Es importante mencionar que la labor de inicialización de un constructor en particular puede ser un proceso mucho más elaborado que el descrito hasta ahora, y que estará en función directa de las responsabilidades de inicialización con que se quiera dotar al constructor, así como de la problemática en particular que se esté resolviendo por medio del objeto en cuestión.

   Los elementos restantes de la clase Parvulo4 han sido previamente abordados en las secciones anteriores por lo que no se repetirán aquí.

   El Ejemplo Parvulo4 tiene también la función main (líneas 37-44). Observe que a diferencia de los ejemplos anteriores, en la línea 38 se proporciona un argumento al constructor, lo cual hace que desde la creación del objeto parvulo se le esté definiendo un nombre.

   Observe también cómo la secuencia de mensajes subsecuentes coincide con las del Ejemplo PruebaParvulo3, excepto que en la línea 41 del Ejemplo Parvulo4, se envía el mensaje establece_nombre al objeto parvulo para asignarle el nombre completo (con apellidos) al párvulo.

   Asegúrese de comprender antes de continuar, que la siguiente muestra la salida correspondiente a la ejecución del Ejemplo Parvulo4:


Salida del Ejemplo PruebaParvulo4.

   Sobrecarga.
   La sobrecarga (overload) es un tipo de polimorfismo que se caracteriza por la capacidad de poder definir más de un método o constructor con el mismo nombre (identificador), siendo distinguidos entre sí por el número y la clase (tipo) de los argumentos que se definen.

   El Ejemplo Parvulo5 muestra la sobrecarga de constructores. Note que las líneas 13 y 20-20 declaran y definen respectivamente el mismo constructor que el en Ejemplo Parvulo4 excepto por el nombre, y que se ha añadido o sobrecargado un nuevo constructor (líneas 15 y 24-27), el cual recibe tres argumentos que representan el nombre (n), el primer apellido (a1), y el segundo apellido (a2) de un párvulo. Note también la inicialización del atributo "nombre" en la línea 25.

   La sobrecarga de constructores se da porque ambos constructores tiene el mismo identificador (parvulo5) pero distinto número de parámetros.

   No puede existir sobrecarga para constructores o métodos con el mismo identificador y el mismo número o clase (tipo) de parámetros; tiene que haber algo que los distinga entre sí, ya que en otro caso habría ambigüedad y un buen compilador la detectará.

   Los métodos restantes del Ejemplo Parvulo5 ya ha sido comentados y descritos con anterioridad en otras secciones de esta misma entrada.

   Respecto al método main, es también muy similar a los anteriormente explicados. Únicamente cabe resaltar la creación del objeto parvulo en la línea 42. Note que ahora se le proporcionan tres argumentos al constructor, lo cual hace que el constructor utilizado sea el definido en las líneas 24-27.

   La salida del Ejemplo Parvulo5 se muestra a continuación. Compare dicha salida con la del ejemplo anterior y asegúrese de comprender la diferencia.

Salida del Ejemplo PruebaParvulo5.