Mostrando las entradas con la etiqueta ADT. Mostrar todas las entradas
Mostrando las entradas con la etiqueta ADT. Mostrar todas las entradas

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.

25 de abril de 2017

Consideraciones adicionales (colas).

   Las colas de espera, y las colas de prioridad ascendentes y descendentes tienen amplias y muy variadas aplicaciones, que van desde la simulación y modelado de situaciones relacionadas con las líneas de espera que hacemos todos los días (bancos, boletos, casetas de cobro, etc.), hasta aspectos de bajo nivel relacionado con el funcionamiento de los sistemas operativos por ejemplo.

   Las colas de prioridad son un ejemplo sumamente claro en el que la definición de la estructura de datos o ADT es independiente de la implementación, debido a que es posible implementar una cola de prioridad por ejemplo, de al menos dos maneras o enfoques posibles.

   La introducción hacia las relaciones de orden en los objetos por medio de la interfaz Comparable presentada en la entrada de Colas de prioridad (Java) resultará fundamental para las entradas siguientes, por lo que se invita al lector a estudiar detenidamente estos conceptos y complementarlos con información adicional como ejercicio y labor de investigación.

    Por otro lado pero en directa correspondencia con las relaciones de orden se proporcionan adicionalmente, para los lectores interesados en desarrollar sus habilidades en C++, dos ejemplos que podrían ser útiles para el desarrollo de los ejercicios propuestos. El primero de ellos tiene que ver con la implementación de operadores, que en el lenguaje en cuestión se denomina sobrecarga de operadores. Ambos ejemplos se basan en un ejemplo previo estudiado con anterioridad en el tema correspondiente a la implementación de la herencia, por lo que sería recomendable echarle antes un vistazo. El segundo ejemplo muestra el funcionamiento de la sobre carga de operadores en la herencia. Se insta al lector a analizarlos, comprender las diferencias y el resultado de la ejecución, con la finalidad de que le sirvan como base para el desarrollo de los ejercicios.

   Finalmente, resulta sumamente importante que el lector se tome el tiempo para la realización de los ejercicios propuestos, los cuales tienen, como todos los ejercicios del blog, la finalidad de reforzar, ampliar, desarrollar y poner en práctica tanto los conceptos como los conocimientos adquiridos. Le auguro éxito.



6 de abril de 2017

Consideraciones adicionales (pilas).

   La pila es una estructura de datos sumamente importante y ampliamente utilizada en distintas áreas no sólo de la computación. Su definición y funcionalidad es clara y simple, por lo que no es casual que haya sido la primera estructura de datos estudiada y presentada en el blog.

   Por otro lado, los conceptos expuestos en el blog respecto a los genéricos en Java y C++ resultarán fundamentales para los siguientes temas, ya que todas las estructuras de datos siguientes se basan en ellos, por lo que se invita nuevamente al lector a realizar un repaso de los conceptos relacionados con los genéricos así como a tener presente la importancia de los mismos a lo largo de las estructuras de datos subsecuentes.

   Adicionalmente, una vez que se han presentado los conceptos de pila (definición y operaciones) y una propuesta de implementación, resulta fundamental el conocer el esquema de relación que existe entre las clases más importantes involucradas en la implementación de la pila genérica estudiada. Los siguientes diagramas de clases UML presentan dicha relación respecto a las dos implementaciones realizadas:

Diagrama de clases UML para la pila genérica (Java).
 
    Insto amablemente al lector a que se tome el tiempo que considere necesario para comparar el diagrama de clases de la figura anterior con las clases en Java que implementan la pila genérica (Ejemplo Pila), el nodo genérico (Ejemplo NodoG), y la excepción de estructura de datos vacía (Ejemplo ExcepcionEDVacia).
 
Diagrama de clases UML para la pila genérica (C++).
 
    De manera similar a lo dicho en el párrafo anterior para el Ejemplo pila_genérica de C++.

   Los detalles de UML quedan fuera de los alcances del blog; sin embargo, los diagramas de clases UML de las figuras anteriores muestran una relación de composición entre la clase Pila y la clase NodoG (Nodo) de cero a muchos (0 .. *), lo cual quiere decir que una pila puede tener ninguno, uno, o muchos nodos. Por otro lado, también se muestra la relación de asociación uno a uno existente entre la clase Pila y la clases de excepción correspondientes a cada diagrama (observe también que los diagramas de clases muestran así mismo la relación de herencia que existe para las excepciones).

   Asegúrese de comprender el diagrama UML que sea de su interés respecto al lenguaje que esté utilizando así como su relación con los ejemplos citados, ya que en las entradas siguientes se utilizarán este tipo de diagramas de clases UML en complemento con la definición del ADT, para definir la implementación de la estructura de datos correspondiente.

31 de marzo de 2017

Pilas (definición y operaciones primitivas).

Definición.
   La pila es un objeto dinámico en constante cambio.

   Una pila es un conjunto ordenado de elementos en el cual se pueden insertar y eliminar elementos únicamente por un extremo: el tope de la pila.

   La característica más importante de una pila es que el último elemento insertado en ella es el primero en eliminarse, mientras que el primero que fue insertado es el último en eliminarse. Por esta razón se dice que una pila es una estructura de datos de tipo LIFO (Last In First Out).

   El proceso de inserción y eliminación de elementos puede observarse en la siguiente figura en donde se muestra, por medio de una flecha, la representación del tope de la pila. Observe cómo con cada inserción o eliminación se modifica el tope de la pila.

Crecimiento y decrecimiento de una pila.
 
    La representación mostrada en la figura anterior permite visualizar todos los elementos de la pila, sin embargo, es importante señalar que en un momento dado únicamente se tiene acceso al elemento que está siendo referido por el tope de la pila, los demás elementos permanecen ocultos, tapados por decirlo de alguna manera, por el conjunto de elementos que se encuentran encima de cada uno de ellos, con excepción del que está en el tope de la pila.

Operaciones primitivas.
   Las operaciones primitivas o fundamentales definidas sobre una pila son la inserción (push) y la eliminación (pop).
  1. El comportamiento definido para la operación push consiste en insertar un nuevo elemento en la pila, mismo que constituirá el nuevo tope de la pila.
  2. El comportamiento definido para la operación pop elimina el elemento referido por el tope de la pila modificando en consecuencia el tope de la pila. El nuevo tope de la pila se refiere ahora al elemento (si existe) que fue insertado inmediatamente antes que el elemento eliminado.
   Existen otras operaciones útiles al usar pilas, como por ejemplo, antes de aplicar la operación pop a una pila, sería conveniente verificar que la pila no esté vacía (operación ¿Está vacía?).

   Otro comportamiento deseable sería la operación "ojeada" (peek), la cual hecha un vistazo al elemento que se encuentra en el tope de la pila y lo regresa pero no lo elimina.

   Las operaciones push, pop y peek, son muy comunes para la estructura de datos pila, por lo que se recomienda conservar dichos nombres en la implementación.

28 de marzo de 2017

Ejercicios selectos (estructuras de datos).

  1. Investigue cómo es que se representan los tipos de datos primitivos en una computadora. Es preciso que conozca cómo se representa un tipo de datos int por ejemplo, ¿cuántos bytes le son asignados a un entero? Los números de punto flotante (float y double por ejemplo), también tiene una representación particular dentro de una computadora basada en los conceptos de mantisa y exponente. Investigue dichos conceptos, así como la representación física (hardware) de los tipos de datos en una computadora convencional.
  2. Modifique el Ejemplo PruebaRacional para que genere una salida como la de la siguiente figura:  . Asegúrese de probar el caso de error tanto para el constructor como para el método estableceDenominador.
  3. Modifique el Ejemplo PruebaRacional para que permita leer datos (números racionales) desde la entrada estándar (teclado).
  4. Considere el Ejemplo Racional, ¿qué valor inicial tiene el objeto Racional s y m en las líneas 44 y 53 respectivamente?
  5. Modifique el Ejemplo Racional para que, en lugar de acceder directamente a los atributos p y q de los objetos s y m (líneas 46-47 y 55-56 respectivamente), se acceda a ellos y se modifiquen sus correspondientes valores a través del método de tipo set correspondiente.
  6. Con base en lo descrito en el blog, modifique el Ejemplo Racional para que cambie la sentencia (línea 46): s.p = p * r.obtenDenominador( ) + q * r.obtenNumerador( ); por: s.p = this.p * r.obtenDenominador( ) + this.q * r.obtenNumerador( ); realice también lo correspondiente para las líneas 47, 55 y 56 respectivamente.
  7. El Ejemplo Racional implementa las operaciones de adición (suma) y multiplicación (multiplica). Para que la implementación sea completa, implemente las operaciones aritméticas faltantes de sustracción y división; utilice resta y divide respectivamente para los nombres de los métodos, y ajústese a la especificación de las operaciones definidas en la sección Especificación del ADT racional de la entrada Tipos de datos abstractos (ADT).
  8. Además de lo desarrollado en el ejercicio anterior, piense en cómo mejorar la definición del ADT racional discutido en el texto. Tome en cuenta, al menos, los siguientes aspectos:
    1. Comparación de igualdad: ¿cuándo se dice que dos números racionales son iguales? Ejemplo: 3 / 21 = 1 / 7.
    2. Simplificación: simplificar a su expresión mínima un número racional, ya sea como resultado de una operación, o simplemente como utilidad. Ejemplo: 3 / 21 => 1 / 7. Sugerencia: obtenga el MCD (Máximo Común Divisor) del numerador y denominador a través de un método privado para simplificar el racional a su expresión mínima.
    3. Cuando haga la representación en cadena del número racional (toString), considere lo siguiente: ¿qué sucede si  el resultado de la simplificación es, por ejemplo 3 / 1?, ¿tendría sentido presentarlo así?, ¿qué sucede si  el resultado de una operación es 0 / 13 por ejemplo? Si el denominador es 1, sólo presente el numerador, por otro lado, si el numerador es cero, sólo presente éste valor para tener una mejor representación en cadena del objeto Racional.
    4. Por último, pero no por ello menos importante, una versión más robusta de Racional podría ser que implementara la interfaz Comparable del API. La idea es aquí definir el método compareTo() para determinar la relación de orden entre dos números racionales; así por ejemplo si r1 = 3 / 4 y r2 = 2 / 5, r1.compareTo(r2) regresaría 1, r2.compareTo(r1) regresaría -1 y 0 si ambos son iguales o equivalentes (vea 8.1).
  9. Sean c1 y c2 dos números complejos definidos de la siguiente manera: c1 = (a + bi) y c2 = (c + di) donde a, b, c, d son números reales. Utilice las definiciones y operaciones siguientes para abstraer un ADT complejo y utilícelas para realizar una implementación de dicha entidad (como se hizo para el Ejemplo Racional). Escriba también una clase de prueba que permita probar la implementación del ADT complejo así como sus respectivas operaciones aritméticas:
    1. La suma de c1 y c2 se define como: c1 + c2 = (a + bi) + (c + di)  = (a + c) + (b + d)i.
    2. La resta de c1 y c2 se define como: c1 - c2 = (a + bi) - (c + di)  = (a - c) + (b - d)i.
    3. La multiplicación de c1 y c2 se define como: c1 * c2 = (a + bi) * (c + di)  = (ac - bd) + (ad + bc)i.
    4. La división es un poco más elaborada debido a que se racionaliza el denominador; es decir, se multiplica el numerador y el denominador por el conjugado del denominador. El conjugado de un número complejo se obtiene cambiando el signo de su componente imaginaria, es decir: si c = a + bi  es un número complejo, su conjugado está dado por c¹ = a - bi, entonces: c1 / c2 = (a + bi) / (c + di)  = [(a + bi) * (c - di)] / [(c + di) * (c - di)] = [(ac + bd) + (bc - ad)i] / (c² + d²).
  10. Considere el Ejemplo Racional2, mismo que muestra el uso de la cláusula (palabra reservada) this e implementa algunas cambios respecto del Ejemplo Racional. Estudie y compare ambos ejemplos a fin de entender sus similitudes y diferencias. Tome en cuenta que, desde el punto de vista funcional (características, servicios y comportamiento de la clase) son exactamente iguales. No olvide probar su funcionamiento.
  11. En base a la explicación dada en la sección Clases autorreferidas de la entrada Abstracción de estructuras de datos, modifique el Ejemplo Nodo de tal forma que sobre cargue el constructor para que, en caso de crear un objeto sin argumentos: Nodo nodo = new Nodo( ); se construya un objeto como el mostrado en la figura: . Utilice cero para el atributo dato y asegúrese de probar el funcionamiento de su constructor.
  12. Diseñe y construya una clase (Cuadratica) que permita instanciar (generar) objetos que representen una ecuación cuadrática de la forma ax^2 + bx + c, donde a, b y c son los coeficientes de la ecuación. Su clase deberá:
    1. Considerar el caso de intentar inicializar un objeto cuyo coeficiente a sea cero y proceder a crear y lanzar una excepción.
    2. Permitir el cálculo de las raíces reales cuando existan.
    3. Lanzar una excepción si las raíces no son reales.

22 de marzo de 2017

Consideraciones adicionales respecto a las ED y los ADT.

   Las nociones de la programación orientada a objetos fueron construidas sobre las ideas y bases teóricas de los tipos de datos abstractos (ADT).
 
   Un ADT es una abstracción sumamente útil, y está estrechamente relacionada con los principios de la orientación a objetos en el sentido en que puede ser definida en términos de las características (modeladas en atributos) y los servicios (representados por los métodos) que ofrece.

   La importancia más significativa en la idea de los ADT es la separación de las nociones de interfaz (servicios) e implementación.

   Por otro lado, la definición y operación de una estructura de datos está en directa relación con la definición de un ADT, por lo que en las secciones de las entradas siguientes, además de definir las estructuras de datos más usuales y convencionales, se presentará un tipo de implementación particular para ellas; sin embargo, es importante que el lector tenga presente que existe más de una posible implementación para una definición de un ADT determinado y que, si en la práctica debe utilizar alguna para resolver algún problema real, lo mejor será recurrir a las estructuras de datos del API o de la biblioteca estándar del lenguaje que utilice. En este blog, la implementación de dichas estructuras persigue un objetivo estrictamente didáctico, y constituyen más un medio para ilustrar el paradigma, que una implementación eficiente.

   Por último pero no menos importante, es menester mencionar que, además de la implementación de las estructuras de datos más comunes, se presentarán también algunas de las aplicaciones tradicionales más representativas, con la principal intención de poner en práctica de manera combinada, tanto los conceptos orientados a objetos, como las de las estructuras de datos propiamente dichas; al mismo que tiempo de que se somete a prueba su implementación.

21 de marzo de 2017

Abstracción de estructuras de datos.

El estudio de las estructuras de datos implica, en general, dos propósitos complementarios:
  1. Identificar y desarrollar tanto entidades como operaciones útiles relacionadas con dichas entidades. En este rubro es preciso determinar el tipo de problemas que se solucionan utilizando tales entidades y operaciones.
  2. Determinar representaciones para dichas entidades abstractas, así como implementar las operaciones abstractas en representaciones concretas.
   El primero de estos dos propósitos considera a un tipo de datos de alto nivel como un instrumento que se usa para solucionar problemas. Este propósito está también en estrecha relación con la definición de tipos de datos abstractos (ADT).

   El segundo propósito considera la implementación del tipo de dato o ADT como un problema que se va a resolver utilizando objetos o elementos existentes por medio de la herencia (relación es-un (is-a)) o la composición (relación tiene-un (has-a)).
 
    Como ya se ha mencionado, en ocasiones ninguna implementación tanto de hardware como de software puede modelar en su totalidad un concepto matemático (un tipo de dato entero o real por ejemplo), por lo que es importante conocer las limitaciones de una implementación particular, ya que una consideración fundamental de cualquier implementación es no sólo su efectividad sino también su eficiencia.

Clases autorreferidas.
   La implementación de las estructuras de datos que se desarrollarán en el blog estarán basadas en un concepto muy importante: la autorreferencia.

   En el contexto de la orientación a objetos, la autorreferencia es la capacidad que tienen los objetos de una clase de almacenar explícitamente una referencia a objetos de su misma clase, es decir, a objetos de su mismo tipo.

   Considere el Ejemplo Nodo Java (Ejemplo nodo C++), el cual muestra una clase autorreferida con dos atributos:
  1. dato: el atributo que representa el elemento a almacenar en el nodo.
  2. siguiente: el cual es una referencia a un objeto cuya clase es la misma que lo define (Nodo), y por lo tanto, es una autorreferencia.
   En el área de las estructuras de datos, un nodo es la unidad mínima de almacenamiento dentro de la estructura de datos, y puede ser tan simple o elaborado como se requiera.

   Para el caso del Ejemplo Nodo, el nodo únicamente almacena enteros primitivos (int), pero como se discurrirá más adelante, es posible representar y almacenar entidades más elaboradas que un número entero.

   Considere la creación de un objeto nodo instanciado de la clase Nodo (Java) y nodo (C++):

Nodo nodo = new Nodo(1974);    // Java: declaración y definición de "nodo"
Nodo nodo(1974);    // C++: declaración y definición de "nodo"

y su correspondiente representación mostrada en la siguiente figura:

Representación de un objeto de la clase Nodo (Ejemplo Nodo).

   En la figura anterior puede apreciarse que un objeto es en realidad una referencia a una instancia (entidad) con características definidas por la clase de la que deriva, que para el caso específico del Ejemplo en turno están dadas por los atributos dato y siguiente.
 
    En este punto quizá valga le pena recordar algo sutil pero sumamente importante: la diferencia entre declaración y definición. Para no extenderse innecesariamente de más, se tomará el caso de Java:
 
Nodo nodo;    // Declaración de "nodo" (cuadro izquierdo en la representación gráfica)
.
.
.
nodo = new Nodo(1974);    // Definición de "nodo" (objeto con dos campos en la representación gráfica)
 
es habitual, pero no necesario, que la declaración y la definición del objeto vayan juntas (como se hizo al mostrar el ejemplo de instanciación líneas arriba). Sin embargo, también es posible declarar las variables que harán referencia a los objetos, para posteriormente crear explícitamente los objetos (new) mismos que serán referidos por la variable a la que se asignan (como lo hacen los constructores). En Java no existe el concepto explícito de referencia (apuntador); sin embargo una variable cuyo tipo es una clase, en realidad es una referencia al inicio de la memoria de la instancia de una clase, por lo que cuando hablamos de un "objeto", nos valemos de una abstracción que se refiere a la instancia propiamente dicha y a la variable que la refiere.

   Ahora bien, observe la relación entre el Ejemplo Nodo y la figura anterior de su representación, y note cómo en el constructor el objeto es inicializado con el valor recibido como argumento, y con null (nullptr) para la referencia siguiente, lo cual ha sido representado en la figura anterior con una diagonal (\) para enfatizar que la referencia representada por el atributo siguiente no hace referencia inicialmente a ningún otro objeto. Observe también que la clase o tipo del objeto nodo es la misma que la del atributo siguiente, lo cual constituye la autorreferencia que es el concepto que se desea ilustrar. Asegúrese de comprender lo anterior antes de continuar.

Implementación.
   Con base en lo descrito en la sección anterior, es posible decir que las estructuras de datos consisten, de manera general, en un conjunto de nodos ordenados en una secuencia lógica como la que se muestra en la siguiente figura:

Secuencia de nodos formados con objetos autorreferidos.

   Las estructuras de datos estudiadas en las entradas siguientes tendrán en general la forma o estructura representada por esta figura.
 
   Por otro lado, el mecanismo utilizado para la inserción y la eliminación de nodos en la estructura de datos, estará en función directa de las reglas especificadas por la definición de la estructura de datos.

   Finalmente, la especificación de las características de la estructura de datos se implementarán a través de sus atributos, mientras que la especificación de las reglas de operación o de comportamiento serán implementadas por medio de sus métodos.

13 de marzo de 2017

Tipos de datos abstractos (ADT).

   Desde el punto de vista de la programación, es importante que los programadores puedan definir sus propias abstracciones de datos, de tal forma que dichas abstracciones trabajen de manera parecida a las abstracciones o a las primitivas de datos proporcionadas por el sistema subyacente.

   Un tipo de dato abstracto o ADT (Abstract Data Type) es definido por una especificación abstracta, es decir, permite especificar las propiedades lógicas y funcionales de un tipo de dato.

   Desde el punto de vista de los ADT, un tipo de dato es un conjunto de valores y un grupo de operaciones definidas sobre dichos valores. El conjunto de valores y el de las operaciones forman una estructura matemática que se implementa usando una estructura de datos particular de hardware o de software.

   El concepto de ADT está relacionado con la concepción matemática que define al tipo de datos por lo que, al definir un ADT como concepto matemático, no interesa la eficiencia del espacio o del tiempo (los cuales son aspectos relacionados con la implementación del ADT), sino las propiedades y características inherentes a él. La definición de un ADT no se relaciona en lo absoluto con los detalles de la implementación; de hecho, tal vez ni siquiera sea posible implementar un ADT particular en ningún tipo de hardware o software (piense por ejemplo en los números reales y en la propiedad de la densidad de los números reales por ejemplo).

   Un ADT consta de dos partes: una definición de valor y una definición de operador, mismas que se describen a continuación:
  1. La definición del valor establece el conjunto de valores para el ADT y consta a su vez de dos partes:
    1. Una cláusula de definición.
    2. Una cláusula de condición. Para la definición de un ADT racional por ejemplo, una cláusula de condición estaría relacionada con la restricción de que el denominador de un número racional no puede ser cero.
  2. En la definición del operador cada operador está definido como una operación abstracta con condiciones previas opcionales, y las condiciones posteriores o postcondiciones.
   Por otro lado, para la implementación de un ADT, es necesario tomar en cuenta los siguientes aspectos:
  • Hacer disponible (pública) la definición del nuevo tipo.
  • Hacer disponibles un conjunto de operaciones que puedan ser utilizadas para manipular las instancias del tipo definido (métodos públicos de servicios).
  • Proteger los datos asociados con el tipo de dato que está siendo definido (atributos privados), de tal forma que dichos datos sólo puedan ser accedidos por medio de los métodos proporcionados para ello.
  • Poder generar múltiples instancias del tipo definido, preferentemente, con más de una opción de inicialización, siempre que ésto no entre en contradicción con la definición o restricciones del tipo.
   Es importante resaltar que, asociado con un ADT particular, puede haber una o más implementaciones distintas.

   En resumen, los ADT son un mecanismo de abstracción sumamente importante y, dado que la programación orientada a objetos (POO) se fundamenta en la abstracción, es posible decir que constituye uno de los pilares de la orientación a objetos.

Especificación del ADT racional.
   Existen varios métodos para especificar un ADT, desde notaciones matemáticas hasta descripciones detalladas hechas en lenguaje natural. Lo importante de la especificación es que sea clara y no ambigüa.

   Esta sección, debido a las limitaciones de formato inherentes al blog, hará uso de algo parecido a una notación matemática apoyándose del lenguaje natural para la especificación del ADT racional.

   Sea r un número racional (Q). Se define r como el cociente de dos números enteros, es decir:


r = p / q

donde p, q son números enteros (Z) y q distinto de cero (!=0).

   Las operaciones aritméticas de suma, resta, multiplicación y división de dos números racionales se especifican a continuación.

   Sean r1 y r2 son dos números racionales (Q) definidos como:

r1 = p1 / q1 con p1, q1 números enteros (Z) y  q1 != 0

r2 = p2 / q2 con p2, q2 números enteros (Z) y  q2 != 0
entonces:
  1. r1 + r2 = (p1 / q1) + (p2 / q2) = [ (p1 * q2) + (p2 * q1) ] / (q1 * q2).
  2. r1 - r2 = (p1 / q1) - (p2 / q2) = [ (p1 * q2) - (p2 * q1) ] / (q1 * q2).
  3. r1 * r2 = (p1 / q1) * (p2 / q2) = (p1 * p2) / (q1 * q2).
  4. r1 / r2 = (p1 / q1) / (p2 / q2) = (p1 * q2) / (q1 * p2).

   Observe la especificación de los valores para el ADT racional, y la restricción que existe sobre el valor del denominador representado por q1 y q2 respectivamente.

   Finalmente, note que se ha proporcionado también la especificación de las operaciones aritméticas asociadas al ADT.